@viamrobotics/motion-tools 1.19.0 → 1.21.0

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.
Files changed (33) hide show
  1. package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
  2. package/dist/buf/draw/v1/metadata_pb.js +55 -0
  3. package/dist/buf/draw/v1/service_connect.d.ts +34 -1
  4. package/dist/buf/draw/v1/service_connect.js +34 -1
  5. package/dist/buf/draw/v1/service_pb.d.ts +136 -0
  6. package/dist/buf/draw/v1/service_pb.js +201 -0
  7. package/dist/components/Entities/Arrows/ArrowGroups.svelte +1 -0
  8. package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
  9. package/dist/components/Entities/Entities.svelte +1 -0
  10. package/dist/components/Entities/Points.svelte +23 -23
  11. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
  12. package/dist/components/FileDrop/FileDrop.svelte +8 -1
  13. package/dist/components/PCD.svelte +9 -1
  14. package/dist/components/PCD.svelte.d.ts +2 -0
  15. package/dist/components/SceneProviders.svelte +2 -0
  16. package/dist/components/Snapshot.svelte +12 -7
  17. package/dist/components/overlay/AddRelationship.svelte +25 -3
  18. package/dist/components/overlay/Details.svelte +293 -227
  19. package/dist/draw.d.ts +22 -9
  20. package/dist/draw.js +75 -46
  21. package/dist/ecs/relations.js +1 -1
  22. package/dist/ecs/traits.d.ts +2 -0
  23. package/dist/ecs/traits.js +63 -0
  24. package/dist/hooks/useDrawService.svelte.d.ts +2 -0
  25. package/dist/hooks/useDrawService.svelte.js +139 -20
  26. package/dist/hooks/useRelationships.svelte.d.ts +12 -0
  27. package/dist/hooks/useRelationships.svelte.js +78 -0
  28. package/dist/hooks/useWorldState.svelte.js +10 -4
  29. package/dist/metadata.d.ts +7 -3
  30. package/dist/metadata.js +26 -2
  31. package/dist/snapshot.d.ts +6 -1
  32. package/dist/snapshot.js +10 -5
  33. package/package.json +5 -2
package/dist/draw.d.ts CHANGED
@@ -2,18 +2,31 @@ import type { TransformWithUUID } from '@viamrobotics/sdk';
2
2
  import type { Entity, Trait, World } from 'koota';
3
3
  import type { Transform as TransformProto } from './buf/common/v1/common_pb';
4
4
  import type { Drawing } from './buf/draw/v1/drawing_pb';
5
+ import type { Relationship } from './metadata';
5
6
  import { type Metadata } from './metadata';
6
7
  export type Transform = TransformWithUUID | TransformProto;
7
- type Options = {
8
+ export declare const uuidBytesToString: (bytes: Uint8Array | undefined) => string | undefined;
9
+ export declare const uuidStringToBytes: (uuid: string) => Uint8Array<ArrayBuffer>;
10
+ interface DrawOptions {
8
11
  removable?: boolean;
12
+ }
13
+ export declare const drawTransform: (world: World, { referenceFrame, poseInObserverFrame, physicalObject, metadata, uuid }: Transform, api: Trait, { removable }?: DrawOptions) => {
14
+ entity: Entity;
15
+ relationships: import("@bufbuild/protobuf").PlainMessage<import("./buf/draw/v1/metadata_pb").Relationship>[] | undefined;
9
16
  };
10
- export declare const drawTransform: (world: World, { referenceFrame, poseInObserverFrame, physicalObject, metadata }: Transform, api: Trait, options?: Options) => Entity;
11
- export declare const drawDrawing: (world: World, drawing: Drawing, api: Trait, options?: Options) => Entity[];
12
- export declare const updateTransform: (entity: Entity, { poseInObserverFrame, physicalObject, metadata }: Transform, options?: Options) => void;
13
- export declare const updateMetadata: (entity: Entity, metadata: Metadata, { pointCloud }?: {
17
+ export interface DrawingResult {
18
+ entity: Entity;
19
+ relationships: Relationship[] | undefined;
20
+ }
21
+ export declare const drawDrawing: (world: World, drawing: Drawing, api: Trait, { removable }?: DrawOptions) => DrawingResult;
22
+ export declare const updateTransform: (entity: Entity, { poseInObserverFrame, physicalObject, metadata }: Transform, { removable }?: DrawOptions) => {
23
+ entity: Entity;
24
+ relationships: import("@bufbuild/protobuf").PlainMessage<import("./buf/draw/v1/metadata_pb").Relationship>[] | undefined;
25
+ };
26
+ interface MetadataOptions {
14
27
  pointCloud?: boolean;
15
- }) => void;
16
- export declare const updateDrawing: (world: World, entities: Entity[], drawing: Drawing, api: Trait, options?: Options) => Entity[];
17
- export declare const addColorTraits: (entity: Entity, colors: Uint8Array) => void;
18
- export declare const setColorTraits: (entity: Entity, colors: Uint8Array) => void;
28
+ }
29
+ export declare const updateMetadata: (entity: Entity, metadata: Metadata, { pointCloud }?: MetadataOptions) => void;
30
+ export declare const updateDrawing: (world: World, entity: Entity, drawing: Drawing, { removable }?: DrawOptions) => DrawingResult;
31
+ export declare const updateModel: (world: World, entity: Entity, drawing: Drawing, api: Trait, { removable }?: DrawOptions) => DrawingResult;
19
32
  export {};
package/dist/draw.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Vector3, Vector4 } from 'three';
2
2
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
3
+ import { UuidTool } from 'uuid-tool';
3
4
  import { createBufferGeometry, preAllocateBufferGeometry, updateBufferGeometry, writeBufferGeometryRange, } from './attribute';
