@vuer-ai/vuer-rtc 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/operations/apply/object.d.ts +2 -0
- package/dist/operations/apply/object.d.ts.map +1 -1
- package/dist/operations/apply/object.js +18 -3
- package/dist/operations/apply/object.js.map +1 -1
- package/package.json +1 -1
- package/src/operations/apply/object.ts +18 -3
- package/tests/operations/unified-schema.test.ts +304 -0
|
@@ -9,6 +9,8 @@ import type { OpMeta } from './types.js';
|
|
|
9
9
|
export declare function ObjectSet(draft: SceneGraph, op: ObjectSetOp, meta: OpMeta): void;
|
|
10
10
|
/**
|
|
11
11
|
* object.merge - Deep merge objects
|
|
12
|
+
*
|
|
13
|
+
* If path is "." or undefined, merge directly into the node (node root level)
|
|
12
14
|
*/
|
|
13
15
|
export declare function ObjectMerge(draft: SceneGraph, op: ObjectMergeOp, meta: OpMeta): void;
|
|
14
16
|
//# sourceMappingURL=object.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../../../src/operations/apply/object.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;GAEG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,MAAM,GACX,IAAI,CAIN;AA8BD
|
|
1
|
+
{"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../../../src/operations/apply/object.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;GAEG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,MAAM,GACX,IAAI,CAIN;AA8BD;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,MAAM,GACX,IAAI,CAmBN"}
|
|
@@ -35,13 +35,28 @@ function deepMerge(target, source) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* object.merge - Deep merge objects
|
|
38
|
+
*
|
|
39
|
+
* If path is "." or undefined, merge directly into the node (node root level)
|
|
38
40
|
*/
|
|
39
41
|
export function ObjectMerge(draft, op, meta) {
|
|
40
42
|
const node = draft.nodes[op.key];
|
|
41
43
|
if (!node || node.deletedAt)
|
|
42
44
|
return;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (op.path === '.' || op.path === undefined) {
|
|
46
|
+
// Merge directly into node root
|
|
47
|
+
for (const [key, value] of Object.entries(op.value)) {
|
|
48
|
+
// Skip internal fields
|
|
49
|
+
if (['key', 'tag', 'children', 'clock', 'lamportTime', 'createdAt', 'updatedAt', 'deletedAt'].includes(key)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
node[key] = value;
|
|
53
|
+
}
|
|
54
|
+
node.updatedAt = meta.timestamp;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const current = getNodeProperty(node, op.path, {});
|
|
58
|
+
const merged = deepMerge(current, op.value);
|
|
59
|
+
setNodeProperty(node, op.path, merged, meta);
|
|
60
|
+
}
|
|
46
61
|
}
|
|
47
62
|
//# sourceMappingURL=object.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object.js","sourceRoot":"","sources":["../../../src/operations/apply/object.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAElF;;GAEG;AACH,MAAM,UAAU,SAAS,CACvB,KAAiB,EACjB,EAAe,EACf,IAAY;IAEZ,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS;QAAE,OAAO;IACpC,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;AAC3D,CAAC;AAED;;GAEG;AACH,SAAS,SAAS,CAAC,MAA+B,EAAE,MAA+B;IACjF,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAE9B,IACE,SAAS,KAAK,IAAI;YAClB,OAAO,SAAS,KAAK,QAAQ;YAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;YACzB,SAAS,KAAK,IAAI;YAClB,OAAO,SAAS,KAAK,QAAQ;YAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EACzB,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CACrB,SAAoC,EACpC,SAAoC,CACrC,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"object.js","sourceRoot":"","sources":["../../../src/operations/apply/object.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAElF;;GAEG;AACH,MAAM,UAAU,SAAS,CACvB,KAAiB,EACjB,EAAe,EACf,IAAY;IAEZ,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS;QAAE,OAAO;IACpC,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;AAC3D,CAAC;AAED;;GAEG;AACH,SAAS,SAAS,CAAC,MAA+B,EAAE,MAA+B;IACjF,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAE9B,IACE,SAAS,KAAK,IAAI;YAClB,OAAO,SAAS,KAAK,QAAQ;YAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;YACzB,SAAS,KAAK,IAAI;YAClB,OAAO,SAAS,KAAK,QAAQ;YAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EACzB,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CACrB,SAAoC,EACpC,SAAoC,CACrC,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,KAAiB,EACjB,EAAiB,EACjB,IAAY;IAEZ,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS;QAAE,OAAO;IAEpC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7C,gCAAgC;QAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;YACpD,uBAAuB;YACvB,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC5G,SAAS;YACX,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAClC,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,GAAG,eAAe,CAA0B,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5C,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -49,6 +49,8 @@ function deepMerge(target: Record<string, unknown>, source: Record<string, unkno
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* object.merge - Deep merge objects
|
|
52
|
+
*
|
|
53
|
+
* If path is "." or undefined, merge directly into the node (node root level)
|
|
52
54
|
*/
|
|
53
55
|
export function ObjectMerge(
|
|
54
56
|
draft: SceneGraph,
|
|
@@ -57,7 +59,20 @@ export function ObjectMerge(
|
|
|
57
59
|
): void {
|
|
58
60
|
const node = draft.nodes[op.key];
|
|
59
61
|
if (!node || node.deletedAt) return;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
if (op.path === '.' || op.path === undefined) {
|
|
64
|
+
// Merge directly into node root
|
|
65
|
+
for (const [key, value] of Object.entries(op.value)) {
|
|
66
|
+
// Skip internal fields
|
|
67
|
+
if (['key', 'tag', 'children', 'clock', 'lamportTime', 'createdAt', 'updatedAt', 'deletedAt'].includes(key)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
node[key] = value;
|
|
71
|
+
}
|
|
72
|
+
node.updatedAt = meta.timestamp;
|
|
73
|
+
} else {
|
|
74
|
+
const current = getNodeProperty<Record<string, unknown>>(node, op.path, {});
|
|
75
|
+
const merged = deepMerge(current, op.value);
|
|
76
|
+
setNodeProperty(node, op.path, merged, meta);
|
|
77
|
+
}
|
|
63
78
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for unified operation schema:
|
|
3
|
+
* - key field targeting specific nodes (defaults to "." for root)
|
|
4
|
+
* - path: "." for node root level operations
|
|
5
|
+
* - Cross-node operations in single message
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
9
|
+
import { createEmptyGraph, applyMessage } from '../../src/index.js';
|
|
10
|
+
import type { SceneGraph, CRDTMessage, Operation } from '../../src/operations/OperationTypes.js';
|
|
11
|
+
|
|
12
|
+
function createMsg(ops: Operation | Operation[], lamport = 1): CRDTMessage {
|
|
13
|
+
return {
|
|
14
|
+
id: `msg-${Math.random().toString(36).slice(2)}`,
|
|
15
|
+
sessionId: 'test-session',
|
|
16
|
+
clock: { 'test-session': lamport },
|
|
17
|
+
lamportTime: lamport,
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
ops: Array.isArray(ops) ? ops : [ops],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setupGraph(): SceneGraph {
|
|
24
|
+
let graph = createEmptyGraph();
|
|
25
|
+
|
|
26
|
+
// Insert root scene
|
|
27
|
+
graph = applyMessage(
|
|
28
|
+
graph,
|
|
29
|
+
createMsg({
|
|
30
|
+
key: '',
|
|
31
|
+
otype: 'node.insert',
|
|
32
|
+
path: 'children',
|
|
33
|
+
value: { key: 'scene', tag: 'Scene', name: 'Root Scene' },
|
|
34
|
+
}, 1)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Insert two child nodes
|
|
38
|
+
graph = applyMessage(
|
|
39
|
+
graph,
|
|
40
|
+
createMsg({
|
|
41
|
+
key: 'scene',
|
|
42
|
+
otype: 'node.insert',
|
|
43
|
+
path: 'children',
|
|
44
|
+
value: {
|
|
45
|
+
key: 'box-1',
|
|
46
|
+
tag: 'Mesh',
|
|
47
|
+
name: 'Box 1',
|
|
48
|
+
position: [0, 0, 0],
|
|
49
|
+
visible: true,
|
|
50
|
+
metadata: { author: 'user-1' },
|
|
51
|
+
},
|
|
52
|
+
}, 2)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
graph = applyMessage(
|
|
56
|
+
graph,
|
|
57
|
+
createMsg({
|
|
58
|
+
key: 'scene',
|
|
59
|
+
otype: 'node.insert',
|
|
60
|
+
path: 'children',
|
|
61
|
+
value: {
|
|
62
|
+
key: 'box-2',
|
|
63
|
+
tag: 'Mesh',
|
|
64
|
+
name: 'Box 2',
|
|
65
|
+
position: [5, 0, 0],
|
|
66
|
+
visible: false,
|
|
67
|
+
metadata: { author: 'user-2' },
|
|
68
|
+
},
|
|
69
|
+
}, 3)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return graph;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('Unified Schema: Key Field', () => {
|
|
76
|
+
let graph: SceneGraph;
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
graph = setupGraph();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should target specific node with key', () => {
|
|
83
|
+
const op: Operation = {
|
|
84
|
+
key: 'box-1',
|
|
85
|
+
otype: 'vector3.set',
|
|
86
|
+
path: 'position',
|
|
87
|
+
value: [10, 20, 30],
|
|
88
|
+
};
|
|
89
|
+
// Use lamportTime > 3 (nodes were created at lamport 1, 2, 3)
|
|
90
|
+
const result = applyMessage(graph, createMsg(op, 10));
|
|
91
|
+
|
|
92
|
+
expect(result.nodes['box-1'].position).toEqual([10, 20, 30]);
|
|
93
|
+
expect(result.nodes['box-2'].position).toEqual([5, 0, 0]); // unchanged
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should use empty string key for root insertion', () => {
|
|
97
|
+
const newGraph = createEmptyGraph();
|
|
98
|
+
const op: Operation = {
|
|
99
|
+
key: '',
|
|
100
|
+
otype: 'node.insert',
|
|
101
|
+
path: 'children',
|
|
102
|
+
value: { key: 'new-root', tag: 'Scene', name: 'New Root' },
|
|
103
|
+
};
|
|
104
|
+
const result = applyMessage(newGraph, createMsg(op));
|
|
105
|
+
|
|
106
|
+
expect(result.nodes['new-root']).toBeDefined();
|
|
107
|
+
expect(result.rootKey).toBe('new-root');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should apply operations to different nodes in sequence', () => {
|
|
111
|
+
// Move box-1 up
|
|
112
|
+
graph = applyMessage(
|
|
113
|
+
graph,
|
|
114
|
+
createMsg({
|
|
115
|
+
key: 'box-1',
|
|
116
|
+
otype: 'vector3.add',
|
|
117
|
+
path: 'position',
|
|
118
|
+
value: [0, 5, 0],
|
|
119
|
+
}, 4)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Move box-2 right
|
|
123
|
+
graph = applyMessage(
|
|
124
|
+
graph,
|
|
125
|
+
createMsg({
|
|
126
|
+
key: 'box-2',
|
|
127
|
+
otype: 'vector3.add',
|
|
128
|
+
path: 'position',
|
|
129
|
+
value: [5, 0, 0],
|
|
130
|
+
}, 5)
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(graph.nodes['box-1'].position).toEqual([0, 5, 0]);
|
|
134
|
+
expect(graph.nodes['box-2'].position).toEqual([10, 0, 0]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('Unified Schema: Path Field', () => {
|
|
139
|
+
let graph: SceneGraph;
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
graph = setupGraph();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should merge at node root with path: "."', () => {
|
|
146
|
+
const op: Operation = {
|
|
147
|
+
key: 'box-1',
|
|
148
|
+
otype: 'object.merge',
|
|
149
|
+
path: '.',
|
|
150
|
+
value: { castShadow: true, receiveShadow: true },
|
|
151
|
+
};
|
|
152
|
+
const result = applyMessage(graph, createMsg(op, 10));
|
|
153
|
+
|
|
154
|
+
expect(result.nodes['box-1'].castShadow).toBe(true);
|
|
155
|
+
expect(result.nodes['box-1'].receiveShadow).toBe(true);
|
|
156
|
+
expect(result.nodes['box-1'].name).toBe('Box 1'); // preserved
|
|
157
|
+
expect(result.nodes['box-1'].position).toEqual([0, 0, 0]); // preserved
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should set nested property with dot path', () => {
|
|
161
|
+
const op: Operation = {
|
|
162
|
+
key: 'box-1',
|
|
163
|
+
otype: 'object.merge',
|
|
164
|
+
path: 'metadata',
|
|
165
|
+
value: { version: 2, modified: true },
|
|
166
|
+
};
|
|
167
|
+
const result = applyMessage(graph, createMsg(op));
|
|
168
|
+
|
|
169
|
+
expect(result.nodes['box-1'].metadata).toEqual({
|
|
170
|
+
author: 'user-1',
|
|
171
|
+
version: 2,
|
|
172
|
+
modified: true,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should set property directly with path', () => {
|
|
177
|
+
const op: Operation = {
|
|
178
|
+
key: 'box-1',
|
|
179
|
+
otype: 'boolean.set',
|
|
180
|
+
path: 'visible',
|
|
181
|
+
value: false,
|
|
182
|
+
};
|
|
183
|
+
const result = applyMessage(graph, createMsg(op, 10));
|
|
184
|
+
|
|
185
|
+
expect(result.nodes['box-1'].visible).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Unified Schema: Batch Operations', () => {
|
|
190
|
+
let graph: SceneGraph;
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
graph = setupGraph();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should apply multiple operations in single message', () => {
|
|
197
|
+
const ops: Operation[] = [
|
|
198
|
+
{ key: 'box-1', otype: 'vector3.set', path: 'position', value: [1, 1, 1] },
|
|
199
|
+
{ key: 'box-2', otype: 'vector3.set', path: 'position', value: [2, 2, 2] },
|
|
200
|
+
{ key: 'box-1', otype: 'boolean.set', path: 'visible', value: false },
|
|
201
|
+
{ key: 'box-2', otype: 'boolean.set', path: 'visible', value: true },
|
|
202
|
+
];
|
|
203
|
+
const result = applyMessage(graph, createMsg(ops, 4));
|
|
204
|
+
|
|
205
|
+
expect(result.nodes['box-1'].position).toEqual([1, 1, 1]);
|
|
206
|
+
expect(result.nodes['box-2'].position).toEqual([2, 2, 2]);
|
|
207
|
+
expect(result.nodes['box-1'].visible).toBe(false);
|
|
208
|
+
expect(result.nodes['box-2'].visible).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should apply additive operations across nodes', () => {
|
|
212
|
+
const ops: Operation[] = [
|
|
213
|
+
{ key: 'box-1', otype: 'vector3.add', path: 'position', value: [1, 0, 0] },
|
|
214
|
+
{ key: 'box-1', otype: 'vector3.add', path: 'position', value: [0, 1, 0] },
|
|
215
|
+
{ key: 'box-2', otype: 'vector3.add', path: 'position', value: [0, 0, 1] },
|
|
216
|
+
];
|
|
217
|
+
const result = applyMessage(graph, createMsg(ops, 4));
|
|
218
|
+
|
|
219
|
+
expect(result.nodes['box-1'].position).toEqual([1, 1, 0]);
|
|
220
|
+
expect(result.nodes['box-2'].position).toEqual([5, 0, 1]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should handle insert and update in same message', () => {
|
|
224
|
+
const ops: Operation[] = [
|
|
225
|
+
{
|
|
226
|
+
key: 'scene',
|
|
227
|
+
otype: 'node.insert',
|
|
228
|
+
path: 'children',
|
|
229
|
+
value: { key: 'box-3', tag: 'Mesh', name: 'Box 3', position: [0, 0, 0] },
|
|
230
|
+
},
|
|
231
|
+
{ key: 'box-3', otype: 'vector3.set', path: 'position', value: [10, 10, 10] },
|
|
232
|
+
];
|
|
233
|
+
const result = applyMessage(graph, createMsg(ops, 4));
|
|
234
|
+
|
|
235
|
+
expect(result.nodes['box-3']).toBeDefined();
|
|
236
|
+
expect(result.nodes['box-3'].position).toEqual([10, 10, 10]);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Unified Schema: Type-specific Operations on Nodes', () => {
|
|
241
|
+
let graph: SceneGraph;
|
|
242
|
+
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
graph = setupGraph();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should apply number operations to node properties', () => {
|
|
248
|
+
// Add scale property first
|
|
249
|
+
graph = applyMessage(
|
|
250
|
+
graph,
|
|
251
|
+
createMsg({
|
|
252
|
+
key: 'box-1',
|
|
253
|
+
otype: 'object.merge',
|
|
254
|
+
path: '.',
|
|
255
|
+
value: { opacity: 1.0 },
|
|
256
|
+
}, 4)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const op: Operation = {
|
|
260
|
+
key: 'box-1',
|
|
261
|
+
otype: 'number.multiply',
|
|
262
|
+
path: 'opacity',
|
|
263
|
+
value: 0.5,
|
|
264
|
+
};
|
|
265
|
+
const result = applyMessage(graph, createMsg(op, 5));
|
|
266
|
+
|
|
267
|
+
expect(result.nodes['box-1'].opacity).toBe(0.5);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should apply string operations to node properties', () => {
|
|
271
|
+
const op: Operation = {
|
|
272
|
+
key: 'box-1',
|
|
273
|
+
otype: 'string.concat',
|
|
274
|
+
path: 'name',
|
|
275
|
+
value: ' (modified)',
|
|
276
|
+
};
|
|
277
|
+
const result = applyMessage(graph, createMsg(op, 4));
|
|
278
|
+
|
|
279
|
+
expect(result.nodes['box-1'].name).toBe('Box 1 (modified)');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should apply array operations to node properties', () => {
|
|
283
|
+
// Add tags property first via object.set on a specific property
|
|
284
|
+
graph = applyMessage(
|
|
285
|
+
graph,
|
|
286
|
+
createMsg({
|
|
287
|
+
key: 'box-1',
|
|
288
|
+
otype: 'array.set',
|
|
289
|
+
path: 'tags',
|
|
290
|
+
value: ['mesh'],
|
|
291
|
+
}, 10)
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const op: Operation = {
|
|
295
|
+
key: 'box-1',
|
|
296
|
+
otype: 'array.push',
|
|
297
|
+
path: 'tags',
|
|
298
|
+
value: 'selectable',
|
|
299
|
+
};
|
|
300
|
+
const result = applyMessage(graph, createMsg(op, 11));
|
|
301
|
+
|
|
302
|
+
expect(result.nodes['box-1'].tags).toEqual(['mesh', 'selectable']);
|
|
303
|
+
});
|
|
304
|
+
});
|