@viamrobotics/motion-tools 1.19.1 → 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 (32) 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/Points.svelte +23 -23
  10. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
  11. package/dist/components/FileDrop/FileDrop.svelte +8 -1
  12. package/dist/components/PCD.svelte +9 -1
  13. package/dist/components/PCD.svelte.d.ts +2 -0
  14. package/dist/components/SceneProviders.svelte +2 -0
  15. package/dist/components/Snapshot.svelte +12 -7
  16. package/dist/components/overlay/AddRelationship.svelte +25 -3
  17. package/dist/components/overlay/Details.svelte +293 -227
  18. package/dist/draw.d.ts +22 -9
  19. package/dist/draw.js +71 -41
  20. package/dist/ecs/relations.js +1 -1
  21. package/dist/ecs/traits.d.ts +2 -0
  22. package/dist/ecs/traits.js +2 -0
  23. package/dist/hooks/useDrawService.svelte.d.ts +2 -0
  24. package/dist/hooks/useDrawService.svelte.js +139 -20
  25. package/dist/hooks/useRelationships.svelte.d.ts +12 -0
  26. package/dist/hooks/useRelationships.svelte.js +78 -0
  27. package/dist/hooks/useWorldState.svelte.js +10 -4
  28. package/dist/metadata.d.ts +7 -3
  29. package/dist/metadata.js +26 -2
  30. package/dist/snapshot.d.ts +6 -1
  31. package/dist/snapshot.js +10 -5
  32. 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)
@@ -116,20 +140,10 @@ export const updateMetadata = (entity, metadata, { pointCloud = false } = {}) =>
116
140
  }
117
141
  entity.set(traits.Opacity, asOpacity(opacities, DEFAULT_OPACITY));
118
142
  };
119
- export const updateDrawing = (world, entities, drawing, api, options = { removable: true }) => {
120
- const { poseInObserverFrame, physicalObject, metadata } = drawing;
121
- if (physicalObject?.geometryType?.case === 'model') {
122
- for (const entity of entities) {
123
- if (world.has(entity))
124
- entity.destroy();
125
- }
126
- return drawDrawing(world, drawing, api, options);
127
- }
128
- if (entities.length === 0)
129
- return entities;
130
- const entity = entities[0];
143
+ export const updateDrawing = (world, entity, drawing, { removable = true } = {}) => {
144
+ const { poseInObserverFrame, metadata } = drawing;
131
145
  if (!world.has(entity))
132
- return entities;
146
+ return { entity, relationships: metadata?.relationships };
133
147
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
134
148
  traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
135
149
  if (metadata?.showAxesHelper)
@@ -140,8 +154,17 @@ export const updateDrawing = (world, entities, drawing, api, options = { removab
140
154
  entity.add(traits.Invisible);
141
155
  if (!metadata?.invisible)
142
156
  entity.remove(traits.Invisible);
157
+ if (removable)
158
+ entity.add(traits.Removable);
159
+ if (!removable)
160
+ entity.remove(traits.Removable);
143
161
  updateShape(entity, drawing);
144
- 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 });
145
168
  };
146
169
  const applyShape = (entity, { physicalObject, metadata }) => {
147
170
  const colors = metadata?.colors;
@@ -242,32 +265,39 @@ const applyShape = (entity, { physicalObject, metadata }) => {
242
265
  }
243
266
  }
244
267
  };
