Skip to content

Commit 81d716b

Browse files
vincentfretinclaude
andcommitted
Add AssetRemoveCommand and asset deletion UI
New generic 'assetremove' command: removes an asset element from <a-assets>, capturing tag name, attributes and position so undo recreates it in place. For <a-material>, entities referencing the asset are re-resolved on undo so they use the recreated THREE.Material instance. UI: DELETE button in the materials modal (disabled for inline materials, confirmation mentions how many entities use the material) and a trash button on each asset image in the textures modal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a88fc24 commit 81d716b

5 files changed

Lines changed: 169 additions & 1 deletion

File tree

src/components/modals/ModalMaterials.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,30 @@ export default class ModalMaterials extends React.Component {
123123
AFRAME.INSPECTOR.execute('assetcreate', { tagName: 'a-material' });
124124
};
125125

126+
removeMaterial = () => {
127+
const el = this.state.selectedEl;
128+
// Inline materials are owned by their `material(...)` definitions.
129+
if (!el || !el.id) return;
130+
const material = el.getMaterial();
131+
const users = Array.from(
132+
document.querySelectorAll('a-scene [material]')
133+
).filter(
134+
(entity) =>
135+
entity.isEntity && entity.components?.material?.material === material
136+
);
137+
const message = users.length
138+
? 'Material `#' +
139+
el.id +
140+
'` is used by ' +
141+
users.length +
142+
' entit' +
143+
(users.length === 1 ? 'y' : 'ies') +
144+
'. Do you really want to remove it?'
145+
: 'Do you really want to remove material `#' + el.id + '`?';
146+
if (!confirm(message)) return;
147+
AFRAME.INSPECTOR.execute('assetremove', { assetEl: el });
148+
};
149+
126150
updateProperty = (name, value) => {
127151
const el = this.state.selectedEl;
128152
const propDef = el.schema[name];
@@ -289,6 +313,17 @@ export default class ModalMaterials extends React.Component {
289313
USE SELECTED
290314
</button>
291315
)}
316+
<button
317+
onClick={this.removeMaterial}
318+
disabled={!this.state.selectedEl || !this.state.selectedEl.id}
319+
title={
320+
this.state.selectedEl && !this.state.selectedEl.id
321+
? 'Inline materials are owned by their material(...) definition'
322+
: 'Remove the selected material asset'
323+
}
324+
>
325+
DELETE
326+
</button>
292327
</div>
293328
</div>
294329
<div className="materialsEditor">{this.renderEditor()}</div>

