Skip to content
Open
102 changes: 102 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,80 @@ AFRAME.INSPECTOR.execute('entityremove', entity);
When executed, this emits an `entityremoved` event with `entity`.
When undone, this emits an `entitycreated` event with `entity`.

### assetcreate

Create an asset element in `<a-assets>` (e.g. `a-material`, `a-mixin`, `img`, `audio`, `video`, `a-asset-item`).

Usage:

```js
AFRAME.INSPECTOR.execute('assetcreate', {
tagName: 'a-material'
});
```

or with an explicit id, attributes and a callback receiving the created element:

```js
AFRAME.INSPECTOR.execute(
'assetcreate',
{
tagName: 'img',
id: 'wood',
attributes: { src: 'wood.png', crossorigin: 'anonymous' }
},
undefined,
(img) => { console.log('created', img); }
);
```

- `tagName`: element tag to create.
- `id`: optional; a unique `<base>-N` id is generated otherwise (e.g. `material-1` for `a-material`). The id stays stable across undo/redo so later commands referencing the asset keep working.
- `attributes`: optional object of attribute name to string value.

When executed, this emits an `assetcreate` event with the created element.
When undone, this emits an `assetremove` event with the asset id.

### assetupdate

Update an attribute of an asset element. Consecutive updates to the same asset attribute are merged in history like entity updates.

Usage:

```js
AFRAME.INSPECTOR.execute('assetupdate', {
assetEl: materialEl, // element or id string
attribute: 'color',
value: 'green' // attribute string, or null to remove the attribute
});
```

When executed or undone, this emits an `assetupdate` event with `{assetEl, attribute, value}`.

Renaming an asset id is supported with `attribute: 'id'`: the command keeps resolving the element across undo/redo, and entities already referencing the applied id are re-resolved. To also update the entities referencing the asset, combine it with `entityupdate` commands in a `multi` command, referencing elements by id string so the payload stays serializable (the rename must be first; its undo runs last and re-resolves the reverted consumer references):

```js
AFRAME.INSPECTOR.execute('multi', [
['assetupdate', { assetEl: 'wood', attribute: 'id', value: 'oak' }],
['entityupdate', { entity: 'box1', component: 'material', property: 'material', value: '#oak' }]
]);
```

### assetremove

Remove an asset element from `<a-assets>`. The tag name, attributes and position are captured so undo recreates the asset in place. For `<a-material>`, entities referencing the asset are re-resolved on undo so they use the recreated `THREE.Material` instance.

Usage:

```js
AFRAME.INSPECTOR.execute('assetremove', {
assetEl: materialEl // element or id string
});
```

When executed, this emits an `assetremove` event with the asset id.
When undone, this emits an `assetcreate` event with the recreated element.

### multi

Create two entities in a row:
Expand Down Expand Up @@ -264,7 +338,35 @@ type EntityUpdateCommand = [
},
];

type AssetCreatePayload = {
tagName: string;
id?: string;
attributes?: Record<string, string>;
};
type AssetCreateCommand =
| ["assetcreate", AssetCreatePayload]
| ["assetcreate", AssetCreatePayload, (el: Element) => void];

type AssetUpdateCommand = [
"assetupdate",
{
assetEl: Element | string;
attribute: string;
value: string | null;
},
];

type AssetRemoveCommand = [
"assetremove",
{
assetEl: Element | string;
},
];

type CommandsForMulti = (
| AssetCreateCommand
| AssetRemoveCommand
| AssetUpdateCommand
| ComponentAddCommand
| ComponentRemoveCommand
| EntityCreateCommand
Expand Down
8 changes: 8 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
<a-mixin id="purple" material="color: #93648D"></a-mixin>
<a-mixin id="short" scale="1 0.5 1"></a-mixin>
<a-mixin id="yellow" material="color: #FFC65D"></a-mixin>
<a-material id="gold" color="#FFC65D" metalness="0.9" roughness="0.2"></a-material>
<a-material id="crateMat" src="#crateImg" roughness="0.9"></a-material>
<img id="crateImg" src="https://aframe.io/sample-assets/assets/images/wood/crate.gif" crossorigin="true">
<img id="floorImg" src="https://aframe.io/sample-assets/assets/images/terrain/grasslight-big.jpg" crossorigin="true">
</a-assets>
Expand All @@ -93,6 +95,12 @@
<a-box id="aBox" position="0 2 0" height="2" color="#FFC65D"></a-box>
<a-plane id="smiley" position="2.5 2 0" width="1" height="1" material="src: #canvasTexture" draw-smiley="canvas: #canvasTexture"></a-plane>

<!-- Shared material assets. -->
<a-entity id="goldBox" geometry="primitive: box" material="material: #gold" position="0 5 -2"></a-entity>
<a-entity id="goldSphere" geometry="primitive: sphere; radius: 0.5" material="material: #gold" position="1.5 5 -2"></a-entity>
<a-entity id="crateBox" geometry="primitive: box" material="material: #crateMat" position="-1.5 5 -2"></a-entity>
<a-entity id="inlineBox" geometry="primitive: box" material="material: material(shader: flat; color: #EF2D5E)" position="-3 5 -2"></a-entity>

<!-- Models. -->
<a-entity class="boxClass" geometry="primitive: box" material="src: #crateImg" position="3 4 0"></a-entity>
<a-entity class="boxClass" geometry="primitive: box" material="color: #0f0" position="4 2 4"></a-entity>
Expand Down
28 changes: 28 additions & 0 deletions src/components/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AwesomeIcon } from './AwesomeIcon';
import Events from '../lib/Events';
import ComponentsSidebar from './components/Sidebar';
import ModalTextures from './modals/ModalTextures';
import ModalMaterials from './modals/ModalMaterials';
import ModalHelp from './modals/ModalHelp';
import ModalSponsor from './modals/ModalSponsor';
import SceneGraph from './scenegraph/SceneGraph';
Expand All @@ -22,6 +23,7 @@ export default class Main extends React.Component {
isHelpOpen: false,
isModalSponsorOpen: false,
isModalTexturesOpen: false,
isModalMaterialsOpen: false,
sceneEl: AFRAME.scenes[0],
visible: {
scenegraph: true,
Expand Down Expand Up @@ -76,6 +78,17 @@ export default class Main extends React.Component {
}.bind(this)
);

Events.on(
'openmaterialsmodal',
function (selectedMaterial, materialOnClose) {
this.setState({
selectedMaterial: selectedMaterial,
isModalMaterialsOpen: true,
materialOnClose: materialOnClose
});
}.bind(this)
);

Events.on('entityselect', (entity) => {
this.setState({ entity: entity });
});
Expand All @@ -100,6 +113,13 @@ export default class Main extends React.Component {
}
};

