From c523feb130efa465583cce6c11363262984a2dce Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:34:25 +0200 Subject: [PATCH 1/8] Allow dash in component instance id like bind__troika-text Backport from aframevr/aframe-inspector#822 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/components/elements/AddGeneratorComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/components/elements/AddGeneratorComponent.js b/src/editor/components/elements/AddGeneratorComponent.js index f321600a5..f7d1a1ddf 100644 --- a/src/editor/components/elements/AddGeneratorComponent.js +++ b/src/editor/components/elements/AddGeneratorComponent.js @@ -31,7 +31,7 @@ export default class AddGeneratorComponent extends React.Component { id = id .trim() .toLowerCase() - .replace(/[^a-z0-9]/g, ''); + .replace(/[^a-z0-9-]/g, ''); // With the transform, id could be empty string, so we need to check again. } if (id) { From c738df541d47db62f0c9e0db984a42bc5dbff086 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:38:55 +0200 Subject: [PATCH 2/8] Deduplicate asset registration, create a-assets if missing Move helper functions (getFilename, isValidId, getValidId, getUrlFromId, getIdFromUrl) to assetsUtils.js. Detect already-loaded textures and show error messages for duplicate/invalid asset names. Backport from aframevr/aframe-inspector#824 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/components/modals/ModalTextures.js | 110 +++++++++--------- .../components/widgets/TextureWidget.js | 63 +--------- src/editor/lib/assetsUtils.js | 64 ++++++++-- src/editor/style/textureModal.scss | 7 ++ 4 files changed, 118 insertions(+), 126 deletions(-) diff --git a/src/editor/components/modals/ModalTextures.js b/src/editor/components/modals/ModalTextures.js index 7d978a1a3..954b7b79b 100644 --- a/src/editor/components/modals/ModalTextures.js +++ b/src/editor/components/modals/ModalTextures.js @@ -2,32 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import Events from '../../lib/Events'; import Modal from '@shared/components/Modal/Modal.jsx'; -import { insertNewAsset } from '../../lib/assetsUtils'; - -function getFilename(url, converted = false) { - var filename = url.split('/').pop(); - if (converted) { - filename = getValidId(filename); - } - return filename; -} - -function isValidId(id) { - // The correct re should include : and . but A-frame seems to fail while accessing them - var re = /^[A-Za-z]+[\w-]*$/; - return re.test(id); -} - -function getValidId(name) { - // info.name.replace(/\.[^/.]+$/, '').replace(/\s+/g, '') - return name - .split('.') - .shift() - .replace(/\s/, '-') - .replace(/^\d+\s*/, '') - .replace(/[\W]/, '') - .toLowerCase(); -} +import { + getFilename, + getIdFromUrl, + insertNewAsset, + isValidId +} from '../../lib/assetsUtils'; export default class ModalTextures extends React.Component { static propTypes = { @@ -157,15 +137,20 @@ export default class ModalTextures extends React.Component { var self = this; function onImageLoaded(img) { var src = self.preview.current.src; + var name = getFilename(src, true); + var existingAssetId = getIdFromUrl(src); + if (existingAssetId) { + name = existingAssetId; + } self.setState({ preview: { width: self.preview.current.naturalWidth, height: self.preview.current.naturalHeight, src: src, id: '', - name: getFilename(src, true), + name: name, filename: getFilename(src), - type: 'new', + type: existingAssetId ? 'asset' : 'new', loaded: true, value: 'url(' + src + ')' } @@ -178,12 +163,6 @@ export default class ModalTextures extends React.Component { this.imageName.current.focus(); }; - onNameKeyUp = (event) => { - if (event.keyCode === 13 && this.isValidAsset()) { - this.addNewAsset(); - } - }; - onNameChanged = (event) => { var state = this.state.preview; state.name = event.target.value; @@ -192,6 +171,7 @@ export default class ModalTextures extends React.Component { toggleNewDialog = () => { this.setState({ addNewDialogOpened: !this.state.addNewDialogOpened }); + this.clear(); }; clear() { @@ -215,23 +195,19 @@ export default class ModalTextures extends React.Component { this.setState({ newUrl: e.target.value }); }; - isValidAsset() { - let validUrl = isValidId(this.state.preview.name); - let validAsset = this.state.preview.loaded && validUrl; - return validAsset; - } - addNewAsset = () => { - var self = this; + if (this.state.preview.type === 'asset') { + return; + } + insertNewAsset( 'img', this.state.preview.name, this.state.preview.src, - true, - function () { - self.generateFromAssets(); - self.setState({ addNewDialogOpened: false }); - self.clear(); + () => { + this.generateFromAssets(); + this.setState({ addNewDialogOpened: false }); + this.clear(); } ); }; @@ -243,15 +219,20 @@ export default class ModalTextures extends React.Component { renderRegistryImages() { var self = this; let selectSample = function (image) { + let name = getFilename(image.name, true); + const existingAssetId = getIdFromUrl(image.src); + if (existingAssetId) { + name = existingAssetId; + } self.setState({ preview: { width: image.width, height: image.height, src: image.src, id: '', - name: getFilename(image.name, true), + name: name, filename: getFilename(image.src), - type: 'registry', + type: existingAssetId ? 'asset' : 'registry', loaded: true, value: 'url(' + image.src + ')' } @@ -291,8 +272,14 @@ export default class ModalTextures extends React.Component { let loadedTextures = this.state.loadedTextures; let preview = this.state.preview; - let validUrl = isValidId(this.state.preview.name); - let validAsset = this.isValidAsset(); + let validId = isValidId(this.state.preview.name); + let assetIdTaken = + validId && !!document.getElementById(this.state.preview.name); + let validAsset = + this.state.preview.loaded && + validId && + !assetIdTaken && + this.state.preview.type !== 'asset'; let addNewAssetButton = this.state.addNewDialogOpened ? 'BACK' @@ -343,13 +330,32 @@ export default class ModalTextures extends React.Component { 0 && !validUrl ? 'error' : '' + this.state.preview.name.length > 0 && + (!validId || assetIdTaken) + ? 'error' + : '' } + readOnly={preview.type === 'asset'} type="text" value={this.state.preview.name} onChange={this.onNameChanged} - onKeyUp={this.onNameKeyUp} + onKeyUp={(event) => { + if (event.keyCode === 13 && validAsset) { + this.addNewAsset(); + } + }} /> + {preview.type !== 'asset' && assetIdTaken && ( +
+ Name already taken by another asset or entity +
+ )} + {this.state.preview.name.length > 0 && !validId && ( +
Name is not valid
+ )} + {preview.type === 'asset' && ( +
Texture already loaded
+ )} 1 && - document.querySelector(assetId) && - document.querySelector(assetId).getAttribute('src') - ); -} - -function GetFilename(url) { - if (url) { - var m = url.toString().match(/.*\/(.+?)\./); - if (m && m.length > 1) { - return m[1]; - } - } - return ''; -} - -function insertNewAsset(type, id, src) { - var element = null; - switch (type) { - case 'img': - element = document.createElement('img'); - element.id = id; - element.src = src; - break; - } - if (element) { - document.getElementsByTagName('a-assets')[0].appendChild(element); - } -} - -function insertOrGetImageAsset(src) { - var id = GetFilename(src); - // Search for already loaded asset by src - var element = document.querySelector("a-assets > img[src='" + src + "']"); - - if (element) { - id = element.id; - } else { - // Check if first char of the ID is a number (Non a valid ID) - // In that case a 'i' preffix will be added - if (!isNaN(parseInt(id[0], 10))) { - id = 'i' + id; - } - if (document.getElementById(id)) { - var i = 1; - while (document.getElementById(id + '_' + i)) { - i++; - } - id += '_' + i; - } - insertNewAsset('img', id, src); - } - - return id; -} +import { getUrlFromId } from '../../lib/assetsUtils'; export default class TextureWidget extends React.Component { static propTypes = { @@ -201,10 +144,6 @@ export default class TextureWidget extends React.Component { return; } var value = image.value; - if (image.type !== 'asset') { - var assetId = insertOrGetImageAsset(image.src); - value = '#' + assetId; - } if (this.props.onChange) { this.props.onChange(this.props.name, value); diff --git a/src/editor/lib/assetsUtils.js b/src/editor/lib/assetsUtils.js index db8ce5dd2..fd97da0b3 100644 --- a/src/editor/lib/assetsUtils.js +++ b/src/editor/lib/assetsUtils.js @@ -1,19 +1,48 @@ -export function insertNewAsset( - type, - id, - src, - anonymousCrossOrigin, - onLoadedCallback -) { - var element = null; +export function getUrlFromId(assetId) { + return ( + assetId.length > 1 && + document.querySelector(assetId) && + document.querySelector(assetId).getAttribute('src') + ); +} + +export function getIdFromUrl(url) { + return document.querySelector("a-assets > [src='" + url + "']")?.id; +} + +export function getFilename(url, converted = false) { + var filename = url.split('/').pop(); + if (converted) { + filename = getValidId(filename); + } + return filename; +} + +export function isValidId(id) { + // The correct re should include : and . but A-frame seems to fail while accessing them + var re = /^[A-Za-z]+[\w-]*$/; + return re.test(id); +} + +export function getValidId(name) { + // info.name.replace(/\.[^/.]+$/, '').replace(/\s+/g, '') + return name + .split('.') + .shift() + .replace(/\s/, '-') + .replace(/^\d+\s*/, '') + .replace(/[\W]/, '') + .toLowerCase(); +} + +export function insertNewAsset(type, id, src, onLoadedCallback = undefined) { + let element; switch (type) { case 'img': element = document.createElement('img'); element.id = id; element.src = src; - if (anonymousCrossOrigin) { - element.crossOrigin = 'anonymous'; - } + element.crossOrigin = 'anonymous'; break; } @@ -23,6 +52,17 @@ export function insertNewAsset( onLoadedCallback(); } }; - document.getElementsByTagName('a-assets')[0].appendChild(element); + + let assetsEl = document.querySelector('a-assets'); + if (!assetsEl) { + assetsEl = document.createElement('a-assets'); + var sceneEl = document.querySelector('a-scene'); + if (!sceneEl) { + throw new Error('No a-scene element found to append a-assets to'); + } + sceneEl.appendChild(assetsEl); + } + + assetsEl.appendChild(element); } } diff --git a/src/editor/style/textureModal.scss b/src/editor/style/textureModal.scss index 05b204289..ec77d25fc 100644 --- a/src/editor/style/textureModal.scss +++ b/src/editor/style/textureModal.scss @@ -179,6 +179,13 @@ margin: 8px 0; width: 144px; } +.preview .iderror { + background: #fff; + color: #a00; + margin-bottom: 8px; + padding: 3px 5px; + width: 148px; +} .preview button { width: 155px; } From 8bd78c75e7bc2d723429a5e2ac7904321a764991 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:39:27 +0200 Subject: [PATCH 3/8] Fix CSS rules that removed color widget inner padding The pseudo-element selectors were missing :: prefix, making the rules ineffective. Backport from aframevr/aframe-inspector#826 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/style/index.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor/style/index.scss b/src/editor/style/index.scss index c63ade597..f9e98f18e 100644 --- a/src/editor/style/index.scss +++ b/src/editor/style/index.scss @@ -394,19 +394,19 @@ body.aframe-inspector-opened { } /* Note these vendor-prefixed selectors cannot be grouped! */ - input[type='color']-webkit-color-swatch { + input[type='color']::-webkit-color-swatch { border: 0; /* To remove the gray border. */ } - input[type='color']-webkit-color-swatch-wrapper { + input[type='color']::-webkit-color-swatch-wrapper { padding: 0; /* To remove the inner padding. */ } - input[type='color']-moz-color-swatch { + input[type='color']::-moz-color-swatch { border: 0; } - input[type='color']-moz-focus-inner { + input[type='color']::-moz-focus-inner { border: 0; /* To remove the inner border (specific to Firefox). */ padding: 0; } From 0e8c96d1077fc2976c774a32ee130c956500e43d Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:40:00 +0200 Subject: [PATCH 4/8] Remove useless THREE.ImageUtils.crossOrigin line THREE.ImageUtils was removed in newer Three.js versions, causing a runtime warning. Backport from aframevr/aframe-inspector#832 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/components/Main.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/editor/components/Main.js b/src/editor/components/Main.js index effe5563d..273b8a206 100644 --- a/src/editor/components/Main.js +++ b/src/editor/components/Main.js @@ -24,8 +24,6 @@ import useStore from '@/store'; import { AIChatProvider } from '../contexts/AIChatContext'; import AIChatPanel from './scenegraph/AIChatPanel'; -THREE.ImageUtils.crossOrigin = ''; - // Define the libraries array as a constant outside of the component const GOOGLE_MAPS_LIBRARIES = ['places']; From 7a31a7691444703e8119a5b6a06c8fbe4436dd3c Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:43:09 +0200 Subject: [PATCH 5/8] Remove unused widgets propTypes, fix clicking on boolean label, add id to widgets Replace componentname/entity propTypes with id prop on all widgets. PropertyRow now generates and passes the id. Fix boolean label click by using the id prop directly instead of constructing it in BooleanWidget. Simplify PropertyRow type/value override logic. Backport from aframevr/aframe-inspector#819 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/components/elements/PropertyRow.js | 34 +++++++++---------- .../components/widgets/BooleanWidget.js | 9 ++--- src/editor/components/widgets/ColorWidget.js | 4 +-- src/editor/components/widgets/InputWidget.js | 4 +-- src/editor/components/widgets/NumberWidget.js | 6 ++-- src/editor/components/widgets/SelectWidget.js | 2 ++ .../components/widgets/TextureWidget.js | 9 ++--- src/editor/components/widgets/Vec2Widget.js | 12 ++----- src/editor/components/widgets/Vec3Widget.js | 14 ++------ src/editor/components/widgets/Vec4Widget.js | 16 +++------ 10 files changed, 40 insertions(+), 70 deletions(-) diff --git a/src/editor/components/elements/PropertyRow.js b/src/editor/components/elements/PropertyRow.js index 82e7902c1..8a16770c4 100644 --- a/src/editor/components/elements/PropertyRow.js +++ b/src/editor/components/elements/PropertyRow.js @@ -44,10 +44,13 @@ export default class PropertyRow extends React.Component { getWidget() { const props = this.props; - const isMap = - props.componentname === 'material' && - (props.name === 'envMap' || props.name === 'src'); let type = props.schema.type; + + if (props.componentname === 'material' && props.name === 'envMap') { + // material envMap has the wrong type string, force it to map + type = 'map'; + } + if ( (props.componentname === 'animation' || props.componentname.startsWith('animation__')) && @@ -60,15 +63,17 @@ export default class PropertyRow extends React.Component { type = 'boolean'; } - const value = - props.schema.type === 'selector' + let value = + type === 'selector' ? props.entity.getDOMAttribute(props.componentname)?.[props.name] : props.data; + if (type === 'string' && value && typeof value !== 'string') { + // Allow editing a custom type like event-set component schema + value = props.schema.stringify(value); + } + const widgetProps = { - componentname: props.componentname, - entity: props.entity, - isSingle: props.isSingle, name: props.name, onChange: function (name, value) { // Auto-switch to custom variant for building segments when modifying certain properties @@ -106,7 +111,8 @@ export default class PropertyRow extends React.Component { onEntityUpdate: props.onEntityUpdate }); }, - value: value + value: value, + id: this.id }; const numberWidgetProps = { min: props.schema.hasOwnProperty('min') ? props.schema.min : -Infinity, @@ -122,7 +128,7 @@ export default class PropertyRow extends React.Component { /> ); } - if (type === 'map' || isMap) { + if (type === 'map') { return ; } @@ -151,14 +157,6 @@ export default class PropertyRow extends React.Component { return ; } default: { - if ( - props.schema.type === 'string' && - widgetProps.value && - typeof widgetProps.value !== 'string' - ) { - // Allow editing a custom type like event-set component schema - widgetProps.value = props.schema.stringify(widgetProps.value); - } return ; } } diff --git a/src/editor/components/widgets/BooleanWidget.js b/src/editor/components/widgets/BooleanWidget.js index 7f4c9cbd5..32db2b8fa 100644 --- a/src/editor/components/widgets/BooleanWidget.js +++ b/src/editor/components/widgets/BooleanWidget.js @@ -4,8 +4,7 @@ import classNames from 'classnames'; export default class BooleanWidget extends React.Component { static propTypes = { - componentname: PropTypes.string.isRequired, - entity: PropTypes.object, + id: PropTypes.string, name: PropTypes.string.isRequired, onChange: PropTypes.func, value: PropTypes.bool @@ -36,8 +35,6 @@ export default class BooleanWidget extends React.Component { }; render() { - const id = this.props.componentname + '.' + this.props.name; - const checkboxClasses = classNames({ checkboxAnim: true, checked: this.state.value @@ -46,13 +43,13 @@ export default class BooleanWidget extends React.Component { return (
null} /> -
); } diff --git a/src/editor/components/widgets/ColorWidget.js b/src/editor/components/widgets/ColorWidget.js index 994e7055d..58d0dbf83 100644 --- a/src/editor/components/widgets/ColorWidget.js +++ b/src/editor/components/widgets/ColorWidget.js @@ -3,8 +3,7 @@ import PropTypes from 'prop-types'; export default class ColorWidget extends React.Component { static propTypes = { - componentname: PropTypes.string.isRequired, - entity: PropTypes.object, + id: PropTypes.string, name: PropTypes.string.isRequired, onChange: PropTypes.func, value: PropTypes.string @@ -77,6 +76,7 @@ export default class ColorWidget extends React.Component { onChange={this.onChange} /> {helpString} - - + + ); } diff --git a/src/editor/components/widgets/Vec3Widget.js b/src/editor/components/widgets/Vec3Widget.js index 4171be891..54fa8927b 100644 --- a/src/editor/components/widgets/Vec3Widget.js +++ b/src/editor/components/widgets/Vec3Widget.js @@ -4,8 +4,6 @@ import React from 'react'; import { areVectorsEqual } from '../../lib/utils.js'; export default class Vec3Widget extends React.Component { static propTypes = { - componentname: PropTypes.string, - entity: PropTypes.object, onChange: PropTypes.func, value: PropTypes.object.isRequired }; @@ -39,17 +37,11 @@ export default class Vec3Widget extends React.Component { } render() { - const widgetProps = { - componentname: this.props.componentname, - entity: this.props.entity, - onChange: this.onChange - }; - return (
- - - + + +
); } diff --git a/src/editor/components/widgets/Vec4Widget.js b/src/editor/components/widgets/Vec4Widget.js index 485d4b187..6292e733c 100644 --- a/src/editor/components/widgets/Vec4Widget.js +++ b/src/editor/components/widgets/Vec4Widget.js @@ -6,8 +6,6 @@ import NumberWidget from './NumberWidget'; export default class Vec4Widget extends React.Component { static propTypes = { - componentname: PropTypes.string, - entity: PropTypes.object, onChange: PropTypes.func, value: PropTypes.object.isRequired }; @@ -43,18 +41,12 @@ export default class Vec4Widget extends React.Component { } render() { - const widgetProps = { - componentname: this.props.componentname, - entity: this.props.entity, - onChange: this.onChange - }; - return (
- - - - + + + +
); } From e83e7aa812d5152f21493930adfe5d85529d640a Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:43:55 +0200 Subject: [PATCH 6/8] Disable spellcheck on some input fields Backport from aframevr/aframe-inspector#825 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/components/modals/ModalTextures.js | 2 ++ src/editor/components/widgets/InputWidget.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/editor/components/modals/ModalTextures.js b/src/editor/components/modals/ModalTextures.js index 954b7b79b..773757fe1 100644 --- a/src/editor/components/modals/ModalTextures.js +++ b/src/editor/components/modals/ModalTextures.js @@ -307,6 +307,7 @@ export default class ModalTextures extends React.Component { value={this.state.newUrl} onChange={this.onUrlChange} onKeyUp={this.onNewUrl} + spellCheck="false" />
  • @@ -344,6 +345,7 @@ export default class ModalTextures extends React.Component { this.addNewAsset(); } }} + spellCheck="false" /> {preview.type !== 'asset' && assetIdTaken && (
    diff --git a/src/editor/components/widgets/InputWidget.js b/src/editor/components/widgets/InputWidget.js index f3cbb395b..6cec96a2b 100644 --- a/src/editor/components/widgets/InputWidget.js +++ b/src/editor/components/widgets/InputWidget.js @@ -41,6 +41,7 @@ export default class InputWidget extends React.Component { className="string" value={this.state.value || ''} onChange={this.onChange} + spellCheck="false" /> ); } From 4be9184259d4bd343e7a4cc76df786cf20dafe91 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:45:00 +0200 Subject: [PATCH 7/8] Fix entity reparent to preserve world scale and clean default attributes Preserve world scale when reparenting by computing local scale relative to the new parent. Remove rotation/scale attributes when they equal default values (0 0 0 / 1 1 1) instead of always setting them. Backport from c-frame/aframe-editor#62 and aframevr/aframe-editor#63 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/commands/EntityReparentCommand.js | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/editor/lib/commands/EntityReparentCommand.js b/src/editor/lib/commands/EntityReparentCommand.js index 00fc2316e..0f8b0bb42 100644 --- a/src/editor/lib/commands/EntityReparentCommand.js +++ b/src/editor/lib/commands/EntityReparentCommand.js @@ -32,11 +32,13 @@ export class EntityReparentCommand extends Command { // losslessly, including correct component dependency resolution. this.entityData = STREET.utils.getElementData(entity); - // Store world position and quaternion before reparenting + // Store world position, quaternion, and scale before reparenting this.worldPosition = new THREE.Vector3(); this.worldQuaternion = new THREE.Quaternion(); + this.worldScale = new THREE.Vector3(); entity.object3D.getWorldPosition(this.worldPosition); entity.object3D.getWorldQuaternion(this.worldQuaternion); + entity.object3D.getWorldScale(this.worldScale); } } @@ -55,9 +57,19 @@ export class EntityReparentCommand extends Command { .invert() .multiply(this.worldQuaternion); + // Calculate the new local scale + const parentWorldScale = new THREE.Vector3(); + newParent.object3D.getWorldScale(parentWorldScale); + const newLocalScale = new THREE.Vector3( + this.worldScale.x / parentWorldScale.x, + this.worldScale.y / parentWorldScale.y, + this.worldScale.z / parentWorldScale.z + ); + // Apply the new local transform to the entity entity.object3D.position.copy(newLocalPosition); entity.object3D.quaternion.copy(newLocalQuaternion); + entity.object3D.scale.copy(newLocalScale); // Update A-Frame attributes to reflect the changes entity.setAttribute('position', { @@ -70,11 +82,28 @@ export class EntityReparentCommand extends Command { newLocalQuaternion, 'YXZ' ); - entity.setAttribute('rotation', { - x: THREE.MathUtils.radToDeg(euler.x), - y: THREE.MathUtils.radToDeg(euler.y), - z: THREE.MathUtils.radToDeg(euler.z) - }); + const rotX = THREE.MathUtils.radToDeg(euler.x); + const rotY = THREE.MathUtils.radToDeg(euler.y); + const rotZ = THREE.MathUtils.radToDeg(euler.z); + if (rotX === 0 && rotY === 0 && rotZ === 0) { + entity.removeAttribute('rotation'); + } else { + entity.setAttribute('rotation', { x: rotX, y: rotY, z: rotZ }); + } + + if ( + newLocalScale.x === 1 && + newLocalScale.y === 1 && + newLocalScale.z === 1 + ) { + entity.removeAttribute('scale'); + } else { + entity.setAttribute('scale', { + x: newLocalScale.x, + y: newLocalScale.y, + z: newLocalScale.z + }); + } } execute(nextCommandCallback) { From 3d2909b1c3e6c95c8b78fae9fb6ea77536794f8e Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 15 Apr 2026 15:45:43 +0200 Subject: [PATCH 8/8] Remove non-existing animatetop animation from texture modal The animatetop keyframes were never defined, making the animation rules ineffective. Backport from aframevr/aframe-inspector#837 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/editor/style/textureModal.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/editor/style/textureModal.scss b/src/editor/style/textureModal.scss index ec77d25fc..b587b711f 100644 --- a/src/editor/style/textureModal.scss +++ b/src/editor/style/textureModal.scss @@ -28,9 +28,6 @@ display: flex; flex-direction: column; row-gap: 32px; - animation: animatetop 0.2s ease-out; - animation-duration: 0.2s; - animation-name: animatetop; background-color: rgb(34, 34, 34); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.5),