src/components/modals/ModalTextures.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { faSearch } from '@fortawesome/free-solid-svg-icons';
3+
import { faSearch, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
44
import { AwesomeIcon } from '../AwesomeIcon';
55
import Events from '../../lib/Events';
66
import Modal from './Modal';
@@ -225,6 +225,15 @@ export default class ModalTextures extends React.Component {
225225
this.setState({ filterText: e.target.value });
226226
};
227227

228+
deleteAsset = (image, event) => {
229+
event.stopPropagation();
230+
if (!confirm('Do you really want to remove asset `#' + image.id + '`?')) {
231+
return;
232+
}
233+
AFRAME.INSPECTOR.execute('assetremove', { assetEl: image.id });
234+
this.generateFromAssets();
235+
};
236+
228237
renderRegistryImages() {
229238
var self = this;
230239
let selectSample = function (image) {
@@ -419,6 +428,13 @@ export default class ModalTextures extends React.Component {
419428
<span>
420429
{image.width} x {image.height}
421430
</span>
431+
<a
432+
className="button delete-asset"
433+
title="Remove asset"
434+
onClick={this.deleteAsset.bind(this, image)}
435+
>
436+
<AwesomeIcon icon={faTrashAlt} />
437+
</a>
422438
</div>
423439
</li>
424440
);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Events from '../Events';
2+
import { Command } from '../command.js';
3+
4+
/**
5+
* Remove an asset element from <a-assets> (e.g., a-material, a-mixin, img,
6+
* audio, video, a-asset-item).
7+
*
8+
* The tag name, attributes and position are captured so undo can recreate the
9+
* asset in place. For <a-material>, entities referencing the asset are
10+
* re-resolved on undo so they use the recreated THREE.Material instance.
11+
*
12+
* @param editor Editor
13+
* @param payload Object containing assetEl (element or ID string).
14+
* @constructor
15+
*/
16+
export class AssetRemoveCommand extends Command {
17+
constructor(editor, payload = null) {
18+
super(editor);
19+
20+
this.type = 'assetremove';
21+
this.name = 'Remove Asset';
22+
23+
if (payload === null) return;
24+
25+
let assetEl;
26+
if (typeof payload.assetEl === 'string') {
27+
assetEl = document.getElementById(payload.assetEl);
28+
if (!assetEl) {
29+
console.error('Asset not found with ID:', payload.assetEl);
30+
return;
31+
}
32+
} else {
33+
assetEl = payload.assetEl;
34+
}
35+
this.assetId = assetEl.id;
36+
this.tagName = assetEl.tagName.toLowerCase();
37+
// Capture attributes and position for undo.
38+
this.attributes = {};
39+
for (const attr of assetEl.attributes) {
40+
if (attr.name === 'id') continue;
41+
this.attributes[attr.name] = attr.value;
42+
}
43+
this.index = assetEl.parentNode
44+
? Array.prototype.indexOf.call(assetEl.parentNode.children, assetEl)
45+
: -1;
46+
}
47+
48+
execute(nextCommandCallback) {
49+
const assetEl = document.getElementById(this.assetId);
50+
if (assetEl && assetEl.parentNode) {
51+
assetEl.parentNode.removeChild(assetEl);
52+
Events.emit('assetremove', this.assetId);
53+
}
54+
nextCommandCallback?.();
55+
}
56+
57+
undo(nextCommandCallback) {
58+
const sceneEl = AFRAME.scenes[0];
59+
let assetsEl = sceneEl.querySelector('a-assets');
60+
if (!assetsEl) {
61+
assetsEl = document.createElement('a-assets');
62+
sceneEl.appendChild(assetsEl);
63+
}
64+
const assetEl = document.createElement(this.tagName);
65+
assetEl.id = this.assetId;
66+
for (const name in this.attributes) {
67+
assetEl.setAttribute(name, this.attributes[name]);
68+
}
69+
assetsEl.insertBefore(assetEl, assetsEl.children[this.index] ?? null);
70+
Events.emit('assetcreate', assetEl);
71+
72+
// The recreated <a-material> is a new THREE.Material instance; re-resolve
73+
// the entities referencing it so they pick it up.
74+
if (assetEl.isMaterialAsset) {
75+
const ref = '#' + this.assetId;
76+
document.querySelectorAll('a-scene [material]').forEach((entity) => {
77+
if (
78+
entity.isEntity &&
79+
entity.getDOMAttribute('material')?.material === ref
80+
) {
81+
entity.setAttribute('material', 'material', ref);
82+
}
83+
});
84+
}
85+
nextCommandCallback?.(assetEl);
86+
}
87+
88+
toJSON() {
89+
const output = super.toJSON(this);
90+
output.tagName = this.tagName;
91+
output.assetId = this.assetId;
92+
output.attributes = this.attributes;
93+
output.index = this.index;
94+
return output;
95+
}
96+
97+
fromJSON(json) {
98+
super.fromJSON(json);
99+
this.tagName = json.tagName;
100+
this.assetId = json.assetId;
101+
this.attributes = json.attributes;
102+
this.index = json.index;
103+
}
104+
}

src/lib/commands/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AssetCreateCommand } from './AssetCreateCommand.js';
2+
import { AssetRemoveCommand } from './AssetRemoveCommand.js';
23
import { AssetUpdateCommand } from './AssetUpdateCommand.js';
34
import { ComponentAddCommand } from './ComponentAddCommand.js';
45
import { ComponentRemoveCommand } from './ComponentRemoveCommand.js';
@@ -17,6 +18,7 @@ commandsByType.set('entitycreate', EntityCreateCommand);
1718
commandsByType.set('entityremove', EntityRemoveCommand);
1819
commandsByType.set('entityreparent', EntityReparentCommand);
1920
commandsByType.set('assetcreate', AssetCreateCommand);
21+
commandsByType.set('assetremove', AssetRemoveCommand);
2022
commandsByType.set('assetupdate', AssetUpdateCommand);
2123
commandsByType.set('entityupdate', EntityUpdateCommand);
2224
commandsByType.set('multi', MultiCommand);

src/style/textureModal.styl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
cursor pointer
7171
margin 8px
7272
overflow hidden
73+
position relative
7374
width 155px
7475

7576
.gallery li.selected,
@@ -217,3 +218,13 @@
217218
.texture canvas
218219
border 1px solid var(--color-base-100)
219220
cursor pointer
221+
222+
.gallery li .delete-asset
223+
bottom 6px
224+
color var(--color-base-content)
225+
cursor pointer
226+
position absolute
227+
right 8px
228+
229+
.gallery li .delete-asset:hover
230+
color var(--color-primary)

0 commit comments

Comments
 (0)