From 298fddc10b0cb172299fe6e15b90d58b5bb63d7a Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Tue, 28 May 2024 19:05:54 +0200 Subject: [PATCH 1/7] new functions to export to json --- src/editor/lib/entity.js | 195 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 4 deletions(-) diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index 937570f8..1ad74f59 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -1,7 +1,7 @@ /* eslint-disable react/no-danger */ import { nanoid } from 'nanoid'; import Events from './Events'; -import { equal } from './utils'; +import { equal, saveBlob } from './utils'; /** * Update a component. @@ -178,9 +178,13 @@ export function getEntityClipboardRepresentation(entity) { * primitive attributes, mixins and defaults. * * @param {Element} entity Root of the DOM hierarchy. + * @param {Function} filterFunc Function to filter out nodes from the serialization * @return {Element} Copy of the DOM hierarchy ready for serialization. */ -export function prepareForSerialization(entity) { +// add default function to filterFunc +export function prepareForSerialization(entity, filterFunc = () => true) { + if (!filterFunc(entity)) return null; + var clone = entity.cloneNode(false); var children = entity.childNodes; for (var i = 0, l = children.length; i < l; i++) { @@ -191,7 +195,7 @@ export function prepareForSerialization(entity) { !child.hasAttribute('data-aframe-inspector') && !child.hasAttribute('data-aframe-canvas')) ) { - clone.appendChild(prepareForSerialization(children[i])); + clone.appendChild(prepareForSerialization(children[i], filterFunc)); } } optimizeComponents(clone, entity); @@ -209,12 +213,29 @@ function optimizeComponents(copy, source) { var removeAttribute = HTMLElement.prototype.removeAttribute; var setAttribute = HTMLElement.prototype.setAttribute; var components = source.components || {}; + for (const blacklistedAttribute of blacklistedComponentProperties.attributes) { + if (source.hasAttribute(blacklistedAttribute)) { + copy.removeAttribute(blacklistedAttribute); + } + } Object.keys(components).forEach(function (name) { + if (blacklistedComponentProperties.components.includes(name)) { + copy.removeAttribute(name); + return; + } + for (const suffix of blacklistedComponentProperties.componentSuffixes) { + if (name.endsWith(suffix)) { + copy.removeAttribute(name); + return; + } + } var component = components[name]; var result = getImplicitValue(component, source); var isInherited = result[1]; var implicitValue = result[0]; - var currentValue = source.getAttribute(name); + // Use getDOMAttribute instead of getAttribute so we we don't get some properties that are modified + // on material-values this.data based on gltf material values just to show the correct values in the inspector. + var currentValue = source.getDOMAttribute(name); var optimalUpdate = getOptimalUpdate( component, implicitValue, @@ -659,3 +680,169 @@ export function createEntity(definition, cb, parentEl = undefined) { return entity; } + +/** + * + * @param {Entity} element + * @returns {EntityObject} + */ +export function elementToObject(element) { + const obj = {}; + + if (element.tagName !== 'A-ENTITY') { + obj.element = element.tagName.toLowerCase(); + } + + if (element.attributes.length > 0) { + const components = {}; + + for (const attribute of element.attributes) { + if ( + attribute.value === '' && + (attribute.name === 'position' || + attribute.name === 'rotation' || + attribute.name === 'scale') + ) { + continue; + } + + if ( + NOT_COMPONENTS.includes(attribute.name) || + attribute.name.startsWith('data-') + ) { + obj[attribute.name] = attribute.value; + continue; + } + + /* if int has more then 6 decimal round it for position rotation and scale */ + if ( + attribute.name === 'position' || + attribute.name === 'rotation' || + attribute.name === 'scale' + ) { + const values = attribute.value.split(' ').map(parseFloat); + const roundedValues = values.map((v) => Math.round(v * 1000) / 1000); + components[attribute.name] = roundedValues.join(' '); + continue; + } + + components[attribute.name] = attribute.value; + } + + obj.components = components; + } + + if (element.childNodes.length > 0) { + const children = []; + + for (const child of element.childNodes) { + if (child.nodeType === Node.ELEMENT_NODE) { + children.push(elementToObject(child)); + } + } + + if (children.length > 0) { + obj.children = children; + } + } + + return obj; +} + +const blacklistedEntityProperties = { + id: [], + classList: ['autocreated'], + tagName: [], + attributes: [] +}; + +const blacklistedComponentProperties = { + attributes: ['draggable', 'data-ignore-raycaster'], + components: [], + componentSuffixes: ['autocreated'] +}; + +export function isBlacklisted(entity) { + if (entity.id) { + if (blacklistedEntityProperties.id.includes(entity.id)) return true; + } + + if (entity.tagName) { + if ( + blacklistedEntityProperties.tagName.includes(entity.tagName.toLowerCase()) + ) { + return true; + } + } + + if (entity.classList) { + for (const className of entity.classList) { + if (blacklistedEntityProperties.classList.includes(className)) { + return true; + } + } + } + + if (entity.attributes) { + for (const attribute of entity.attributes) { + if (blacklistedEntityProperties.attributes.includes(attribute.name)) { + return true; + } + } + } + + return false; +} +/** + * + * @param {Entity} rootEntity + * @param {EntityObject} existingJSON + * @returns {EntityObject} + */ +export function exportSceneToObject(rootEntity, existingJSON = undefined) { + const newJSON = + existingJSON !== undefined ? structuredClone(existingJSON) : {}; + if (!newJSON.children) { + newJSON.children = []; + } + + const idsSaved = []; + rootEntity.childNodes.forEach((entity) => { + if (entity.nodeType !== Node.ELEMENT_NODE) return; + // set an id on the entity if we had none + if (!entity.id) entity.id = createUniqueId(); + idsSaved.push(entity.id); + // prepare entity for serialization and check if it's blacklisted + const preparedElement = prepareForSerialization( + entity, + (e) => !isBlacklisted(e) + ); + if (!preparedElement) return; + // convert entity to object + const entityObj = elementToObject(preparedElement); + // if an entity of newJSON already has the same id, replace it by the new one, otherwise add the new entity + const index = newJSON.children.findIndex((e) => e.id === entityObj.id); + if (index !== -1) { + newJSON.children.splice(index, 1, entityObj); + } else { + newJSON.children.push(entityObj); + } + }); + + // remove entities that doesn't exist in the current scene + newJSON.children = newJSON.children.filter( + (entity) => idsSaved.indexOf(entity.id) > -1 + ); + + return newJSON; +} + +export function exportSceneToJSON(rootEntity, existingJSON = undefined) { + const obj = exportSceneToObject(rootEntity, existingJSON); + const sceneJSON = JSON.stringify(obj, null, 2); + return sceneJSON; +} + +export function downloadJSON(jsonString, filename) { + saveBlob(new Blob([jsonString], { type: 'application/json' }), filename); +} From 7fc32a5bf1c4781e711a67339e07f5ecd434dfe3 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 29 May 2024 18:33:49 +0200 Subject: [PATCH 2/7] new convertToObject function that uses the new functions, keep the old implementation around for testing and comparison --- src/editor/components/scenegraph/Toolbar.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/editor/components/scenegraph/Toolbar.js b/src/editor/components/scenegraph/Toolbar.js index b05a15ac..7211de6e 100644 --- a/src/editor/components/scenegraph/Toolbar.js +++ b/src/editor/components/scenegraph/Toolbar.js @@ -20,7 +20,7 @@ import { uploadThumbnailImage } from '../modals/ScreenshotModal/ScreenshotModal. import { sendMetric } from '../../services/ga.js'; import posthog from 'posthog-js'; import { UndoRedo } from '../components/UndoRedo'; -// const LOCALSTORAGE_MOCAP_UI = "aframeinspectormocapuienabled"; +import { downloadJSON, exportSceneToJSON } from '../../lib/entity.js'; function filterHelpers(scene, visible) { scene.traverse((o) => { @@ -128,7 +128,7 @@ export default class Toolbar extends Component { } }; - static convertToObject = () => { + static convertToObjectOld = () => { try { posthog.capture('convert_to_json_clicked', { scene_id: STREET.utils.getCurrentSceneId() @@ -156,6 +156,23 @@ export default class Toolbar extends Component { } }; + static convertToObject = () => { + try { + const rootEntity = document.getElementById('street-container'); + const exportedScene = exportSceneToJSON(rootEntity, { + title: STREET.utils.getCurrentSceneTitle() + }); + // download the file + downloadJSON(exportedScene, 'data.json'); + STREET.notify.successMessage('3DStreet JSON file saved successfully.'); + } catch (error) { + STREET.notify.errorMessage( + `Error trying to save 3DStreet JSON file. Error: ${error}` + ); + console.error(error); + } + }; + cloudSaveAsHandler = async () => { this.cloudSaveHandler({ doSaveAs: true }); }; From 079d94d3b1c1e017683408c618db9b9e6df337d8 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Fri, 31 May 2024 13:48:08 +0200 Subject: [PATCH 3/7] add environment to blacklisted entity ids --- src/editor/lib/entity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index 1ad74f59..0b5459a0 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -750,7 +750,7 @@ export function elementToObject(element) { } const blacklistedEntityProperties = { - id: [], + id: ['environment'], classList: ['autocreated'], tagName: [], attributes: [] From 7e7a57631846a9f00e44d760f6e46807899b7ec4 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Sat, 31 Aug 2024 14:53:33 +0200 Subject: [PATCH 4/7] round rotation to 1 degree --- src/editor/lib/entity.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index 0b5459a0..17832a89 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -721,7 +721,11 @@ export function elementToObject(element) { attribute.name === 'scale' ) { const values = attribute.value.split(' ').map(parseFloat); - const roundedValues = values.map((v) => Math.round(v * 1000) / 1000); + /// Round rotation values to 1 degree, position and scale to 0.001 + const precision = attribute.name === 'rotation' ? 1 : 1000; + const roundedValues = values.map( + (v) => Math.round(v * precision) / precision + ); components[attribute.name] = roundedValues.join(' '); continue; } From d32b0da1d1cf728d4a0b4cda300e53f94d23eb5a Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 2 Sep 2024 14:54:18 +0200 Subject: [PATCH 5/7] don't export visible component with empty string --- src/editor/lib/entity.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index 17832a89..d74b4383 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -699,7 +699,8 @@ export function elementToObject(element) { for (const attribute of element.attributes) { if ( attribute.value === '' && - (attribute.name === 'position' || + (attribute.name === 'visible' || + attribute.name === 'position' || attribute.name === 'rotation' || attribute.name === 'scale') ) { From 6161bcf8450dcf3aeec191b0034162a7c757fbe9 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 2 Sep 2024 14:54:58 +0200 Subject: [PATCH 6/7] Revert "round rotation to 1 degree" This reverts commit 7e7a57631846a9f00e44d760f6e46807899b7ec4. --- src/editor/lib/entity.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index d74b4383..89019197 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -722,11 +722,7 @@ export function elementToObject(element) { attribute.name === 'scale' ) { const values = attribute.value.split(' ').map(parseFloat); - /// Round rotation values to 1 degree, position and scale to 0.001 - const precision = attribute.name === 'rotation' ? 1 : 1000; - const roundedValues = values.map( - (v) => Math.round(v * precision) / precision - ); + const roundedValues = values.map((v) => Math.round(v * 1000) / 1000); components[attribute.name] = roundedValues.join(' '); continue; } From e7d3cf866fbb89ae931fdf1c2713468ab517ac36 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Sat, 7 Sep 2024 14:20:25 +0200 Subject: [PATCH 7/7] move code that remove empty position/rotation/scale/visible to optimizeComponents so that applies to both json and html export --- src/editor/lib/entity.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/editor/lib/entity.js b/src/editor/lib/entity.js index 89019197..ff874e09 100644 --- a/src/editor/lib/entity.js +++ b/src/editor/lib/entity.js @@ -249,6 +249,17 @@ function optimizeComponents(copy, source) { var value = stringifyComponentValue(schema, optimalUpdate); setAttribute.call(copy, name, value); } + + // Remove special components if they use the default value + if ( + value === '' && + (name === 'visible' || + name === 'position' || + name === 'rotation' || + name === 'scale') + ) { + removeAttribute.call(copy, name); + } }); } @@ -697,16 +708,6 @@ export function elementToObject(element) { const components = {}; for (const attribute of element.attributes) { - if ( - attribute.value === '' && - (attribute.name === 'visible' || - attribute.name === 'position' || - attribute.name === 'rotation' || - attribute.name === 'scale') - ) { - continue; - } - if ( NOT_COMPONENTS.includes(attribute.name) || attribute.name.startsWith('data-')