@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.
@@ -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;;GAEG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,MAAM,GACX,IAAI,CAMN"}
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
- const current = getNodeProperty(node, op.path, {});
44
- const merged = deepMerge(current, op.value);
45
- setNodeProperty(node, op.path, merged, meta);
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;;GAEG;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;IACpC,MAAM,OAAO,GAAG,eAAe,CAA0B,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;IAC5C,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vuer-ai/vuer-rtc",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "CRDT-based real-time collaborative data structures",
6
6
  "main": "dist/index.js",
@@ -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
- const current = getNodeProperty<Record<string, unknown>>(node, op.path, {});
61
- const merged = deepMerge(current, op.value);
62
- setNodeProperty(node, op.path, merged, meta);
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
+ });