onModalMaterialsClose = (value) => {
this.setState({ isModalMaterialsOpen: false });
if (this.state.materialOnClose) {
this.state.materialOnClose(value);
}
};

openModalSponsor = () => {
this.setState({ isModalSponsorOpen: true });
};
Expand Down Expand Up @@ -220,6 +240,14 @@ export default class Main extends React.Component {
isOpen={this.state.isModalSponsorOpen}
onClose={this.onCloseModalSponsor}
/>
<ModalMaterials
isOpen={this.state.isModalMaterialsOpen}
selectedMaterial={this.state.selectedMaterial}
pickEnabled={!!this.state.materialOnClose}
onClose={this.onModalMaterialsClose}
/>
{/* Rendered after ModalMaterials so it stacks on top when the textures
modal is opened from a map property in the materials modal. */}
<ModalTextures
isOpen={this.state.isModalTexturesOpen}
selectedTexture={this.state.selectedTexture}
Expand Down
9 changes: 9 additions & 0 deletions src/components/components/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,18 @@ export default class Component extends React.Component {
);
}

// When the material component uses a material asset (material property set),
// every other property is ignored, entirely defined by the <a-material>; only
// show the material property.
const usesMaterialAsset =
this.props.name === 'material' && !!componentData.data.material;

return Object.keys(componentData.schema)
.sort()
.filter((propertyName) => shouldShowProperty(propertyName, componentData))
.filter(
(propertyName) => !usesMaterialAsset || propertyName === 'material'
)
.map((propertyName) => (
<PropertyRow
key={propertyName}
Expand Down
12 changes: 11 additions & 1 deletion src/components/components/PropertyRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CopyToClipboardButton from '../CopyToClipboardButton';
import BooleanWidget from '../widgets/BooleanWidget';
import ColorWidget from '../widgets/ColorWidget';
import InputWidget from '../widgets/InputWidget';
import MaterialWidget from '../widgets/MaterialWidget';
import NumberWidget from '../widgets/NumberWidget';
import SelectWidget from '../widgets/SelectWidget';
import TextureWidget from '../widgets/TextureWidget';
Expand Down Expand Up @@ -68,7 +69,12 @@ export default class PropertyRow extends React.Component {
getWidget() {
const props = this.props;
const type = this.getType();
const isSelectorType = type === 'selector' || type === 'selectorAll';
// The material type behaves like a selector (`#myMaterial` or an inline
// `material(...)` definition): edit the raw string and commit on blur so
// partial selectors typed by the user are not committed keystroke by
// keystroke (which would detach the entity from its shared material).
const isSelectorType =
type === 'selector' || type === 'selectorAll' || type === 'material';

const value = isSelectorType
? props.entity.getDOMAttribute(props.componentname)?.[props.name]
Expand Down Expand Up @@ -111,6 +117,10 @@ export default class PropertyRow extends React.Component {
if (type === 'map') {
return <TextureWidget {...widgetProps} />;
}
if (type === 'material') {
// widgetProps contains onBlur (material is a selector-like type).
return <MaterialWidget {...widgetProps} />;
}

switch (type) {
case 'number': {
Expand Down
15 changes: 15 additions & 0 deletions src/components/modals/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class Modal extends React.Component {
handleGlobalKeydown = (event) => {
if (
this.state.isOpen &&
this.isTopmostModal() &&
(event.keyCode === 27 ||
(this.props.extraCloseKeyCode &&
event.keyCode === this.props.extraCloseKeyCode))
Expand All @@ -42,6 +43,16 @@ export default class Modal extends React.Component {
}
};

// Modals can be stacked (e.g., the textures modal opened from the materials
// modal); only the topmost open modal should react to ESC or outside clicks.
isTopmostModal = () => {
const openModals = document.querySelectorAll('.modal:not(.hide)');
return (
openModals.length > 0 &&
openModals[openModals.length - 1].contains(this.self.current)
);
};

shouldClickDismiss = (event) => {
var target = event.target;
// This piece of code isolates targets which are fake clicked by things
Expand All @@ -52,6 +63,10 @@ export default class Modal extends React.Component {
if (target === this.self.current || this.self.current.contains(target)) {
return false;
}
// Don't dismiss when interacting with another modal stacked on top.
if (!this.isTopmostModal()) {
return false;
}
return true;
};

Expand Down
Loading
Loading