Skip to content

Commit db6962b

Browse files
Make all commands and undo/redo stack serializable (#58)
1 parent 610e1fe commit db6962b

10 files changed

Lines changed: 380 additions & 69 deletions

src/lib/command.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,25 @@
77
export class Command {
88
constructor(editor) {
99
this.id = -1;
10+
this.inMemory = false;
1011
this.updatable = false;
1112
this.type = '';
1213
this.name = '';
1314
this.editor = editor;
1415
}
16+
17+
toJSON() {
18+
const output = {};
19+
output.type = this.type;
20+
output.id = this.id;
21+
output.name = this.name;
22+
return output;
23+
}
24+
25+
fromJSON(json) {
26+
this.inMemory = true;
27+
this.type = json.type;
28+
this.id = json.id;
29+
this.name = json.name;
30+
}
1531
}

src/lib/commands/ComponentAddCommand.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,41 @@ import Events from '../Events.js';
22
import { Command } from '../command.js';
33
import { createUniqueId } from '../entity.js';
44

5+
/**
6+
* Command to add a component to an entity
7+
* @param editor Editor
8+
* @param payload Object containing entity (element or ID string), component, and value
9+
* @constructor
10+
*/
511
export class ComponentAddCommand extends Command {
6-
constructor(editor, payload) {
12+
constructor(editor, payload = null) {
713
super(editor);
814

915
this.type = 'componentadd';
1016
this.name = 'Add Component';
1117
this.updatable = false;
1218

13-
const entity = payload.entity;
14-
if (!entity.id) {
15-
entity.id = createUniqueId();
19+
if (payload !== null) {
20+
// Handle case where entity is passed as ID string when used with multi command
21+
let entity;
22+
if (typeof payload.entity === 'string') {
23+
entity = document.querySelector(`#${payload.entity}:not(a-mixin)`);
24+
if (!entity) {
25+
console.error('Entity not found with ID:', payload.entity);
26+
return;
27+
}
28+
this.entityId = payload.entity;
29+
} else {
30+
entity = payload.entity;
31+
if (!entity.id) {
32+
entity.id = createUniqueId();
33+
}
34+
this.entityId = entity.id;
35+
}
36+
37+
this.component = payload.component;
38+
this.value = payload.value;
1639
}
17-
this.entityId = entity.id;
18-
this.component = payload.component;
19-
this.value = payload.value;
2040
}
2141

2242
execute(nextCommandCallback) {
@@ -43,4 +63,19 @@ export class ComponentAddCommand extends Command {
4363
nextCommandCallback?.(entity);
4464
}
4565
}
66+
67+
toJSON() {
68+
const output = super.toJSON(this);
69+
output.entityId = this.entityId;
70+
output.component = this.component;
71+
output.value = this.value;
72+
return output;
73+
}
74+
75+
fromJSON(json) {
76+
super.fromJSON(json);
77+
this.entityId = json.entityId;
78+
this.component = json.component;
79+
this.value = json.value;
80+
}
4681
}

src/lib/commands/ComponentRemoveCommand.js

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,47 @@ import Events from '../Events.js';
22
import { Command } from '../command.js';
33
import { createUniqueId } from '../entity.js';
44

5+
/**
6+
* Command to remove a component from an entity
7+
* @param editor Editor
8+
* @param payload Object containing entity (element or ID string) and component
9+
* @constructor
10+
*/
511
export class ComponentRemoveCommand extends Command {
6-
constructor(editor, payload) {
12+
constructor(editor, payload = null) {
713
super(editor);
814

915
this.type = 'componentremove';
1016
this.name = 'Remove Component';
1117
this.updatable = false;
1218

13-
const entity = payload.entity;
14-
if (!entity.id) {
15-
entity.id = createUniqueId();
19+
if (payload !== null) {
20+
// Handle case where entity is passed as ID string when used with multi command
21+
let entity;
22+
if (typeof payload.entity === 'string') {
23+
entity = document.querySelector(`#${payload.entity}:not(a-mixin)`);
24+
if (!entity) {
25+
console.error('Entity not found with ID:', payload.entity);
26+
return;
27+
}
28+
this.entityId = payload.entity;
29+
} else {
30+
entity = payload.entity;
31+
if (!entity.id) {
32+
entity.id = createUniqueId();
33+
}
34+
this.entityId = entity.id;
35+
}
36+
37+
this.component = payload.component;
38+
39+
const component =
40+
entity.components[payload.component] ??
41+
AFRAME.components[payload.component];
42+
this.value = component.isSingleProperty
43+
? component.schema.stringify(entity.getAttribute(payload.component))
44+
: structuredClone(entity.getDOMAttribute(payload.component));
1645
}
17-
this.entityId = entity.id;
18-
this.component = payload.component;
19-
20-
const component =
21-
entity.components[payload.component] ??
22-
AFRAME.components[payload.component];
23-
this.value = component.isSingleProperty
24-
? component.schema.stringify(entity.getAttribute(payload.component))
25-
: structuredClone(entity.getDOMAttribute(payload.component));
2646
}
2747

2848
execute(nextCommandCallback) {
@@ -49,4 +69,19 @@ export class ComponentRemoveCommand extends Command {
4969
nextCommandCallback?.(entity);
5070
}
5171
}
72+
73+
toJSON() {
74+
const output = super.toJSON(this);
75+
output.entityId = this.entityId;
76+
output.component = this.component;
77+
output.value = this.value;
78+
return output;
79+
}
80+
81+
fromJSON(json) {
82+
super.fromJSON(json);
83+
this.entityId = json.entityId;
84+
this.component = json.component;
85+
this.value = json.value;
86+
}
5287
}

src/lib/commands/EntityCloneCommand.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
import Events from '../Events.js';
22
import { Command } from '../command.js';
3-
import { cloneEntityImpl, createUniqueId, insertAfter } from '../entity.js';
3+
import {
4+
cloneEntityImpl,
5+
createUniqueId,
6+
elementToObject,
7+
insertAfter,
8+
objectToElement
9+
} from '../entity.js';
410

511
export class EntityCloneCommand extends Command {
6-
constructor(editor, entity) {
12+
constructor(editor, entity = null) {
713
super(editor);
814

915
this.type = 'entityclone';
1016
this.name = 'Clone Entity';
1117
this.updatable = false;
12-
if (!entity.id) {
13-
entity.id = createUniqueId();
14-
}
15-
this.entityIdToClone = entity.id;
18+
1619
this.entityId = null;
17-
this.detachedClone = null;
20+
if (entity !== null) {
21+
if (!entity.id) {
22+
entity.id = createUniqueId();
23+
}
24+
this.entityIdToClone = entity.id;
25+
this.detachedClone = null;
26+
}
1827
}
1928

2029
execute(nextCommandCallback) {
@@ -29,13 +38,14 @@ export class EntityCloneCommand extends Command {
2938
if (!this.detachedClone) {
3039
this.detachedClone = cloneEntityImpl(entityToClone);
3140
}
41+
if (this.detachedClone === null) return;
3242
const clone = this.detachedClone.cloneNode(true);
3343
clone.addEventListener(
3444
'loaded',
35-
function () {
45+
() => {
3646
clone.pause();
3747
Events.emit('entityclone', clone);
38-
AFRAME.INSPECTOR.selectEntity(clone);
48+
this.editor.selectEntity(clone);
3949
},
4050
{ once: true }
4151
);
@@ -58,4 +68,26 @@ export class EntityCloneCommand extends Command {
5868
nextCommandCallback?.(entity);
5969
}
6070
}
71+
72+
toJSON() {
73+
const output = super.toJSON(this);
74+
output.entityIdToClone = this.entityIdToClone;
75+
output.definition = this.detachedClone
76+
? elementToObject(this.detachedClone)
77+
: null;
78+
output.entityId = this.entityId;
79+
return output;
80+
}
81+
82+
fromJSON(json) {
83+
super.fromJSON(json);
84+
this.entityIdToClone = json.entityIdToClone;
85+
if (json.definition) {
86+
this.detachedClone = objectToElement(json.definition);
87+
this.detachedClone.flushToDOM();
88+
} else {
89+
this.detachedClone = null;
90+
}
91+
this.entityId = json.entityId;
92+
}
6193
}

src/lib/commands/EntityCreateCommand.js

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,28 @@ import { createEntity, createUniqueId } from '../entity.js';
1010
* @constructor
1111
*/
1212
export class EntityCreateCommand extends Command {
13-
constructor(editor, definition, callback = undefined) {
13+
constructor(editor, definition = null, callback = undefined) {
1414
super(editor);
1515

1616
this.type = 'entitycreate';
1717
this.name = 'Create Entity';
18-
this.definition = definition;
1918
this.callback = callback;
20-
this.entityId = null;
21-
// If we have parentEl in the definition, be sure it has an id and store the definition with the id
22-
if (
23-
this.definition.parentEl &&
24-
typeof this.definition.parentEl !== 'string'
25-
) {
26-
if (!this.definition.parentEl.id) {
27-
this.definition.parentEl.id = createUniqueId();
19+
if (definition !== null) {
20+
this.definition = definition;
21+
this.entityId = definition.id ?? null;
22+
// If we have parentEl in the definition, be sure it has an id and store the definition with the id
23+
if (
24+
this.definition.parentEl &&
25+
typeof this.definition.parentEl !== 'string'
26+
) {
27+
if (!this.definition.parentEl.id) {
28+
this.definition.parentEl.id = createUniqueId();
29+
}
30+
this.definition = {
31+
...this.definition,
32+
parentEl: this.definition.parentEl.id
33+
};
2834
}
29-
this.definition = {
30-
...this.definition,
31-
parentEl: this.definition.parentEl.id
32-
};
3335
}
3436
}
3537

@@ -43,7 +45,9 @@ export class EntityCreateCommand extends Command {
4345
};
4446
let parentEl;
4547
if (this.definition.parentEl) {
46-
parentEl = document.getElementById(this.definition.parentEl);
48+
parentEl = document.querySelector(
49+
`#${this.definition.parentEl}:not(a-mixin)`
50+
);
4751
}
4852
if (!parentEl) {
4953
parentEl = document.querySelector(this.editor.config.defaultParent);
@@ -67,4 +71,17 @@ export class EntityCreateCommand extends Command {
6771
nextCommandCallback?.(entity);
6872
}
6973
}
74+
75+
toJSON() {
76+
const output = super.toJSON(this);
77+
output.definition = this.definition;
78+
output.entityId = this.entityId;
79+
return output;
80+
}
81+
82+
fromJSON(json) {
83+
super.fromJSON(json);
84+
this.definition = json.definition;
85+
this.entityId = json.entityId;
86+
}
7087
}

src/lib/commands/EntityRemoveCommand.js

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,38 @@ import Events from '../Events';
22
import { Command } from '../command.js';
33
import { findClosestEntity, prepareForSerialization } from '../entity.js';
44

5+
/**
6+
* Command to remove an entity from the scene
7+
* @param editor Editor
8+
* @param entity Entity element or ID string
9+
* @constructor
10+
*/
511
export class EntityRemoveCommand extends Command {
6-
constructor(editor, entity) {
12+
constructor(editor, entity = null) {
713
super(editor);
814

915
this.type = 'entityremove';
1016
this.name = 'Remove Entity';
1117
this.updatable = false;
1218

13-
this.entity = entity;
14-
// Store the parent element and index for precise reinsertion
15-
this.parentEl = entity.parentNode;
16-
this.index = Array.from(this.parentEl.children).indexOf(entity);
19+
if (entity !== null) {
20+
// Handle case where entity is passed as ID string when used with multi command
21+
if (typeof entity === 'string') {
22+
this.entity = document.querySelector(`#${entity}:not(a-mixin)`);
23+
if (!this.entity) {
24+
console.error('Entity not found with ID:', entity);
25+
return;
26+
}
27+
this.entityId = entity;
28+
} else {
29+
this.entity = entity;
30+
this.entityId = entity.id;
31+
}
32+
33+
// Store the parent element and index for precise reinsertion
34+
this.parentEl = this.entity.parentNode;
35+
this.index = Array.from(this.parentEl.children).indexOf(this.entity);
36+
}
1737
}
1838

1939
execute(nextCommandCallback) {
@@ -51,4 +71,20 @@ export class EntityRemoveCommand extends Command {
5171
{ once: true }
5272
);
5373
}
74+
75+
toJSON() {
76+
const output = super.toJSON(this);
77+
output.entityId = this.entity.id;
78+
output.parentId = this.parentEl.id;
79+
output.index = this.index;
80+
return output;
81+
}
82+
83+
fromJSON(json) {
84+
super.fromJSON(json);
85+
this.entity = document.querySelector(`#${json.entityId}:not(a-mixin)`);
86+
this.entityId = json.entityId;
87+
this.parentEl = document.querySelector(`#${json.parentId}:not(a-mixin)`);
88+
this.index = json.index;
89+
}
5490
}

0 commit comments

Comments
 (0)