245
- const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata }, api, { removable = true }) => {
246
- const entities = [];
247
- const geometryType = physicalObject?.geometryType;
248
- if (geometryType?.case !== 'model')
249
- 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;
250
271
  const baseTraits = [
251
272
  traits.Name(referenceFrame),
252
273
  traits.Pose(createPose(poseInObserverFrame?.pose)),
253
274
  api,
254
275
  ...traits.getParentTrait(poseInObserverFrame?.referenceFrame),
255
276
  ];
277
+ const uuidStr = uuidBytesToString(uuid);
278
+ if (uuidStr)
279
+ baseTraits.push(traits.UUID(uuidStr));
256
280
  if (removable)
257
281
  baseTraits.push(traits.Removable);
258
282
  if (metadata?.invisible)
259
283
  baseTraits.push(traits.Invisible);
260
- entities.push(world.spawn(...baseTraits, traits.ReferenceFrame));
261
- const { scale, animationName } = geometryType.value;
284
+ if (metadata?.showAxesHelper)
285
+ baseTraits.push(traits.ShowAxesHelper);
286
+ const root = world.spawn(...baseTraits, traits.ReferenceFrame);
262
287
  let i = 1;
263
- for (const asset of geometryType.value.assets) {
288
+ for (const asset of assets) {
264
289
  const subEntityTraits = [
265
290
  traits.Name(`${referenceFrame} model ${i++}`),
266
291
  traits.Parent(referenceFrame),
292
+ relations.ChildOf(root),
267
293
  api,
268
294
  ];
269
295
  if (scale)
270
296
  subEntityTraits.push(traits.Scale(scale));
297
+ if (metadata?.invisible)
298
+ subEntityTraits.push(traits.Invisible);
299
+ if (metadata?.showAxesHelper)
300
+ subEntityTraits.push(traits.ShowAxesHelper);
271
301
  if (asset.content.case === 'url') {
272
302
  subEntityTraits.push(traits.GLTF({
273
303
  source: { url: asset.content.value },
@@ -280,9 +310,9 @@ const drawModel = (world, { referenceFrame, poseInObserverFrame, physicalObject,
280
310
  animationName: animationName ?? DEFAULT_ANIMATION_NAME,
281
311
  }));
282
312
  }
283
- entities.push(world.spawn(...subEntityTraits));
313
+ world.spawn(...subEntityTraits);
284
314
  }
285
- return entities;
315
+ return { entity: root, relationships: metadata?.relationships };
286
316
  };
287
317
  const parsePointCloud = (world, entity, pointCloud, metadata) => {
288
318
  parsePcdInWorker(new Uint8Array(pointCloud)).then((pointcloud) => {
@@ -422,7 +452,7 @@ const updateShape = (entity, { physicalObject, metadata }) => {
422
452
  }
423
453
  }
424
454
  };
425
- export const addColorTraits = (entity, colors) => {
455
+ const addColorTraits = (entity, colors) => {
426
456
  if (isVertexColors(colors)) {
427
457
  entity.add(traits.Colors(colors));
428
458
  }
@@ -430,7 +460,7 @@ export const addColorTraits = (entity, colors) => {
430
460
  entity.add(traits.Color(asRGB(colors, rgb)));
431
461
  }
432
462
  };
433
- export const setColorTraits = (entity, colors) => {
463
+ const setColorTraits = (entity, colors) => {
434
464
  if (isVertexColors(colors)) {
435
465
  if (entity.has(traits.Colors))
436
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>;
@@ -8,6 +8,7 @@ import { parsePcdInWorker } from '../loaders/pcd';
8
8
  import { parsePlyInput } from '../ply';
9
9
  export const Name = trait(() => '');
10
10
  export const Parent = trait(() => 'world');
11
+ export const UUID = trait(() => '');
11
12
  export const Pose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
12
13
  export const EditedPose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
13
14
  export const Center = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
@@ -53,6 +54,7 @@ export const Color = trait({ r: 0, g: 0, b: 0 });
53
54
  */
54
55
  export const Material = trait({
55
56
  depthTest: false,
57
+ depthWrite: true,
56
58
  });
57
59
  export const DepthTest = trait(() => true);
58
60
  export const Arrow = trait(() => true);
@@ -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() {
@@ -0,0 +1,12 @@
1
+ import type { Entity } from 'koota';
2
+ import type { Relationship } from '../metadata';
3
+ export declare const provideRelationships: () => {
4
+ apply(entity: Entity, relationships: Relationship[] | undefined): void;
5
+ flush(targetUuid: string): void;
6
+ clear(): void;
7
+ };
8
+ export declare const useRelationships: () => {
9
+ apply(entity: Entity, relationships: Relationship[] | undefined): void;
10
+ flush(targetUuid: string): void;
11
+ clear(): void;
12
+ };
@@ -0,0 +1,78 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { uuidBytesToString } from '../draw';
3
+ import { relations, traits, useQuery } from '../ecs';
4
+ const RELATIONSHIPS_CONTEXT_KEY = Symbol('relationships');
5
+ export const provideRelationships = () => {
6
+ const uuids = useQuery(traits.UUID);
7
+ const pending = new Map();
8
+ const addPending = (targetUuid, relationship) => {
9
+ const next = pending.get(targetUuid) ?? [];
10
+ next.push(relationship);
11
+ pending.set(targetUuid, next);
12
+ };
13
+ return setContext(RELATIONSHIPS_CONTEXT_KEY, {
14
+ apply(entity, relationships) {
15
+ const desired = relationships ?? [];
16
+ const currentTargets = entity.targetsFor(relations.SubEntityLink);
17
+ const desiredByUuid = new Map();
18
+ for (const rel of desired) {
19
+ const targetUUID = uuidBytesToString(rel.targetUuid);
20
+ if (!targetUUID)
21
+ continue;
22
+ desiredByUuid.set(targetUUID, rel);
23
+ }
24
+ for (const target of currentTargets) {
25
+ if (!target.isAlive())
26
+ continue;
27
+ const targetUuid = target.get(traits.UUID);
28
+ if (!targetUuid || !desiredByUuid.has(targetUuid)) {
29
+ entity.remove(relations.SubEntityLink(target));
30
+ }
31
+ }
32
+ for (const [uuid, relationship] of desiredByUuid) {
33
+ const targetEntity = uuids.current.find((e) => e.get(traits.UUID) === uuid);
34
+ if (!targetEntity) {
35
+ addPending(uuid, {
36
+ entity: entity,
37
+ type: relationship.type,
38
+ indexMapping: relationship.indexMapping,
39
+ });
40
+ continue;
41
+ }
42
+ const existing = entity.get(relations.SubEntityLink(targetEntity));
43
+ if (existing &&
44
+ existing.type === relationship.type &&
45
+ existing.indexMapping === (relationship.indexMapping ?? 'index')) {
46
+ continue;
47
+ }
48
+ entity.add(relations.SubEntityLink(targetEntity, {
49
+ type: relationship.type,
50
+ indexMapping: relationship.indexMapping ?? 'index',
51
+ }));
52
+ }
53
+ },
54
+ flush(targetUuid) {
55
+ const relationship = pending.get(targetUuid);
56
+ if (!relationship)
57
+ return;
58
+ pending.delete(targetUuid);
59
+ const targetEntity = uuids.current.find((e) => e.get(traits.UUID) === targetUuid);
60
+ if (!targetEntity)
61
+ return;
62
+ for (const { entity, type, indexMapping } of relationship) {
63
+ if (!entity.isAlive())
64
+ continue;
65
+ entity.add(relations.SubEntityLink(targetEntity, {
66
+ type,
67
+ indexMapping: indexMapping ?? 'index',
68
+ }));
69
+ }
70
+ },
71
+ clear() {
72
+ pending.clear();
73
+ },
74
+ });
75
+ };
76
+ export const useRelationships = () => {
77
+ return getContext(RELATIONSHIPS_CONTEXT_KEY);
78
+ };