4
5
  import { asFloat32Array, asOpacity, asRGB, inMeters, isSingleColor, isVertexColors, STRIDE, } from './buffer';
5
- import { traits } from './ecs';
6
+ import { relations, traits } from './ecs';
6
7
  import { parsePcdInWorker } from './loaders/pcd';
7
8
  import { metadataFromStruct } from './metadata';
8
9
  import { createPose } from './transform';
@@ -21,12 +22,28 @@ const DEFAULT_LINE_DOT_COLORS = new Uint8Array([0, 0, 139]);
21
22
  const DEFAULT_POINTS_COLORS = new Uint8Array([51, 51, 51]);
22
23
  const DEFAULT_NURBS_COLORS = new Uint8Array([0, 255, 255]);
23
24
  const DEFAULT_OPACITY = 1;
24
- export const drawTransform = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata }, api, options = { removable: true }) => {
25
+ export const uuidBytesToString = (bytes) => {
26
+ if (!bytes || bytes.length === 0)
27
+ return undefined;
28
+ return UuidTool.toString([...bytes]);
29
+ };
30
+ export const uuidStringToBytes = (uuid) => {
31
+ const arr = new Uint8Array(16);
32
+ arr.set(UuidTool.toBytes(uuid));
33
+ return arr;
34
+ };
35
+ const isModel = (drawing) => {
36
+ return drawing.physicalObject?.geometryType?.case === 'model';
37
+ };
38
+ export const drawTransform = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata, uuid }, api, { removable = true } = {}) => {
25
39
  const entityTraits = [
26
40
  traits.Name(referenceFrame),
27
41
  traits.Pose(createPose(poseInObserverFrame?.pose)),
28
42
  api,
29
43
  ];
44
+ const uuidStr = uuidBytesToString(uuid);
45
+ if (uuidStr)
46
+ entityTraits.push(traits.UUID(uuidStr));
30
47
  if (physicalObject) {
31
48
  entityTraits.push(traits.Geometry(physicalObject));
32
49
  const center = physicalObject.center;
@@ -36,7 +53,7 @@ export const drawTransform = (world, { referenceFrame, poseInObserverFrame, phys
36
53
  else {
37
54
  entityTraits.push(traits.ReferenceFrame);
38
55
  }
39
- if (options.removable)
56
+ if (removable)
40
57
  entityTraits.push(traits.Removable);
41
58
  entityTraits.push(...traits.getParentTrait(poseInObserverFrame?.referenceFrame));
42
59
  const parsedMetadata = metadataFromStruct(metadata?.fields);
@@ -60,23 +77,28 @@ export const drawTransform = (world, { referenceFrame, poseInObserverFrame, phys
60
77
  const entity = world.spawn(...entityTraits);
61
78
  if (pointCloud)
62
79
  parsePointCloud(world, entity, pointCloud, parsedMetadata);
63
- return entity;
80
+ return { entity, relationships: parsedMetadata.relationships };
64
81
  };
65
- export const drawDrawing = (world, drawing, api, options = { removable: true }) => {
66
- const { referenceFrame, poseInObserverFrame, physicalObject, metadata } = drawing;
67
- if (physicalObject?.geometryType?.case === 'model')
68
- return drawModel(world, drawing, api, options);
69
- const entity = world.spawn(traits.Name(referenceFrame), traits.Pose(createPose(poseInObserverFrame?.pose)), api, ...traits.getParentTrait(poseInObserverFrame?.referenceFrame));
70
- if (options.removable)
82
+ export const drawDrawing = (world, drawing, api, { removable = true } = {}) => {
83
+ const { referenceFrame, poseInObserverFrame, metadata, uuid } = drawing;
84
+ if (isModel(drawing)) {
85
+ return drawModel(world, drawing, api, { removable });
86
+ }
87
+ const uuidTraits = [];
88
+ const uuidStr = uuidBytesToString(uuid);
89
+ if (uuidStr)
90
+ uuidTraits.push(traits.UUID(uuidStr));
91
+ const entity = world.spawn(traits.Name(referenceFrame), traits.Pose(createPose(poseInObserverFrame?.pose)), api, ...traits.getParentTrait(poseInObserverFrame?.referenceFrame), ...uuidTraits);
92
+ if (removable)
71
93
  entity.add(traits.Removable);
72
94
  if (metadata?.showAxesHelper)
73
95
  entity.add(traits.ShowAxesHelper);
74
96
  if (metadata?.invisible)
75
97
  entity.add(traits.Invisible);
76
98
  applyShape(entity, drawing);
77
- return [entity];
99
+ return { entity, relationships: metadata?.relationships };
78
100
  };
79
- export const updateTransform = (entity, { poseInObserverFrame, physicalObject, metadata }, options = { removable: true }) => {
101
+ export const updateTransform = (entity, { poseInObserverFrame, physicalObject, metadata }, { removable = true } = {}) => {
80
102
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
81
103
  traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
82
104
  if (physicalObject) {
@@ -89,13 +111,15 @@ export const updateTransform = (entity, { poseInObserverFrame, physicalObject, m
89
111
  entity.remove(traits.Center);
90
112
  }
91
113
  }
92
- updateMetadata(entity, metadataFromStruct(metadata?.fields), {
114
+ const parsedMetadata = metadataFromStruct(metadata?.fields);
115
+ updateMetadata(entity, parsedMetadata, {
93
116
  pointCloud: isPointCloud(physicalObject?.geometryType),
94
117
  });
95
- if (options.removable)
118
+ if (removable)
96
119
  entity.add(traits.Removable);
97
- if (!options.removable)
120
+ if (!removable)
98
121
  entity.remove(traits.Removable);
122
+ return { entity, relationships: parsedMetadata.relationships };
99
123
  };
100
124
  export const updateMetadata = (entity, metadata, { pointCloud = false } = {}) => {
101
125
  if (metadata.showAxesHelper)
@@ -109,28 +133,17 @@ export const updateMetadata = (entity, metadata, { pointCloud = false } = {}) =>
109
133
  const { colors, opacities } = metadata;
110
134
  if (colors) {
111
135
  if (pointCloud) {
112
- updateColors(entity, metadata);
113
- }
114
- else {
115
- setColorTraits(entity, colors);
136
+ updatePointCloudColors(entity, metadata);
116
137
  }
138
+ // Always set color traits so any subsequent async work can read them
139
+ setColorTraits(entity, colors);
117
140
  }
118
141
  entity.set(traits.Opacity, asOpacity(opacities, DEFAULT_OPACITY));
119
142
  };
120
- export const updateDrawing = (world, entities, drawing, api, options = { removable: true }) => {
121
- const { poseInObserverFrame, physicalObject, metadata } = drawing;
122
- if (physicalObject?.geometryType?.case === 'model') {
123
- for (const entity of entities) {
124
- if (world.has(entity))
125
- entity.destroy();
126
- }
127
- return drawDrawing(world, drawing, api, options);
128
- }
129
- if (entities.length === 0)
130
- return entities;
131
- const entity = entities[0];
143
+ export const updateDrawing = (world, entity, drawing, { removable = true } = {}) => {
144
+ const { poseInObserverFrame, metadata } = drawing;
132
145
  if (!world.has(entity))
133
- return entities;
146
+ return { entity, relationships: metadata?.relationships };
134
147
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
135
148
  traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
136
149
  if (metadata?.showAxesHelper)
@@ -141,8 +154,17 @@ export const updateDrawing = (world, entities, drawing, api, options = { removab
141
154
  entity.add(traits.Invisible);
142
155
  if (!metadata?.invisible)
143
156
  entity.remove(traits.Invisible);
157
+ if (removable)
158
+ entity.add(traits.Removable);
159
+ if (!removable)
160
+ entity.remove(traits.Removable);
144
161
  updateShape(entity, drawing);
145
- return entities;
162
+ return { entity, relationships: metadata?.relationships };
163
+ };
164
+ export const updateModel = (world, entity, drawing, api, { removable = true } = {}) => {
165
+ if (world.has(entity))
166
+ entity.destroy();
167
+ return drawDrawing(world, drawing, api, { removable });
146
168
  };
147
169
  const applyShape = (entity, { physicalObject, metadata }) => {
148
170
  const colors = metadata?.colors;
@@ -243,32 +265,39 @@ const applyShape = (entity, { physicalObject, metadata }) => {
243
265
  }
244
266
  }
245
267
  };
246
- const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata }, api, { removable = true }) => {
247
- const entities = [];
248
- const geometryType = physicalObject?.geometryType;
249
- if (geometryType?.case !== 'model')
250
- return entities;
268
+ const drawModel = (world, model, api, { removable = true }) => {
269
+ const { referenceFrame, physicalObject, poseInObserverFrame, metadata, uuid } = model;
270
+ const { animationName, assets, scale } = physicalObject.geometryType.value;
251
271
  const baseTraits = [
252
272
  traits.Name(referenceFrame),
253
273
  traits.Pose(createPose(poseInObserverFrame?.pose)),
254
274
  api,
255
275
  ...traits.getParentTrait(poseInObserverFrame?.referenceFrame),
256
276
  ];
277
+ const uuidStr = uuidBytesToString(uuid);
278
+ if (uuidStr)
279
+ baseTraits.push(traits.UUID(uuidStr));
257
280
  if (removable)
258
281
  baseTraits.push(traits.Removable);
259
282
  if (metadata?.invisible)
260
283
  baseTraits.push(traits.Invisible);
261
- entities.push(world.spawn(...baseTraits, traits.ReferenceFrame));
262
- const { scale, animationName } = geometryType.value;
284
+ if (metadata?.showAxesHelper)
285
+ baseTraits.push(traits.ShowAxesHelper);
286
+ const root = world.spawn(...baseTraits, traits.ReferenceFrame);
263
287
  let i = 1;
264
- for (const asset of geometryType.value.assets) {
288
+ for (const asset of assets) {
265
289
  const subEntityTraits = [
266
290
  traits.Name(`${referenceFrame} model ${i++}`),
267
291
  traits.Parent(referenceFrame),
292
+ relations.ChildOf(root),
268
293
  api,
269
294
  ];
270
295
  if (scale)
271
296
  subEntityTraits.push(traits.Scale(scale));
297
+ if (metadata?.invisible)
298
+ subEntityTraits.push(traits.Invisible);
299
+ if (metadata?.showAxesHelper)
300
+ subEntityTraits.push(traits.ShowAxesHelper);
272
301
  if (asset.content.case === 'url') {
273
302
  subEntityTraits.push(traits.GLTF({
274
303
  source: { url: asset.content.value },
@@ -281,9 +310,9 @@ const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject,
281
310
  animationName: animationName ?? DEFAULT_ANIMATION_NAME,
282
311
  }));
283
312
  }
284
- entities.push(world.spawn(...subEntityTraits));
313
+ world.spawn(...subEntityTraits);
285
314
  }
286
- return entities;
315
+ return { entity: root, relationships: metadata?.relationships };
287
316
  };
288
317
  const parsePointCloud = (world, entity, pointCloud, metadata) => {
289
318
  parsePcdInWorker(new Uint8Array(pointCloud)).then((pointcloud) => {
@@ -315,7 +344,7 @@ const parsePointCloud = (world, entity, pointCloud, metadata) => {
315
344
  entity.add(traits.Points);
316
345
  });
317
346
  };
318
- const updateColors = (entity, metadata) => {
347
+ const updatePointCloudColors = (entity, metadata) => {
319
348
  const buffer = entity.get(traits.BufferGeometry);
320
349
  if (!buffer) {
321
350
  if (metadata.colors)
@@ -423,7 +452,7 @@ const updateShape = (entity, { physicalObject, metadata }) => {
423
452
  }
424
453
  }
425
454
  };
426
- export const addColorTraits = (entity, colors) => {
455
+ const addColorTraits = (entity, colors) => {
427
456
  if (isVertexColors(colors)) {
428
457
  entity.add(traits.Colors(colors));
429
458
  }
@@ -431,7 +460,7 @@ export const addColorTraits = (entity, colors) => {
431
460
  entity.add(traits.Color(asRGB(colors, rgb)));
432
461
  }
433
462
  };
434
- export const setColorTraits = (entity, colors) => {
463
+ const setColorTraits = (entity, colors) => {
435
464
  if (isVertexColors(colors)) {
436
465
  if (entity.has(traits.Colors))
437
466
  entity.set(traits.Colors, colors);
@@ -1,5 +1,5 @@
1
1
  import { relation } from 'koota';
2
- export const ChildOf = relation({ exclusive: true });
2
+ export const ChildOf = relation({ exclusive: true, autoDestroy: 'orphan' });
3
3
  export const SubEntityLinkType = {
4
4
  HoverLink: 'HoverLink',
5
5
  };
@@ -4,6 +4,7 @@ import { type ConfigurableTrait, type Entity } from 'koota';
4
4
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
5
5
  export declare const Name: import("koota").Trait<() => string>;
6
6
  export declare const Parent: import("koota").Trait<() => string>;
7
+ export declare const UUID: import("koota").Trait<() => string>;
7
8
  export declare const Pose: import("koota").Trait<{
8
9
  x: number;
9
10
  y: number;
@@ -77,6 +78,7 @@ export declare const Color: import("koota").Trait<{
77
78
  */
78
79
  export declare const Material: import("koota").Trait<{
79
80
  depthTest: boolean;
81
+ depthWrite: boolean;
80
82
  }>;
81
83
  export declare const DepthTest: import("koota").Trait<() => boolean>;
82
84
  export declare const Arrow: import("koota").Trait<() => boolean>;
@@ -1,10 +1,14 @@
1
1
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
2
2
  import { trait } from 'koota';
3
3
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
4
+ import { createBufferGeometry, updateBufferGeometry } from '../attribute';
5
+ import { ColorFormat } from '../buf/draw/v1/metadata_pb';
4
6
  import { createBox, createCapsule, createSphere } from '../geometry';
7
+ import { parsePcdInWorker } from '../loaders/pcd';
5
8
  import { parsePlyInput } from '../ply';
6
9
  export const Name = trait(() => '');
7
10
  export const Parent = trait(() => 'world');
11
+ export const UUID = trait(() => '');
8
12
  export const Pose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
9
13
  export const EditedPose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
10
14
  export const Center = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
@@ -50,6 +54,7 @@ export const Color = trait({ r: 0, g: 0, b: 0 });
50
54
  */
51
55
  export const Material = trait({
52
56
  depthTest: false,
57
+ depthWrite: true,
53
58
  });
54
59
  export const DepthTest = trait(() => true);
55
60
  export const Arrow = trait(() => true);
@@ -203,4 +208,62 @@ export const updateGeometryTrait = (entity, geometry) => {
203
208
  entity.add(BufferGeometry(parsePlyInput(geometry.geometryType.value.mesh)));
204
209
  }
205
210
  }
211
+ else if (geometry.geometryType.case === 'pointcloud') {
212
+ updatePointCloud(entity, geometry.geometryType.value.pointCloud);
213
+ }
214
+ };
215
+ const updatePointCloud = (entity, pointCloud) => {
216
+ parsePcdInWorker(new Uint8Array(pointCloud))
217
+ .then((parsed) => {
218
+ if (!entity.isAlive())
219
+ return;
220
+ const buffer = entity.get(BufferGeometry);
221
+ let colors = parsed.colors;
222
+ if (buffer) {
223
+ // Reapply single color trait if the point count changed
224
+ if (parsed.colors === undefined) {
225
+ const color = entity.get(Color);
226
+ if (color) {
227
+ const newCount = parsed.positions.length / 3;
228
+ colors = new Uint8Array(newCount * 3);
229
+ const r = Math.round(color.r * 255);
230
+ const g = Math.round(color.g * 255);
231
+ const b = Math.round(color.b * 255);
232
+ for (let i = 0; i < newCount; i++) {
233
+ colors[i * 3] = r;
234
+ colors[i * 3 + 1] = g;
235
+ colors[i * 3 + 2] = b;
236
+ }
237
+ }
238
+ }
239
+ // When the point count changes, attributes must be reallocated.
240
+ const oldCount = buffer.getAttribute('position').count;
241
+ const newCount = parsed.positions.length / 3;
242
+ if (oldCount === newCount) {
243
+ updateBufferGeometry(buffer, parsed.positions, {
244
+ colors,
245
+ colorFormat: ColorFormat.RGB,
246
+ });
247
+ }
248
+ else {
249
+ const fresh = createBufferGeometry(parsed.positions, {
250
+ colors,
251
+ colorFormat: ColorFormat.RGB,
252
+ });
253
+ buffer.dispose();
254
+ entity.set(BufferGeometry, fresh);
255
+ }
256
+ return;
257
+ }
258
+ entity.remove(Box, Capsule, Sphere);
259
+ entity.add(BufferGeometry(createBufferGeometry(parsed.positions, {
260
+ colors: parsed.colors,
261
+ colorFormat: ColorFormat.RGB,
262
+ })));
263
+ if (!entity.has(Points))
264
+ entity.add(Points);
265
+ })
266
+ .catch((error) => {
267
+ console.error('Failed to update pointcloud buffer geometry:', error);
268
+ });
206
269
  };
@@ -6,6 +6,8 @@ declare const ConnectionStatus: {
6
6
  type ConnectionStatusType = (typeof ConnectionStatus)[keyof typeof ConnectionStatus];
7
7
  interface Context {
8
8
  connectionStatus: ConnectionStatusType;
9
+ createRelationship: (sourceUuid: string, targetUuid: string, type: string, indexMapping?: string) => Promise<void>;
10
+ deleteRelationship: (sourceUuid: string, targetUuid: string) => Promise<void>;
9
11
  }
10
12
  export declare function provideDrawService(): void;
11
13
  export declare function useDrawService(): Context;
@@ -5,13 +5,17 @@ import { useThrelte } from '@threlte/core';
5
5
  import {} from 'koota';
6
6
  import { getContext, setContext } from 'svelte';
7
7
  import { UuidTool } from 'uuid-tool';
8
+ import { writeBufferGeometryRange } from '../attribute';
8
9
  import { DrawService } from '../buf/draw/v1/service_connect';
9
- import { EntityChangeType, StreamEntityChangesResponse } from '../buf/draw/v1/service_pb';
10
- import { drawDrawing, drawTransform, updateDrawing, updateTransform, } from '../draw';
10
+ import { CreateRelationshipRequest, DeleteRelationshipRequest, EntityChangeType, StreamEntityChangesResponse, } from '../buf/draw/v1/service_pb';
11
+ import { asFloat32Array, inMeters, STRIDE } from '../buffer';
12
+ import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, uuidStringToBytes, } from '../draw';
11
13
  import { traits, useWorld } from '../ecs';
12
14
  import { useCameraControls } from './useControls.svelte';
13
15
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
16
+ import { useRelationships } from './useRelationships.svelte';
14
17
  const DRAW_SERVICE_KEY = Symbol('draw-service-context');
18
+ const FLOAT32_SIZE = 4;
15
19
  const ConnectionStatus = {
16
20
  CONNECTED: 'connected',
17
21
  DISCONNECTED: 'disconnected',
@@ -22,6 +26,7 @@ export function provideDrawService() {
22
26
  const world = useWorld();
23
27
  const cameraControls = useCameraControls();
24
28
  const drawConnectionConfig = useDrawConnectionConfig();
29
+ const relationships = useRelationships();
25
30
  let connectionStatus = $state(ConnectionStatus.DISCONNECTED);
26
31
  const url = $derived(drawConnectionConfig.current?.backendIP
27
32
  ? `http://${drawConnectionConfig.current.backendIP}:3030`
@@ -30,6 +35,9 @@ export function provideDrawService() {
30
35
  const drawingEntities = new Map();
31
36
  let pendingEvents = [];
32
37
  let flushScheduled = false;
38
+ let activeClient;
39
+ let activeSignal;
40
+ const activeChunkPulls = new Set();
33
41
  const destroyTransform = (uuidStr) => {
34
42
  const entity = transformEntities.get(uuidStr);
35
43
  if (!entity)
@@ -39,13 +47,11 @@ export function provideDrawService() {
39
47
  transformEntities.delete(uuidStr);
40
48
  };
41
49
  const destroyDrawing = (uuidStr) => {
42
- const entities = drawingEntities.get(uuidStr);
43
- if (!entities)
50
+ const entity = drawingEntities.get(uuidStr);
51
+ if (!entity)
44
52
  return;
45
- for (const entity of entities) {
46
- if (world.has(entity))
47
- entity.destroy();
48
- }
53
+ if (world.has(entity))
54
+ entity.destroy();
49
55
  drawingEntities.delete(uuidStr);
50
56
  };
51
57
  const processEvent = (event) => {
@@ -62,7 +68,9 @@ export function provideDrawService() {
62
68
  if (changeType === EntityChangeType.ADDED) {
63
69
  if (!transformEntities.has(uuid)) {
64
70
  const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
65
- transformEntities.set(uuid, spawned);
71
+ relationships.apply(spawned.entity, spawned.relationships);
72
+ transformEntities.set(uuid, spawned.entity);
73
+ relationships.flush(uuid);
66
74
  }
67
75
  }
68
76
  else if (changeType === EntityChangeType.REMOVED) {
@@ -71,11 +79,76 @@ export function provideDrawService() {
71
79
  else if (changeType === EntityChangeType.UPDATED) {
72
80
  const existing = transformEntities.get(uuid);
73
81
  if (existing) {
74
- updateTransform(existing, transform);
82
+ const updated = updateTransform(existing, transform);
83
+ relationships.apply(updated.entity, updated.relationships);
75
84
  }
76
85
  else {
77
86
  const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
78
- transformEntities.set(uuid, spawned);
87
+ relationships.apply(spawned.entity, spawned.relationships);
88
+ transformEntities.set(uuid, spawned.entity);
89
+ relationships.flush(uuid);
90
+ }
91
+ }
92
+ };
93
+ const isChunkedDrawing = (drawing) => {
94
+ return drawing.metadata?.chunks !== undefined && drawing.metadata.chunks.total > 0;
95
+ };
96
+ const getChunkInfo = (drawing) => {
97
+ const meta = drawing.metadata?.chunks;
98
+ if (!meta || meta.total === 0)
99
+ return undefined;
100
+ const shape = drawing.physicalObject?.geometryType;
101
+ if (shape?.case === 'points') {
102
+ const chunkElements = shape.value.positions.length / (STRIDE.POSITIONS * FLOAT32_SIZE);
103
+ return {
104
+ total: meta.total,
105
+ firstEnd: chunkElements,
106
+ };
107
+ }
108
+ return undefined;
109
+ };
110
+ const pullChunks = async (client, uuid, uuidBytes, entity, totalElements, firstChunkEnd, signal) => {
111
+ if (activeChunkPulls.has(uuid))
112
+ return;
113
+ activeChunkPulls.add(uuid);
114
+ try {
115
+ let nextStart = firstChunkEnd;
116
+ while (!signal.aborted) {
117
+ const response = await client.getEntityChunk({ uuid: uuidBytes, start: nextStart }, { signal });
118
+ // done with no payload is the server's "past end" sentinel (startByte >= posLen), not the final real chunk
119
+ if (response.done && !response.entity.value)
120
+ break;
121
+ const drawing = response.entity.case === 'drawing' ? response.entity.value : undefined;
122
+ if (!drawing)
123
+ break;
124
+ const shape = drawing.physicalObject?.geometryType;
125
+ if (shape?.case !== 'points')
126
+ break;
127
+ const buffer = entity.get(traits.BufferGeometry);
128
+ if (!buffer)
129
+ break;
130
+ const positions = asFloat32Array(shape.value.positions, inMeters);
131
+ const metadata = drawing.metadata;
132
+ if (!metadata)
133
+ break;
134
+ writeBufferGeometryRange(buffer, positions, response.start, metadata);
135
+ const chunkElements = positions.length / 3;
136
+ nextStart = response.start + chunkElements;
137
+ entity.set(traits.ChunkProgress, { loaded: nextStart, total: totalElements });
138
+ invalidate();
139
+ if (response.done)
140
+ break;
141
+ }
142
+ }
143
+ catch (error) {
144
+ if (!signal.aborted) {
145
+ console.error(`Chunk pull failed for entity ${uuid}:`, error);
146
+ }
147
+ }
148
+ finally {
149
+ activeChunkPulls.delete(uuid);
150
+ if (world.has(entity)) {
151
+ entity.remove(traits.ChunkProgress);
79
152
  }
80
153
  }
81
154
  };
@@ -83,7 +156,17 @@ export function provideDrawService() {
83
156
  if (changeType === EntityChangeType.ADDED) {
84
157
  if (!drawingEntities.has(uuid)) {
85
158
  const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
86
- drawingEntities.set(uuid, spawned);
159
+ relationships.apply(spawned.entity, spawned.relationships);
160
+ drawingEntities.set(uuid, spawned.entity);
161
+ relationships.flush(uuid);
162
+ if (isChunkedDrawing(drawing) && activeClient && activeSignal) {
163
+ const chunk = getChunkInfo(drawing);
164
+ if (chunk) {
165
+ spawned.entity.add(traits.ChunkProgress({ loaded: chunk.firstEnd, total: chunk.total }));
166
+ const uuidBytes = drawing.uuid ?? new Uint8Array();
167
+ void pullChunks(activeClient, uuid, uuidBytes, spawned.entity, chunk.total, chunk.firstEnd, activeSignal);
168
+ }
169
+ }
87
170
  }
88
171
  }
89
172
  else if (changeType === EntityChangeType.REMOVED) {
@@ -92,12 +175,18 @@ export function provideDrawService() {
92
175
  else if (changeType === EntityChangeType.UPDATED) {
93
176
  const existing = drawingEntities.get(uuid);
94
177
  if (existing) {
95
- const next = updateDrawing(world, existing, drawing, traits.DrawServiceAPI);
96
- drawingEntities.set(uuid, next);
178
+ const isModel = drawing.physicalObject?.geometryType?.case === 'model';
179
+ const result = isModel
180
+ ? updateModel(world, existing, drawing, traits.DrawServiceAPI)
181
+ : updateDrawing(world, existing, drawing);
182
+ relationships.apply(result.entity, result.relationships);
183
+ drawingEntities.set(uuid, result.entity);
97
184
  }
98
185
  else {
99
186
  const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
100
- drawingEntities.set(uuid, spawned);
187
+ relationships.apply(spawned.entity, spawned.relationships);
188
+ drawingEntities.set(uuid, spawned.entity);
189
+ relationships.flush(uuid);
101
190
  }
102
191
  }
103
192
  };
@@ -201,38 +290,68 @@ export function provideDrawService() {
201
290
  }
202
291
  }
203
292
  };
293
+ const createRelationship = async (sourceUuid, targetUuid, type, indexMapping) => {
294
+ if (!activeClient)
295
+ return;
296
+ const rel = {
297
+ targetUuid: uuidStringToBytes(targetUuid),
298
+ type,
299
+ };
300
+ if (indexMapping !== undefined) {
301
+ rel.indexMapping = indexMapping;
302
+ }
303
+ await activeClient.createRelationship(new CreateRelationshipRequest({
304
+ sourceUuid: uuidStringToBytes(sourceUuid),
305
+ relationship: rel,
306
+ }));
307
+ };
308
+ const deleteRelationship = async (sourceUuid, targetUuid) => {
309
+ if (!activeClient)
310
+ return;
311
+ await activeClient.deleteRelationship(new DeleteRelationshipRequest({
312
+ sourceUuid: uuidStringToBytes(sourceUuid),
313
+ targetUuid: uuidStringToBytes(targetUuid),
314
+ }));
315
+ };
204
316
  $effect(() => {
205
317
  if (!url) {
206
318
  connectionStatus = ConnectionStatus.DISCONNECTED;
319
+ activeClient = undefined;
207
320
  return;
208
321
  }
209
322
  const controller = new AbortController();
210
323
  connectionStatus = ConnectionStatus.CONNECTING;
211
324
  const transport = createConnectTransport({ baseUrl: url });
212
325
  const client = createClient(DrawService, transport);
326
+ activeClient = client;
327
+ activeSignal = controller.signal;
213
328
  void streamEntityChanges(client, controller.signal);
214
329
  void streamSceneChanges(client, controller.signal);
215
330
  return () => {
216
331
  controller.abort();
332
+ activeClient = undefined;
333
+ activeSignal = undefined;
217
334
  connectionStatus = ConnectionStatus.DISCONNECTED;
335
+ activeClient = undefined;
218
336
  for (const entity of transformEntities.values()) {
219
337
  if (world.has(entity))
220
338
  entity.destroy();
221
339
  }
222
340
  transformEntities.clear();
223
- for (const entities of drawingEntities.values()) {
224
- for (const entity of entities) {
225
- if (world.has(entity))
226
- entity.destroy();
227
- }
341
+ for (const entity of drawingEntities.values()) {
342
+ if (world.has(entity))
343
+ entity.destroy();
228
344
  }
229
345
  drawingEntities.clear();
346
+ relationships.clear();
230
347
  };
231
348
  });
232
349
  setContext(DRAW_SERVICE_KEY, {
233
350
  get connectionStatus() {
234
351
  return connectionStatus;
235
352
  },
353
+ createRelationship,
354
+ deleteRelationship,
236
355
  });
237
356
  }
238
357
  export function useDrawService() {