diff --git a/package-lock.json b/package-lock.json index 16ec62b19..b5d0b4e54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "firebase-admin": "^12.1.1", "firebase-functions": "^5.0.1", "lodash-es": "^4.17.21", + "nanoid": "^5.0.7", "posthog-js": "^1.138.3", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -22483,6 +22484,24 @@ "optional": true, "peer": true }, + "node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/natives": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", diff --git a/package.json b/package.json index fe3fbad20..1c6cc7647 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "firebase-admin": "^12.1.1", "firebase-functions": "^5.0.1", "lodash-es": "^4.17.21", + "nanoid": "^5.0.7", "posthog-js": "^1.138.3", "prop-types": "^15.8.1", "react": "^18.2.0", diff --git a/src/components/street-geo.js b/src/components/street-geo.js index ee181d254..71f44d11e 100644 --- a/src/components/street-geo.js +++ b/src/components/street-geo.js @@ -129,11 +129,11 @@ AFRAME.registerComponent('street-geo', { }); google3dElement.classList.add('autocreated'); - if (AFRAME.INSPECTOR && AFRAME.INSPECTOR.opened) { - // emit play event to start loading tiles in Editor mode + if (AFRAME.INSPECTOR?.opened) { google3dElement.addEventListener( 'loaded', () => { + // emit play event to start loading tiles in Editor mode google3dElement.play(); }, { once: true } @@ -212,12 +212,21 @@ AFRAME.registerComponent('street-geo', { osm3dBuildingElement.classList.add('autocreated'); osm3dBuildingElement.setAttribute('data-ignore-raycaster', ''); - if (AFRAME.INSPECTOR && AFRAME.INSPECTOR.opened) { - // emit play event to start loading tiles in Editor mode + if (AFRAME.INSPECTOR?.opened) { osm3dElement.addEventListener( 'loaded', () => { + // emit play event to start loading tiles in Editor mode osm3dElement.play(); + }, + { once: true } + ); + } + if (AFRAME.INSPECTOR?.opened) { + osm3dBuildingElement.addEventListener( + 'loaded', + () => { + // emit play event to start loading tiles in Editor mode osm3dBuildingElement.play(); }, { once: true } diff --git a/src/editor/components/components/AddComponent.js b/src/editor/components/components/AddComponent.js index b090f2db5..465699ed1 100644 --- a/src/editor/components/components/AddComponent.js +++ b/src/editor/components/components/AddComponent.js @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Events from '../../lib/Events'; import Select from 'react-select'; import { sendMetric } from '../../services/ga'; @@ -25,8 +24,12 @@ export default class AddComponent extends React.Component { componentName = id ? `${componentName}__${id}` : componentName; } - entity.setAttribute(componentName, ''); - Events.emit('componentadd', { entity: entity, component: componentName }); + AFRAME.INSPECTOR.execute('componentadd', { + entity, + component: componentName, + value: '' + }); + sendMetric('Components', 'addComponent', componentName); }; diff --git a/src/editor/components/components/AddLayerPanel/AddLayerPanel.component.jsx b/src/editor/components/components/AddLayerPanel/AddLayerPanel.component.jsx index 6f2796f7d..beb7cd9e0 100644 --- a/src/editor/components/components/AddLayerPanel/AddLayerPanel.component.jsx +++ b/src/editor/components/components/AddLayerPanel/AddLayerPanel.component.jsx @@ -131,24 +131,12 @@ const createEntityOnPosition = (mixinId, position) => { if (previewEntity) { previewEntity.remove(); } - const newEntity = document.createElement('a-entity'); - newEntity.setAttribute('mixin', mixinId); - newEntity.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEntity); - AFRAME.INSPECTOR.selectEntity(newEntity); - }, - { once: true } - ); - newEntity.setAttribute('position', position); - const streetContainer = document.querySelector('#street-container'); - // apppend element as a child of street-container - if (streetContainer) { - streetContainer.appendChild(newEntity); - } else { - AFRAME.scenes[0].appendChild(newEntity); - } + AFRAME.INSPECTOR.execute('entitycreate', { + mixin: mixinId, + components: { + position: position + } + }); }; const createEntity = (mixinId) => { @@ -156,16 +144,10 @@ const createEntity = (mixinId) => { if (previewEntity) { previewEntity.remove(); } - const newEntity = document.createElement('a-entity'); - newEntity.setAttribute('mixin', mixinId); - newEntity.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEntity); - AFRAME.INSPECTOR.selectEntity(newEntity); - }, - { once: true } - ); + const newEntityObject = { + mixin: mixinId, + components: {} + }; const selectedElement = AFRAME.INSPECTOR.selectedEntity; const [ancestorEl, inSegment] = selectedElement @@ -176,27 +158,40 @@ const createEntity = (mixinId) => { if (selectedElement && !ancestorEl.parentEl.isScene) { // append element as a child of the entity with .custom-group class. let customGroupEl = ancestorEl.querySelector('.custom-group'); - let entityToMove; + let customGroupCreated = false; if (!customGroupEl) { customGroupEl = document.createElement('a-entity'); // .custom-group entity is a child of segment or .street-parent/.buildings-parent elements ancestorEl.appendChild(customGroupEl); customGroupEl.classList.add('custom-group'); - entityToMove = customGroupEl; - } else { - entityToMove = newEntity; + customGroupCreated = true; } - customGroupEl.appendChild(newEntity); + newEntityObject.parentEl = customGroupEl; if (inSegment) { // get elevation position Y from attribute of segment element const segmentElevationPosY = getSegmentElevationPosY(ancestorEl); // set position y by elevation level of segment - entityToMove.setAttribute('position', { y: segmentElevationPosY }); + if (customGroupCreated) { + customGroupEl.setAttribute('position', { y: segmentElevationPosY }); + newEntityObject.components.position = { x: 0, y: 0, z: 0 }; + } else { + newEntityObject.components.position = { + x: 0, + y: segmentElevationPosY, + z: 0 + }; + } } else { // if we are creating element not inside segment-parent - selectedElement.object3D.getWorldPosition(entityToMove.object3D.position); - entityToMove.object3D.parent.worldToLocal(entityToMove.object3D.position); + const pos = new THREE.Vector3(); + selectedElement.object3D.getWorldPosition(pos); + if (customGroupCreated) { + customGroupEl.object3D.parent.worldToLocal(pos); + } else { + customGroupEl.object3D.worldToLocal(pos); + } + newEntityObject.components.position = { x: pos.x, y: pos.y, z: pos.z }; } } else { const position = pickPointOnGroundPlane({ @@ -204,15 +199,9 @@ const createEntity = (mixinId) => { normalizedY: -0.1, camera: AFRAME.INSPECTOR.camera }); - newEntity.setAttribute('position', position); - const streetContainer = document.querySelector('#street-container'); - // apppend element as a child of street-container - if (streetContainer) { - streetContainer.appendChild(newEntity); - } else { - AFRAME.scenes[0].appendChild(newEntity); - } + newEntityObject.components.position = position; } + AFRAME.INSPECTOR.execute('entitycreate', newEntityObject); }; const cardMouseEnter = (mixinId) => { @@ -222,15 +211,16 @@ const cardMouseEnter = (mixinId) => { previewEntity.setAttribute('id', 'previewEntity'); AFRAME.scenes[0].appendChild(previewEntity); const dropCursorEntity = document.createElement('a-entity'); + dropCursorEntity.classList.add('hideFromSceneGraph'); dropCursorEntity.innerHTML = ` - - + - - - - + + + `; previewEntity.appendChild(dropCursorEntity); } diff --git a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js index 4c50c3313..a664a90a3 100644 --- a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js +++ b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js @@ -1,4 +1,3 @@ -import Events from '../../../lib/Events'; import { loadScript, roundCoord } from '../../../../../src/utils.js'; export function createSvgExtrudedEntity() { @@ -15,44 +14,38 @@ export function createSvgExtrudedEntity() { ` ); if (svgString && svgString !== '') { - const newEl = document.createElement('a-entity'); - newEl.setAttribute('svg-extruder', `svgString: ${svgString}`); - newEl.setAttribute('data-layer-name', 'SVG Path • My Custom Path'); - const parentEl = document.querySelector('#street-container'); - newEl.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEl); - AFRAME.INSPECTOR.selectEntity(newEl); - }, - { once: true } - ); - parentEl.appendChild(newEl); + const definition = { + element: 'a-entity', + components: { + 'svg-extruder': `svgString: ${svgString}`, + 'data-layer-name': 'SVG Path • My Custom Path' + } + }; + AFRAME.INSPECTOR.execute('entitycreate', definition); } } - export function createMapbox() { // This component accepts a long / lat and renders a plane with dimensions that // (should be) at a correct scale. const geoLayer = document.getElementById('reference-layers'); let latitude = 0; let longitude = 0; - const streetGeo = document - .getElementById('reference-layers') - ?.getAttribute('street-geo'); + const streetGeo = geoLayer?.getAttribute('street-geo'); if (streetGeo && streetGeo['latitude'] && streetGeo['longitude']) { latitude = roundCoord(parseFloat(streetGeo['latitude'])); longitude = roundCoord(parseFloat(streetGeo['longitude'])); } - geoLayer.setAttribute( - 'street-geo', - ` - latitude: ${latitude}; longitude: ${longitude}; maps: mapbox2d - ` - ); - Events.emit('entitycreated', geoLayer); + AFRAME.INSPECTOR.execute(streetGeo ? 'entityupdate' : 'componentadd', { + entity: geoLayer, + component: 'street-geo', + value: { + latitude: latitude, + longitude: longitude, + maps: 'mapbox2d' + } + }); } export function createStreetmixStreet(position, streetmixURL, hideBuildings) { @@ -64,24 +57,20 @@ export function createStreetmixStreet(position, streetmixURL, hideBuildings) { 'https://streetmix.net/kfarr/3/3dstreet-demo-street' ); } + // position the street further from the current one so as not to overlap each other if (streetmixURL && streetmixURL !== '') { - const newEl = document.createElement('a-entity'); - newEl.setAttribute('id', streetmixURL); - // position the street further from the current one so as not to overlap each other - if (position) { - newEl.setAttribute('position', position); - } else { - newEl.setAttribute('position', '0 0 -20'); - } - - newEl.setAttribute( - 'streetmix-loader', - `streetmixStreetURL: ${streetmixURL}; showBuildings: ${!hideBuildings}` - ); - const parentEl = document.querySelector('#street-container'); - parentEl.appendChild(newEl); - // update sceneGraph - Events.emit('entitycreated', newEl); + const definition = { + id: streetmixURL, + components: { + position: position ?? '0 0 -20', + 'streetmix-loader': { + streetmixStreetURL: streetmixURL, + showBuildings: !hideBuildings + } + } + }; + + AFRAME.INSPECTOR.execute('entitycreate', definition); } } @@ -131,9 +120,7 @@ export function create3DTiles() { let latitude = 0; let longitude = 0; let ellipsoidalHeight = 0; - const streetGeo = document - .getElementById('reference-layers') - ?.getAttribute('street-geo'); + const streetGeo = geoLayer?.getAttribute('street-geo'); if (streetGeo && streetGeo['latitude'] && streetGeo['longitude']) { latitude = roundCoord(parseFloat(streetGeo['latitude'])); @@ -141,14 +128,16 @@ export function create3DTiles() { ellipsoidalHeight = parseFloat(streetGeo['ellipsoidalHeight']) || 0; } - geoLayer.setAttribute( - 'street-geo', - ` - latitude: ${latitude}; longitude: ${longitude}; ellipsoidalHeight: ${ellipsoidalHeight}; maps: google3d - ` - ); - // update sceneGraph - Events.emit('entitycreated', geoLayer); + AFRAME.INSPECTOR.execute(streetGeo ? 'entityupdate' : 'componentadd', { + entity: geoLayer, + component: 'street-geo', + value: { + latitude: latitude, + longitude: longitude, + ellipsoidalHeight: ellipsoidalHeight, + maps: 'google3d' + } + }); }; if (AFRAME.components['loader-3dtiles']) { @@ -172,59 +161,38 @@ export function createCustomModel() { 'https://cdn.glitch.global/690c7ea3-3f1c-434b-8b8d-3907b16de83c/Mission_Bay_school_low_poly_model_v03_draco.glb' ); if (modelUrl && modelUrl !== '') { - const newEl = document.createElement('a-entity'); - newEl.classList.add('custom-model'); - newEl.setAttribute('gltf-model', `url(${modelUrl})`); - newEl.setAttribute('data-layer-name', 'glTF Model • My Custom Object'); - const parentEl = document.querySelector('#street-container'); - newEl.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEl); - AFRAME.INSPECTOR.selectEntity(newEl); - }, - { once: true } - ); - parentEl.appendChild(newEl); + const definition = { + class: 'custom-model', + components: { + 'gltf-model': `url(${modelUrl})`, + 'data-layer-name': 'glTF Model • My Custom Object' + } + }; + AFRAME.INSPECTOR.execute('entitycreate', definition); } } export function createPrimitiveGeometry() { - const newEl = document.createElement('a-entity'); - newEl.setAttribute('geometry', 'primitive: circle; radius: 50;'); - newEl.setAttribute('rotation', '-90 -90 0'); - newEl.setAttribute( - 'data-layer-name', - 'Plane Geometry • Traffic Circle Asphalt' - ); - newEl.setAttribute('material', 'src: #asphalt-texture; repeat: 5 5;'); - const parentEl = document.querySelector('#street-container'); - newEl.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEl); - AFRAME.INSPECTOR.selectEntity(newEl); - }, - { once: true } - ); - parentEl.appendChild(newEl); + const definition = { + 'data-layer-name': 'Plane Geometry • Traffic Circle Asphalt', + components: { + geometry: 'primitive: circle; radius: 50;', + rotation: '-90 -90 0', + material: 'src: #asphalt-texture; repeat: 5 5;' + } + }; + AFRAME.INSPECTOR.execute('entitycreate', definition); } export function createIntersection() { - const newEl = document.createElement('a-entity'); - newEl.setAttribute('intersection', ''); - newEl.setAttribute('data-layer-name', 'Street • Intersection 90º'); - newEl.setAttribute('rotation', '-90 -90 0'); - const parentEl = document.querySelector('#street-container'); - newEl.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEl); - AFRAME.INSPECTOR.selectEntity(newEl); - }, - { once: true } - ); - parentEl.appendChild(newEl); + const definition = { + 'data-layer-name': 'Street • Intersection 90º', + components: { + intersection: '', + rotation: '-90 -90 0' + } + }; + AFRAME.INSPECTOR.execute('entitycreate', definition); } export function createSplatObject() { @@ -236,21 +204,15 @@ export function createSplatObject() { ); if (modelUrl && modelUrl !== '') { - const newEl = document.createElement('a-entity'); - newEl.classList.add('splat-model'); - newEl.setAttribute('data-no-pause', ''); - newEl.setAttribute('gaussian_splatting', `src: ${modelUrl}`); - newEl.setAttribute('data-layer-name', 'Splat Model • My Custom Object'); - newEl.play(); - const parentEl = document.querySelector('#street-container'); - newEl.addEventListener( - 'loaded', - () => { - Events.emit('entitycreated', newEl); - AFRAME.INSPECTOR.selectEntity(newEl); - }, - { once: true } - ); - parentEl.appendChild(newEl); + const definition = { + class: 'splat-model', + 'data-layer-name': 'Splat Model • My Custom Object', + 'data-no-pause': '', + components: { + gaussian_splatting: `src: ${modelUrl}` + } + }; + const entity = AFRAME.INSPECTOR.execute('entitycreate', definition); + entity.play(); } } diff --git a/src/editor/components/components/Component.js b/src/editor/components/components/Component.js index 3058c3cf4..e919a7243 100644 --- a/src/editor/components/components/Component.js +++ b/src/editor/components/components/Component.js @@ -81,8 +81,7 @@ export default class Component extends React.Component { if ( confirm('Do you really want to remove component `' + componentName + '`?') ) { - this.props.entity.removeAttribute(componentName); - Events.emit('componentremove', { + AFRAME.INSPECTOR.execute('componentremove', { entity: this.props.entity, component: componentName }); diff --git a/src/editor/components/components/PropertyRow.js b/src/editor/components/components/PropertyRow.js index bc8e1da6b..493a7a570 100644 --- a/src/editor/components/components/PropertyRow.js +++ b/src/editor/components/components/PropertyRow.js @@ -12,7 +12,6 @@ import TextureWidget from '../widgets/TextureWidget'; import Vec4Widget from '../widgets/Vec4Widget'; import Vec3Widget from '../widgets/Vec3Widget'; import Vec2Widget from '../widgets/Vec2Widget'; -import { updateEntity } from '../../lib/entity'; export default class PropertyRow extends React.Component { static propTypes = { @@ -56,14 +55,13 @@ export default class PropertyRow extends React.Component { entity: props.entity, isSingle: props.isSingle, name: props.name, - // Wrap updateEntity for tracking. onChange: function (name, value) { - var propertyName = props.componentname; - if (!props.isSingle) { - propertyName += '.' + props.name; - } - - updateEntity.apply(this, [props.entity, propertyName, value]); + AFRAME.INSPECTOR.execute('entityupdate', { + entity: props.entity, + component: props.componentname, + property: !props.isSingle ? props.name : '', + value: value + }); }, value: value }; diff --git a/src/editor/components/modals/GeoModal/GeoModal.component.jsx b/src/editor/components/modals/GeoModal/GeoModal.component.jsx index d77b229f9..4b651f9da 100644 --- a/src/editor/components/modals/GeoModal/GeoModal.component.jsx +++ b/src/editor/components/modals/GeoModal/GeoModal.component.jsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect } from 'react'; -import { SavingModal } from '../SavingModal/SavingModal.component.jsx'; +import { SavingModal } from '../SavingModal'; import styles from './GeoModal.module.scss'; import { Mangnifier20Icon, Save24Icon, QR32Icon } from '../../../icons'; @@ -16,7 +16,7 @@ import { } from '@react-google-maps/api'; import GeoImg from '../../../../../ui_assets/geo.png'; import { roundCoord } from '../../../../../src/utils.js'; -import { QrCode } from '../../components/QrCode/QrCode.component.jsx'; +import { QrCode } from '../../components/QrCode'; const GeoModal = ({ isOpen, onClose }) => { const { isLoaded } = useJsApiLoader({ @@ -136,9 +136,19 @@ const GeoModal = ({ isOpen, onClose }) => { console.log(`elevation: ${data.ellipsoidalHeight}`); const geoLayer = document.getElementById('reference-layers'); - geoLayer.setAttribute( - 'street-geo', - `latitude: ${latitude}; longitude: ${longitude}; ellipsoidalHeight: ${data.ellipsoidalHeight}; orthometricHeight: ${data.orthometricHeight}; geoidHeight: ${data.geoidHeight}` + AFRAME.INSPECTOR.execute( + geoLayer.hasAttribute('street-geo') ? 'entityupdate' : 'componentadd', + { + entity: geoLayer, + component: 'street-geo', + value: { + latitude: latitude, + longitude: longitude, + ellipsoidalHeight: data.ellipsoidalHeight, + orthometricHeight: data.orthometricHeight, + geoidHeight: data.geoidHeight + } + } ); } diff --git a/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx b/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx index 2f543b391..2c812439e 100644 --- a/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx +++ b/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx @@ -9,7 +9,6 @@ import { inputStreetmix } from '../../../lib/toolbar'; import { getCommunityScenes, getUserScenes } from '../../../api/scene'; -import Events from '../../../lib/Events'; import { Load24Icon, Loader, Upload24Icon } from '../../../icons'; import { signIn } from '../../../api'; import posthog from 'posthog-js'; @@ -68,7 +67,6 @@ const ScenesModal = ({ isOpen, onClose, initialTab = 'owner', delay }) => { AFRAME.scenes[0].setAttribute('metadata', 'sceneId', sceneId); AFRAME.scenes[0].setAttribute('metadata', 'sceneTitle', sceneTitle); - Events.emit('entitycreate', { element: 'a-entity', components: {} }); STREET.notify.successMessage('Scene loaded from 3DStreet Cloud.'); onClose(); } diff --git a/src/editor/components/scenegraph/SceneGraph.js b/src/editor/components/scenegraph/SceneGraph.js index 75355ae9c..b58683435 100644 --- a/src/editor/components/scenegraph/SceneGraph.js +++ b/src/editor/components/scenegraph/SceneGraph.js @@ -1,5 +1,6 @@ /* eslint-disable no-unused-vars, react/no-danger */ import classNames from 'classnames'; +import debounce from 'lodash-es/debounce'; import PropTypes from 'prop-types'; import React from 'react'; import Events from '../../lib/Events'; @@ -9,7 +10,7 @@ import { LayersIcon, ArrowLeftIcon } from '../../icons'; import { getEntityDisplayName } from '../../lib/entity'; import posthog from 'posthog-js'; -const HIDDEN_CLASSES = ['teleportRay', 'hitEntity']; +const HIDDEN_CLASSES = ['teleportRay', 'hitEntity', 'hideFromSceneGraph']; const HIDDEN_IDS = ['dropPlane', 'previewEntity']; export default class SceneGraph extends React.Component { @@ -31,6 +32,11 @@ export default class SceneGraph extends React.Component { leftBarHide: false, selectedIndex: -1 }; + + this.rebuildEntityOptions = debounce( + this.rebuildEntityOptions.bind(this), + 0 + ); } onMixinUpdate = (detail) => { @@ -39,21 +45,31 @@ export default class SceneGraph extends React.Component { } }; + onChildAttachedDetached = (event) => { + if (this.includeInSceneGraph(event.detail.el)) { + this.rebuildEntityOptions(); + } + }; + componentDidMount() { this.rebuildEntityOptions(); - Events.on('updatescenegraph', this.rebuildEntityOptions); Events.on('entityidchange', this.rebuildEntityOptions); - Events.on('entitycreated', this.rebuildEntityOptions); - Events.on('entityclone', this.rebuildEntityOptions); Events.on('entityupdate', this.onMixinUpdate); + document.addEventListener('child-attached', this.onChildAttachedDetached); + document.addEventListener('child-detached', this.onChildAttachedDetached); } componentWillUnmount() { - Events.off('updatescenegraph', this.rebuildEntityOptions); Events.off('entityidchange', this.rebuildEntityOptions); - Events.off('entitycreated', this.rebuildEntityOptions); - Events.off('entityclone', this.rebuildEntityOptions); Events.off('entityupdate', this.onMixinUpdate); + document.removeEventListener( + 'child-attached', + this.onChildAttachedDetached + ); + document.removeEventListener( + 'child-detached', + this.onChildAttachedDetached + ); } /** @@ -90,10 +106,21 @@ export default class SceneGraph extends React.Component { } }; + includeInSceneGraph = (element) => { + return !( + element.dataset.isInspector || + !element.isEntity || + element.isInspector || + 'aframeInspector' in element.dataset || + HIDDEN_CLASSES.includes(element.className) || + HIDDEN_IDS.includes(element.id) + ); + }; + rebuildEntityOptions = () => { const entities = []; - function treeIterate(element, depth) { + const treeIterate = (element, depth) => { if (!element) { return; } @@ -102,15 +129,7 @@ export default class SceneGraph extends React.Component { for (let i = 0; i < element.children.length; i++) { let entity = element.children[i]; - if ( - entity.dataset.isInspector || - !entity.isEntity || - entity.isInspector || - 'aframeInspector' in entity.dataset || - HIDDEN_CLASSES.includes(entity.className) || - HIDDEN_IDS.includes(entity.id) || - (depth === 1 && !entity.id) - ) { + if (!this.includeInSceneGraph(entity) || (depth === 1 && !entity.id)) { continue; } @@ -122,7 +141,7 @@ export default class SceneGraph extends React.Component { treeIterate(entity, depth); } - } + }; const layers = this.props.scene.children; const orderedLayers = []; diff --git a/src/editor/components/scenegraph/Toolbar.js b/src/editor/components/scenegraph/Toolbar.js index e48865630..b05a15acf 100644 --- a/src/editor/components/scenegraph/Toolbar.js +++ b/src/editor/components/scenegraph/Toolbar.js @@ -19,7 +19,7 @@ import { SavingModal } from '../modals/SavingModal'; import { uploadThumbnailImage } from '../modals/ScreenshotModal/ScreenshotModal.component.jsx'; import { sendMetric } from '../../services/ga.js'; import posthog from 'posthog-js'; -import { UndoRedo } from '../components/UndoRedo/UndoRedo.component.jsx'; +import { UndoRedo } from '../components/UndoRedo'; // const LOCALSTORAGE_MOCAP_UI = "aframeinspectormocapuienabled"; function filterHelpers(scene, visible) { @@ -163,7 +163,6 @@ export default class Toolbar extends Component { newHandler = () => { AFRAME.INSPECTOR.selectEntity(null); STREET.utils.newScene(); - Events.emit('updatescenegraph'); }; cloudSaveHandler = async ({ doSaveAs = false }) => { @@ -339,10 +338,6 @@ export default class Toolbar extends Component { } } - addEntity() { - Events.emit('entitycreate', { element: 'a-entity', components: {} }); - } - toggleScenePlaying = () => { if (this.state.isPlaying) { AFRAME.scenes[0].pause(); diff --git a/src/editor/index.js b/src/editor/index.js index 266cdfb85..973470674 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -5,7 +5,7 @@ import { AuthProvider, GeoProvider } from './contexts'; import Events from './lib/Events'; import { AssetsLoader } from './lib/assetsLoader'; import { initCameras } from './lib/cameras'; -import { createEntity } from './lib/entity'; +import { Config } from './lib/config'; import { History } from './lib/history'; import { Shortcuts } from './lib/shortcuts'; import { Viewport } from './lib/viewport'; @@ -13,14 +13,15 @@ import { firebaseConfig } from './services/firebase.js'; import './style/index.scss'; import ReactGA from 'react-ga4'; import posthog from 'posthog-js'; +import { commandsByType } from './lib/commands/index.js'; function Inspector() { this.assetsLoader = new AssetsLoader(); this.exporters = { gltf: new GLTFExporter() }; + this.config = new Config(); this.history = new History(); this.isFirstOpen = true; this.modules = {}; - this.on = Events.on; this.opened = false; // Wait for stuff. @@ -89,7 +90,6 @@ Inspector.prototype = { this.sceneHelpers.userData.source = 'INSPECTOR'; this.sceneHelpers.visible = true; this.inspectorActive = false; - this.debugUndoRedo = false; this.viewport = new Viewport(this); @@ -107,36 +107,34 @@ Inspector.prototype = { Events.emit('objectremove', object); }, - addHelper: (function () { - return function (object) { - let helper; - - if (object instanceof THREE.Camera) { - this.cameraHelper = helper = new THREE.CameraHelper(object); - } else if (object instanceof THREE.PointLight) { - helper = new THREE.PointLightHelper(object, 1); - } else if (object instanceof THREE.DirectionalLight) { - helper = new THREE.DirectionalLightHelper(object, 1); - } else if (object instanceof THREE.SpotLight) { - helper = new THREE.SpotLightHelper(object, 1); - } else if (object instanceof THREE.HemisphereLight) { - helper = new THREE.HemisphereLightHelper(object, 1); - } else if (object instanceof THREE.SkinnedMesh) { - helper = new THREE.SkeletonHelper(object); - } else { - // no helper for this object type - return; - } + addHelper: function (object) { + let helper; + + if (object instanceof THREE.Camera) { + this.cameraHelper = helper = new THREE.CameraHelper(object); + } else if (object instanceof THREE.PointLight) { + helper = new THREE.PointLightHelper(object, 1); + } else if (object instanceof THREE.DirectionalLight) { + helper = new THREE.DirectionalLightHelper(object, 1); + } else if (object instanceof THREE.SpotLight) { + helper = new THREE.SpotLightHelper(object, 1); + } else if (object instanceof THREE.HemisphereLight) { + helper = new THREE.HemisphereLightHelper(object, 1); + } else if (object instanceof THREE.SkinnedMesh) { + helper = new THREE.SkeletonHelper(object); + } else { + // no helper for this object type + return; + } - helper.visible = false; - this.sceneHelpers.add(helper); - this.helpers[object.uuid] = helper; - // SkeletonHelper doesn't have an update method - if (helper.update) { - helper.update(); - } - }; - })(), + helper.visible = false; + this.sceneHelpers.add(helper); + this.helpers[object.uuid] = helper; + // SkeletonHelper doesn't have an update method + if (helper.update) { + helper.update(); + } + }, removeHelpers: function (object) { object.traverse((node) => { @@ -163,7 +161,7 @@ Inspector.prototype = { } // Update helper visibilities. - for (let id in this.helpers) { + for (const id in this.helpers) { this.helpers[id].visible = false; } @@ -183,7 +181,7 @@ Inspector.prototype = { initEvents: function () { window.addEventListener('keydown', (evt) => { // Alt + Ctrl + i: Shorcut to toggle the inspector - var shortcutPressed = + const shortcutPressed = evt.keyCode === 73 && ((evt.ctrlKey && evt.altKey) || evt.getModifierState('AltGraph')); if (shortcutPressed) { @@ -213,24 +211,23 @@ Inspector.prototype = { this.sceneHelpers.visible = this.inspectorActive; }); - Events.on('entitycreate', (definition) => { - createEntity(definition, (entity) => { - this.selectEntity(entity); - }); - }); - this.sceneEl.addEventListener('newScene', () => { this.history.clear(); }); document.addEventListener('child-detached', (event) => { - var entity = event.detail.el; + const entity = event.detail.el; AFRAME.INSPECTOR.removeObject(entity.object3D); }); }, - execute: function (cmd, optionalName) { - this.history.execute(cmd, optionalName); + execute: function (cmdName, payload, optionalName) { + const Cmd = commandsByType.get(cmdName); + if (!Cmd) { + console.error(`Command ${cmdName} not found`); + return; + } + return this.history.execute(new Cmd(this, payload), optionalName); }, undo: function () { diff --git a/src/editor/lib/commands/ComponentAddCommand.js b/src/editor/lib/commands/ComponentAddCommand.js new file mode 100644 index 000000000..11528b54b --- /dev/null +++ b/src/editor/lib/commands/ComponentAddCommand.js @@ -0,0 +1,44 @@ +import Events from '../Events.js'; +import { Command } from '../command.js'; +import { createUniqueId } from '../entity.js'; + +export class ComponentAddCommand extends Command { + constructor(editor, payload) { + super(editor); + + this.type = 'componentadd'; + this.name = 'Add Component'; + this.updatable = false; + + const entity = payload.entity; + if (!entity.id) { + entity.id = createUniqueId(); + } + this.entityId = entity.id; + this.component = payload.component; + this.value = payload.value; + } + + execute() { + const entity = document.getElementById(this.entityId); + if (entity) { + entity.setAttribute(this.component, this.value); + Events.emit('componentadd', { + entity: entity, + component: this.component, + value: this.value + }); + } + } + + undo() { + const entity = document.getElementById(this.entityId); + if (entity) { + entity.removeAttribute(this.component); + Events.emit('componentremove', { + entity, + component: this.component + }); + } + } +} diff --git a/src/editor/lib/commands/ComponentRemoveCommand.js b/src/editor/lib/commands/ComponentRemoveCommand.js new file mode 100644 index 000000000..18f367172 --- /dev/null +++ b/src/editor/lib/commands/ComponentRemoveCommand.js @@ -0,0 +1,50 @@ +import Events from '../Events.js'; +import { Command } from '../command.js'; +import { createUniqueId } from '../entity.js'; + +export class ComponentRemoveCommand extends Command { + constructor(editor, payload) { + super(editor); + + this.type = 'componentremove'; + this.name = 'Remove Component'; + this.updatable = false; + + const entity = payload.entity; + if (!entity.id) { + entity.id = createUniqueId(); + } + this.entityId = entity.id; + this.component = payload.component; + + const component = + entity.components[payload.component] ?? + AFRAME.components[payload.component]; + this.value = component.isSingleProperty + ? component.schema.stringify(entity.getAttribute(payload.component)) + : structuredClone(entity.getDOMAttribute(payload.component)); + } + + execute() { + const entity = document.getElementById(this.entityId); + if (entity) { + entity.removeAttribute(this.component); + Events.emit('componentremove', { + entity, + component: this.component + }); + } + } + + undo() { + const entity = document.getElementById(this.entityId); + if (entity) { + entity.setAttribute(this.component, this.value); + Events.emit('componentadd', { + entity, + component: this.component, + value: this.value + }); + } + } +} diff --git a/src/editor/lib/commands/EntityCloneCommand.js b/src/editor/lib/commands/EntityCloneCommand.js new file mode 100644 index 000000000..97cfa922d --- /dev/null +++ b/src/editor/lib/commands/EntityCloneCommand.js @@ -0,0 +1,36 @@ +import Events from '../Events.js'; +import { Command } from '../command.js'; +import { cloneEntityImpl, createUniqueId } from '../entity.js'; + +export class EntityCloneCommand extends Command { + constructor(editor, entity) { + super(editor); + + this.type = 'entityclone'; + this.name = 'Clone Entity'; + this.updatable = false; + if (!entity.id) { + entity.id = createUniqueId(); + } + this.entityIdToClone = entity.id; + this.entityId = null; + } + + execute() { + const entityToClone = document.getElementById(this.entityIdToClone); + if (entityToClone) { + const clone = cloneEntityImpl(entityToClone, this.entityId); + this.entityId = clone.id; + return clone; + } + } + + undo() { + const entity = document.getElementById(this.entityId); + if (entity) { + entity.parentNode.removeChild(entity); + Events.emit('entityremoved', entity); + this.editor.selectEntity(document.getElementById(this.entityIdToClone)); + } + } +} diff --git a/src/editor/lib/commands/EntityCreateCommand.js b/src/editor/lib/commands/EntityCreateCommand.js new file mode 100644 index 000000000..c8ceea4b0 --- /dev/null +++ b/src/editor/lib/commands/EntityCreateCommand.js @@ -0,0 +1,41 @@ +import Events from '../Events'; +import { Command } from '../command.js'; +import { createEntity } from '../entity.js'; + +export class EntityCreateCommand extends Command { + constructor(editor, definition) { + super(editor); + + this.type = 'entitycreate'; + this.name = 'Create Entity'; + this.definition = definition; + this.entityId = null; + } + + execute() { + let definition = this.definition; + const callback = (entity) => { + this.editor.selectEntity(entity); + }; + const parentEl = + this.definition.parentEl ?? + document.querySelector(this.editor.config.defaultParent); + // If we undo and redo, use the previous id so next redo actions (for example entityupdate to move the position) works correctly + if (this.entityId) { + definition = { ...this.definition, id: this.entityId }; + } + + const entity = createEntity(definition, callback, parentEl); + this.entityId = entity.id; + return entity; + } + + undo() { + const entity = document.getElementById(this.entityId); + if (entity) { + entity.parentNode.removeChild(entity); + Events.emit('entityremoved', entity); + this.editor.selectEntity(null); + } + } +} diff --git a/src/editor/lib/commands/EntityRemoveCommand.js b/src/editor/lib/commands/EntityRemoveCommand.js new file mode 100644 index 000000000..f206d09e4 --- /dev/null +++ b/src/editor/lib/commands/EntityRemoveCommand.js @@ -0,0 +1,51 @@ +import Events from '../Events'; +import { Command } from '../command.js'; +import { findClosestEntity, prepareForSerialization } from '../entity.js'; + +export class EntityRemoveCommand extends Command { + constructor(editor, entity) { + super(editor); + + this.type = 'entityremove'; + this.name = 'Remove Entity'; + this.updatable = false; + + this.entity = entity; + // Store the parent element and index for precise reinsertion + this.parentEl = entity.parentNode; + this.index = Array.from(this.parentEl.children).indexOf(entity); + } + + execute() { + const closest = findClosestEntity(this.entity); + + // Keep a clone not attached to DOM for undo + this.entity.flushToDOM(); + const clone = prepareForSerialization(this.entity); + + // Remove entity + this.entity.parentNode.removeChild(this.entity); + Events.emit('entityremoved', this.entity); + + // Replace this.entity by clone + this.entity = clone; + + this.editor.selectEntity(closest); + } + + undo() { + // Reinsert the entity at its original position using the stored index + const referenceNode = this.parentEl.children[this.index] ?? null; + this.parentEl.insertBefore(this.entity, referenceNode); + + // Emit event after entity is loaded + this.entity.addEventListener( + 'loaded', + () => { + Events.emit('entitycreated', this.entity); + this.editor.selectEntity(this.entity); + }, + { once: true } + ); + } +} diff --git a/src/editor/lib/commands/EntityUpdateCommand.js b/src/editor/lib/commands/EntityUpdateCommand.js index 0449a4255..5cd28eb6b 100644 --- a/src/editor/lib/commands/EntityUpdateCommand.js +++ b/src/editor/lib/commands/EntityUpdateCommand.js @@ -1,25 +1,5 @@ -import Events from '../Events'; import { Command } from '../command.js'; - -function updateEntity(entity, component, property, value) { - if (property) { - if (value === null || value === undefined) { - // Remove property. - entity.removeAttribute(component, property); - } else { - // Set property. - entity.setAttribute(component, property, value); - } - } else { - if (value === null || value === undefined) { - // Remove component. - entity.removeAttribute(component); - } else { - // Set component. - entity.setAttribute(component, value); - } - } -} +import { createUniqueId, updateEntity } from '../entity.js'; /** * @param editor Editor @@ -30,18 +10,22 @@ export class EntityUpdateCommand extends Command { constructor(editor, payload) { super(editor); - this.type = 'EntityUpdateCommand'; + this.type = 'entityupdate'; this.name = 'Update Entity'; this.updatable = true; - this.entity = payload.entity; + const entity = payload.entity; + if (!entity.id) { + entity.id = createUniqueId(); + } + this.entityId = entity.id; this.component = payload.component; - this.property = payload.property; + this.property = payload.property ?? ''; const component = - this.entity.components[payload.component] ?? + entity.components[payload.component] ?? AFRAME.components[payload.component]; - // First try to get `this.entity.components[payload.component]` to have the dynamic schema, and fallback to `AFRAME.components[payload.component]` if not found. + // First try to get `entity.components[payload.component]` to have the dynamic schema, and fallback to `AFRAME.components[payload.component]` if not found. // This is to properly stringify some properties that uses for example vec2 or vec3 on material component. // This is important to fallback to `AFRAME.components[payload.component]` for primitive components position rotation and scale // that may not have been created initially on the entity. @@ -61,15 +45,19 @@ export class EntityUpdateCommand extends Command { payload.property ]; } - if (this.editor.debugUndoRedo) { + if (this.editor.config.debugUndoRedo) { console.log(this.component, this.oldValue, this.newValue); } } else { - this.newValue = component.schema.stringify(payload.value); - this.oldValue = component.schema.stringify( - payload.entity.getAttribute(payload.component) - ); - if (this.editor.debugUndoRedo) { + this.newValue = component.isSingleProperty + ? component.schema.stringify(payload.value) + : payload.value; + this.oldValue = component.isSingleProperty + ? component.schema.stringify( + payload.entity.getAttribute(payload.component) + ) + : structuredClone(payload.entity.getDOMAttribute(payload.component)); + if (this.editor.config.debugUndoRedo) { console.log(this.component, this.oldValue, this.newValue); } } @@ -77,43 +65,34 @@ export class EntityUpdateCommand extends Command { } execute() { - if (this.editor.debugUndoRedo) { - console.log( - 'execute', - this.entity, - this.component, - this.property, - this.newValue - ); + const entity = document.getElementById(this.entityId); + if (entity) { + if (this.editor.config.debugUndoRedo) { + console.log( + 'execute', + entity, + this.component, + this.property, + this.newValue + ); + } + updateEntity(entity, this.component, this.property, this.newValue); } - updateEntity(this.entity, this.component, this.property, this.newValue); - Events.emit('entityupdate', { - entity: this.entity, - component: this.component, - property: this.property, - value: this.newValue - }); } undo() { - if ( - this.editor.selectedEntity && - this.editor.selectedEntity !== this.entity - ) { - // If the selected entity is not the entity we are undoing, select the entity. - this.editor.selectEntity(this.entity); + const entity = document.getElementById(this.entityId); + if (entity) { + if (this.editor.selectedEntity && this.editor.selectedEntity !== entity) { + // If the selected entity is not the entity we are undoing, select the entity. + this.editor.selectEntity(entity); + } + updateEntity(entity, this.component, this.property, this.oldValue); } - updateEntity(this.entity, this.component, this.property, this.oldValue); - Events.emit('entityupdate', { - entity: this.entity, - component: this.component, - property: this.property, - value: this.oldValue - }); } update(command) { - if (this.editor.debugUndoRedo) { + if (this.editor.config.debugUndoRedo) { console.log('update', command); } this.newValue = command.newValue; diff --git a/src/editor/lib/commands/index.js b/src/editor/lib/commands/index.js index 1ce9b2ef8..2f1a6afbc 100644 --- a/src/editor/lib/commands/index.js +++ b/src/editor/lib/commands/index.js @@ -1 +1,14 @@ -export { EntityUpdateCommand } from './EntityUpdateCommand.js'; +import { ComponentAddCommand } from './ComponentAddCommand.js'; +import { ComponentRemoveCommand } from './ComponentRemoveCommand.js'; +import { EntityCloneCommand } from './EntityCloneCommand.js'; +import { EntityCreateCommand } from './EntityCreateCommand.js'; +import { EntityRemoveCommand } from './EntityRemoveCommand.js'; +import { EntityUpdateCommand } from './EntityUpdateCommand.js'; + +export const commandsByType = new Map(); +commandsByType.set('componentadd', ComponentAddCommand); +commandsByType.set('componentremove', ComponentRemoveCommand); +commandsByType.set('entityclone', EntityCloneCommand); +commandsByType.set('entitycreate', EntityCreateCommand); +commandsByType.set('entityremove', EntityRemoveCommand); +commandsByType.set('entityupdate', EntityUpdateCommand); diff --git a/src/editor/lib/config.js b/src/editor/lib/config.js new file mode 100644 index 000000000..a50a9efd5 --- /dev/null +++ b/src/editor/lib/config.js @@ -0,0 +1,8 @@ +export function Config() { + return { + // Debug undo/redo + debugUndoRedo: false, + // Default parent to add new elements to + defaultParent: '#street-container' + }; +} diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index 7c3dff355..937570f89 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -1,31 +1,36 @@ /* eslint-disable react/no-danger */ +import { nanoid } from 'nanoid'; import Events from './Events'; -import { EntityUpdateCommand } from './commands'; import { equal } from './utils'; /** * Update a component. * * @param {Element} entity - Entity to modify. - * @param {string} propertyName - component or component.property - * @param {string|number} value - New value. + * @param {string} component - component name + * @param {string} property - property name, use empty string if component is single property or if value is an object + * @param {string|number|object} value - New value. */ -export function updateEntity(entity, propertyName, value) { - var splitName; - - if (propertyName.indexOf('.') !== -1) { - // Multi-prop - splitName = propertyName.split('.'); +export function updateEntity(entity, component, property, value) { + if (property) { + if (value === null || value === undefined) { + // Remove property. + entity.removeAttribute(component, property); + } else { + // Set property. + entity.setAttribute(component, property, value); + } + } else { + if (value === null || value === undefined) { + // Remove component. + entity.removeAttribute(component); + } else { + // Set component. + entity.setAttribute(component, value); + } } - AFRAME.INSPECTOR.execute( - new EntityUpdateCommand(AFRAME.INSPECTOR, { - entity: entity, - component: splitName ? splitName[0] : propertyName, - property: splitName ? splitName[1] : '', - value: value - }) - ); + Events.emit('entityupdate', { entity, component, property, value }); } /** @@ -40,19 +45,16 @@ export function removeEntity(entity, force) { force === true || confirm( 'Do you really want to remove entity `' + - (entity.id || entity.tagName) + + getEntityDisplayName(entity) + '`?' ) ) { - var closest = findClosestEntity(entity); - AFRAME.INSPECTOR.removeObject(entity.object3D); - entity.parentNode.removeChild(entity); - AFRAME.INSPECTOR.selectEntity(closest); + AFRAME.INSPECTOR.execute('entityremove', entity); } } } -function findClosestEntity(entity) { +export function findClosestEntity(entity) { // First we try to find the after the entity var nextEntity = entity.nextElementSibling; while (nextEntity && (!nextEntity.isEntity || nextEntity.isInspector)) { @@ -106,26 +108,49 @@ function insertAfter(newNode, referenceNode) { /** * Clone an entity, inserting it after the cloned one. - * @param {Element} entity Entity to clone + * @param {Element} entity Entity to clone + * @returns {Element} The clone */ export function cloneEntity(entity) { + return AFRAME.INSPECTOR.execute('entityclone', entity); +} + +/** + * Clone an entity, inserting it after the cloned one. This is the implementation of the entityclone command. + * @param {Element} entity Entity to clone + * @param {string|undefined} newId The new id to use for the clone + * @returns {Element} The clone + */ +export function cloneEntityImpl(entity, newId = undefined) { entity.flushToDOM(); const clone = prepareForSerialization(entity); clone.addEventListener( 'loaded', function () { - AFRAME.INSPECTOR.selectEntity(clone); Events.emit('entityclone', clone); + AFRAME.INSPECTOR.selectEntity(clone); }, { once: true } ); - // Get a valid unique ID for the entity - if (entity.id) { - clone.id = getUniqueId(entity.id); + if (newId) { + clone.id = newId; + } else { + if (entity.id) { + if (entity.id.length === 21) { + // nanoid generated id, create a new one + clone.id = createUniqueId(); + } else { + // Get a valid unique ID for the entity + clone.id = getUniqueId(entity.id); + } + } else { + entity.id = createUniqueId(); + } } insertAfter(clone, entity); + return clone; } /** @@ -155,7 +180,7 @@ export function getEntityClipboardRepresentation(entity) { * @param {Element} entity Root of the DOM hierarchy. * @return {Element} Copy of the DOM hierarchy ready for serialization. */ -function prepareForSerialization(entity) { +export function prepareForSerialization(entity) { var clone = entity.cloneNode(false); var children = entity.childNodes; for (var i = 0, l = children.length; i < l; i++) { @@ -473,6 +498,19 @@ function getUniqueId(baseId) { return baseId + '-' + i; } +/** + * Create a unique id that can be used on a DOM element. + * @return {string} Valid Id + */ +export function createUniqueId() { + let id = nanoid(); + do { + id = nanoid(); + // be sure to not return an id starting with a number + } while (/^\d/.test(id)); + return id; +} + export function getComponentClipboardRepresentation(entity, componentName) { /** * Get the list of modified properties @@ -507,7 +545,7 @@ export function getComponentClipboardRepresentation(entity, componentName) { } export function getEntityDisplayName(entity) { - let entityName = entity.id; + let entityName = ''; if (!entity.isScene && !entityName && entity.getAttribute('class')) { entityName = entity.getAttribute('class').split(' ')[0]; } else if (!entity.isScene && !entityName && entity.getAttribute('mixin')) { @@ -563,18 +601,42 @@ export function printEntity(entity, onDoubleClick) { ); } +const NOT_COMPONENTS = ['id', 'class', 'mixin']; + /** * Helper function to add a new entity with a list of components - * @param {object} definition Entity definition to add: - * {element: 'a-entity', components: {geometry: 'primitive:box'}} + * @param {object} definition Entity definition to add, only components is required: + * {element: 'a-entity', id: "hbiuSdYL2", class: "box", components: {geometry: 'primitive:box'}} + * @param {function} cb Callback to call when the entity is created + * @param {Element} parentEl Element to append the entity to * @return {Element} Entity created */ -export function createEntity(definition, cb) { - const entity = document.createElement(definition.element); +export function createEntity(definition, cb, parentEl = undefined) { + const entity = document.createElement(definition.element || 'a-entity'); + if (definition.id) { + entity.id = definition.id; + } else { + entity.id = createUniqueId(); + } - // load default attributes - for (let attr in definition.components) { - entity.setAttribute(attr, definition.components[attr]); + // Set class, mixin + for (const attribute of NOT_COMPONENTS) { + if (attribute !== 'id' && definition[attribute]) { + entity.setAttribute(attribute, definition[attribute]); + } + } + + // Set data attributes + for (const key in definition) { + if (key.startsWith('data-')) { + entity.setAttribute(key, definition[key]); + } + } + + // Set components + for (const componentName in definition.components) { + const componentValue = definition.components[componentName]; + entity.setAttribute(componentName, componentValue); } // Ensure the components are loaded before update the UI @@ -587,7 +649,13 @@ export function createEntity(definition, cb) { { once: true } ); - AFRAME.scenes[0].appendChild(entity); + if (parentEl) { + parentEl.appendChild(entity); + } else { + document + .querySelector(AFRAME.INSPECTOR.config.defaultParent) + .appendChild(entity); + } return entity; } diff --git a/src/editor/lib/history.js b/src/editor/lib/history.js index 2fe679557..6b40f2857 100644 --- a/src/editor/lib/history.js +++ b/src/editor/lib/history.js @@ -23,7 +23,7 @@ export class History { lastCmd && lastCmd.updatable && cmd.updatable && - lastCmd.entity === cmd.entity && + lastCmd.entityId === cmd.entityId && lastCmd.type === cmd.type && lastCmd.component === cmd.component && lastCmd.property === cmd.property; diff --git a/src/editor/lib/shortcuts.js b/src/editor/lib/shortcuts.js index 69c7b1d4c..6f89b6a4f 100644 --- a/src/editor/lib/shortcuts.js +++ b/src/editor/lib/shortcuts.js @@ -65,11 +65,6 @@ export const Shortcuts = { Events.emit('togglegrid'); } - // m: motion capture - if (keyCode === 77) { - Events.emit('togglemotioncapture'); - } - // backspace & supr: remove selected entity if (keyCode === 8 || keyCode === 46) { removeSelectedEntity(); @@ -120,6 +115,7 @@ export const Shortcuts = { if (!shouldCaptureKeyEvent(event) || !AFRAME.INSPECTOR.opened) { return; } + if ( (event.ctrlKey && os !== 'macos') || (event.metaKey && os === 'macos') diff --git a/src/editor/lib/utils.js b/src/editor/lib/utils.js index bf193670a..074c13a47 100644 --- a/src/editor/lib/utils.js +++ b/src/editor/lib/utils.js @@ -105,6 +105,9 @@ export function saveBlob(blob, filename) { link.remove(); } +// Compares 2 vector objects up to size 4 +// Expect v1 and v2 to take format {x: number, y: number, z: number, w:number} +// Smaller vectors (ie. vec2) should work as well since their z & w vals will be the same (undefined) export function areVectorsEqual(v1, v2) { return ( Object.is(v1.x, v2.x) && diff --git a/src/editor/lib/viewport.js b/src/editor/lib/viewport.js index a4be475cb..b8a05101e 100644 --- a/src/editor/lib/viewport.js +++ b/src/editor/lib/viewport.js @@ -5,7 +5,6 @@ import EditorControls from './EditorControls.js'; import { initRaycaster } from './raycaster'; import Events from './Events'; import { sendMetric } from '../services/ga.js'; -import { EntityUpdateCommand } from './commands/EntityUpdateCommand.js'; // variables used by OrientedBoxHelper const auxEuler = new THREE.Euler(); @@ -178,14 +177,11 @@ export function Viewport(inspector) { value = `${object.scale.x} ${object.scale.y} ${object.scale.z}`; } - inspector.execute( - new EntityUpdateCommand(inspector, { - component: component, - entity: transformControls.object.el, - property: '', - value: value - }) - ); + inspector.execute('entityupdate', { + component: component, + entity: transformControls.object.el, + value: value + }); }); transformControls.addEventListener('mouseDown', () => { diff --git a/src/json-utils_1.1.js b/src/json-utils_1.1.js index 3fe4011a4..90c7b240d 100644 --- a/src/json-utils_1.1.js +++ b/src/json-utils_1.1.js @@ -443,9 +443,6 @@ function createEntityFromObj(entityData, parentEl) { if (entityData.mixin) { entity.setAttribute('mixin', entityData.mixin); } - // Ensure the components are loaded before update the UI - - entity.emit('entitycreated', {}, false); }); if (entityData.children) {