@viamrobotics/motion-tools 1.26.0 → 1.26.2

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 (52) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +42 -29
  2. package/dist/buf/common/v1/common_pb.d.ts +19 -0
  3. package/dist/buf/common/v1/common_pb.js +32 -0
  4. package/dist/components/App.svelte +4 -1
  5. package/dist/components/BatchedArrows.svelte +31 -15
  6. package/dist/components/Entities/Entities.svelte +3 -8
  7. package/dist/components/Entities/Frame.svelte +25 -9
  8. package/dist/components/Entities/Frame.svelte.d.ts +0 -2
  9. package/dist/components/Entities/GLTF.svelte +5 -4
  10. package/dist/components/Entities/Line.svelte +5 -4
  11. package/dist/components/Entities/Mesh.svelte +12 -18
  12. package/dist/components/Entities/Points.svelte +5 -4
  13. package/dist/components/Entities/Pose.svelte +17 -24
  14. package/dist/components/Entities/Pose.svelte.d.ts +1 -4
  15. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
  16. package/dist/components/SceneProviders.svelte +2 -1
  17. package/dist/components/SelectedTransformControls.svelte +57 -34
  18. package/dist/components/StaticGeometries.svelte +1 -1
  19. package/dist/components/hover/HoveredEntity.svelte +33 -3
  20. package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
  21. package/dist/components/overlay/Details.svelte +72 -94
  22. package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
  23. package/dist/components/overlay/left-pane/Tree.svelte +9 -9
  24. package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
  25. package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
  26. package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
  27. package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
  28. package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
  29. package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
  30. package/dist/draw.js +21 -7
  31. package/dist/ecs/index.d.ts +1 -0
  32. package/dist/ecs/index.js +1 -0
  33. package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
  34. package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
  35. package/dist/ecs/traits.d.ts +41 -45
  36. package/dist/ecs/traits.js +57 -28
  37. package/dist/ecs/useTrait.svelte.d.ts +1 -6
  38. package/dist/ecs/useTrait.svelte.js +21 -13
  39. package/dist/ecs/worldMatrix.d.ts +10 -0
  40. package/dist/ecs/worldMatrix.js +148 -0
  41. package/dist/editing/FrameEditSession.js +31 -18
  42. package/dist/hooks/use3DModels.svelte.js +1 -1
  43. package/dist/hooks/useConfigFrames.svelte.js +12 -0
  44. package/dist/hooks/useDrawAPI.svelte.js +14 -6
  45. package/dist/hooks/useFrames.svelte.js +23 -11
  46. package/dist/hooks/useGeometries.svelte.js +4 -2
  47. package/dist/hooks/usePartConfig.svelte.js +38 -3
  48. package/dist/hooks/useWorldState.svelte.js +10 -2
  49. package/dist/transform.js +55 -21
  50. package/package.json +4 -4
  51. package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
  52. package/dist/components/overlay/left-pane/buildTree.js +0 -48
@@ -3,22 +3,22 @@
3
3
  lang="ts"
4
4
  >
5
5
  import { ThemeUtils } from 'svelte-tweakpane-ui'
6
- import { BufferAttribute, Euler, MathUtils, Quaternion, Vector3 } from 'three'
6
+ import { BufferAttribute, Euler, MathUtils, Quaternion } from 'three'
7
7
 
8
8
  import { OrientationVector } from '../../three/OrientationVector'
9
9
 
10
- const vec3 = new Vector3()
11
10
  const quaternion = new Quaternion()
12
11
  const ov = new OrientationVector()
13
12
  const euler = new Euler()
14
13
  </script>
15
14
 
16
15
  <script lang="ts">
16
+ import type { Pose } from '@viamrobotics/sdk'
17
17
  import type { Entity } from 'koota'
18
18
  import type { Snippet } from 'svelte'
19
19
 
20
20
  import { draggable } from '@neodrag/svelte'
21
- import { isInstanceOf, useTask, useThrelte } from '@threlte/core'
21
+ import { isInstanceOf, useThrelte } from '@threlte/core'
22
22
  import { Button, Icon, Tooltip } from '@viamrobotics/prime-core'
23
23
  import { Check, Copy } from 'lucide-svelte'
24
24
  import {
@@ -53,7 +53,7 @@
53
53
  useSelectedEntity,
54
54
  useSelectedObject3d,
55
55
  } from '../../hooks/useSelection.svelte'
56
- import { createPose } from '../../transform'
56
+ import { createPose, matrixToPose } from '../../transform'
57
57
 
58
58
  interface Props {
59
59
  details?: Snippet<[{ entity: Entity }]>
@@ -73,14 +73,17 @@
73
73
  const environment = useEnvironment()
74
74
  const focusedEntity = useFocusedEntity()
75
75
  const focusedObject3d = useFocusedObject3d()
76
+ const linkedEntities = useLinkedEntities()
77
+
76
78
  const entity = $derived(focusedEntity.current ?? selectedEntity.current)
77
79
  const object3d = $derived(focusedObject3d.current ?? selectedObject3d.current)
78
- const worldPosition = $state({ x: 0, y: 0, z: 0 })
79
- const worldOrientation = $state({ x: 0, y: 0, z: 1, th: 0 })
80
- const linkedEntities = useLinkedEntities()
80
+
81
81
  const name = useTrait(() => entity, traits.Name)
82
82
  const parent = useParentName(() => entity)
83
- const localPose = useTrait(() => entity, traits.EditedPose)
83
+ const matrix = useTrait(() => entity, traits.Matrix)
84
+ const editedMatrix = useTrait(() => entity, traits.EditedMatrix)
85
+ const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
86
+ const center = useTrait(() => entity, traits.Center)
84
87
  const box = useTrait(() => entity, traits.Box)
85
88
  const sphere = useTrait(() => entity, traits.Sphere)
86
89
  const capsule = useTrait(() => entity, traits.Capsule)
@@ -88,14 +91,27 @@
88
91
  const points = useTrait(() => entity, traits.Points)
89
92
  const arrows = useTrait(() => entity, traits.Arrows)
90
93
  const opacity = useTrait(() => entity, traits.Opacity)
91
-
92
94
  const framesAPI = useTrait(() => entity, traits.FramesAPI)
93
- const isFrameNode = $derived(!!framesAPI.current)
95
+ const geometriesAPI = useTrait(() => entity, traits.GeometriesAPI)
96
+
97
+ const localPose = $derived.by<Pose | undefined>(() => {
98
+ const source = editedMatrix.current ?? matrix.current
99
+ if (source) return matrixToPose(source, createPose())
100
+ if (center.current) return createPose(center.current)
101
+ return undefined
102
+ })
103
+ const worldPose = $derived.by<Pose | undefined>(() => {
104
+ if (!worldMatrix.current) return
105
+
106
+ return matrixToPose(worldMatrix.current, createPose())
107
+ })
94
108
 
109
+ const isFrameNode = $derived(!!framesAPI.current)
110
+ const isGeometry = $derived(!!geometriesAPI.current)
95
111
  const showEditFrameOptions = $derived(isFrameNode && partConfig.hasEditPermissions)
96
112
  const showRelationshipOptions = $derived(points.current || arrows.current)
97
-
98
113
  const resourceName = $derived(name.current ? resourceByName.current[name.current] : undefined)
114
+ const displayType = $derived(isFrameNode ? resourceName?.subtype : isGeometry ? 'geometry' : '')
99
115
 
100
116
  let geometryType = $derived.by<'box' | 'sphere' | 'capsule' | 'none'>(() => {
101
117
  if (box.current) return 'box'
@@ -120,13 +136,8 @@
120
136
  let dragElement = $state.raw<HTMLElement>()
121
137
 
122
138
  const eulerValue = $derived.by<RotationEulerValueObject>(() => {
123
- if (!localPose.current) return { x: 0, y: 0, z: 0 }
124
- ov.set(
125
- localPose.current.oX,
126
- localPose.current.oY,
127
- localPose.current.oZ,
128
- MathUtils.degToRad(localPose.current.theta)
129
- )
139
+ if (!localPose) return { x: 0, y: 0, z: 0 }
140
+ ov.set(localPose.oX, localPose.oY, localPose.oZ, MathUtils.degToRad(localPose.theta))
130
141
  ov.toEuler(euler)
131
142
  return {
132
143
  x: MathUtils.radToDeg(euler.x),
@@ -236,65 +247,28 @@
236
247
  }
237
248
  }
238
249
 
239
- useTask(
240
- () => {
241
- object3d?.getWorldPosition(vec3)
242
- if (!vec3.equals(worldPosition)) {
243
- worldPosition.x = vec3.x
244
- worldPosition.y = vec3.y
245
- worldPosition.z = vec3.z
246
- }
247
-
248
- object3d?.getWorldQuaternion(quaternion)
249
- ov.setFromQuaternion(quaternion)
250
-
251
- if (!ov.equals(worldOrientation)) {
252
- worldOrientation.x = ov.x
253
- worldOrientation.y = ov.y
254
- worldOrientation.z = ov.z
255
- worldOrientation.th = ov.th
256
- }
257
- },
258
- {
259
- autoInvalidate: false,
260
- running: () => object3d !== undefined,
261
- }
262
- )
263
-
264
- $effect(() => {
265
- if (entity) {
266
- const worldPose = createPose({
267
- x: worldPosition.x,
268
- y: worldPosition.y,
269
- z: worldPosition.z,
270
- oX: worldOrientation.x,
271
- oY: worldOrientation.y,
272
- oZ: worldOrientation.z,
273
- theta: MathUtils.radToDeg(worldOrientation.th),
274
- })
275
- if (entity.has(traits.WorldPose)) {
276
- entity.set(traits.WorldPose, worldPose)
277
- } else {
278
- entity.add(traits.WorldPose(worldPose))
279
- }
280
- }
281
- })
282
-
283
250
  const getCopyClipboardText = () => {
284
251
  return JSON.stringify(
285
252
  {
286
- worldPosition: worldPosition,
287
- worldOrientation: worldOrientation,
253
+ worldPosition: worldPose ? { x: worldPose.x, y: worldPose.y, z: worldPose.z } : null,
254
+ worldOrientation: worldPose
255
+ ? {
256
+ x: worldPose.oX,
257
+ y: worldPose.oY,
258
+ z: worldPose.oZ,
259
+ th: MathUtils.degToRad(worldPose.theta),
260
+ }
261
+ : null,
288
262
  localPosition: {
289
- x: localPose.current?.x,
290
- y: localPose.current?.y,
291
- z: localPose.current?.z,
263
+ x: localPose?.x,
264
+ y: localPose?.y,
265
+ z: localPose?.z,
292
266
  },
293
267
  localOrientation: {
294
- x: localPose.current?.oX,
295
- y: localPose.current?.oY,
296
- z: localPose.current?.oZ,
297
- th: localPose.current?.theta,
268
+ x: localPose?.oX,
269
+ y: localPose?.oY,
270
+ z: localPose?.oZ,
271
+ th: localPose?.theta,
298
272
  },
299
273
  geometry: {
300
274
  type: geometryType,
@@ -350,7 +324,7 @@
350
324
  >
351
325
  <div class="flex w-[90%] items-center gap-1">
352
326
  <strong class="overflow-hidden text-nowrap text-ellipsis">{name.current}</strong>
353
- <span class="text-subtle-2">{resourceName?.subtype}</span>
327
+ <span class="text-subtle-2">{displayType}</span>
354
328
  </div>
355
329
 
356
330
  {#if object3d}
@@ -438,15 +412,15 @@
438
412
  <div class="flex gap-3">
439
413
  <div>
440
414
  <span class="text-subtle-2">x</span>
441
- {(worldPosition.x * 1000).toFixed(2)}
415
+ {(worldPose?.x ?? 0).toFixed(2)}
442
416
  </div>
443
417
  <div>
444
418
  <span class="text-subtle-2">y</span>
445
- {(worldPosition.y * 1000).toFixed(2)}
419
+ {(worldPose?.y ?? 0).toFixed(2)}
446
420
  </div>
447
421
  <div>
448
422
  <span class="text-subtle-2">z</span>
449
- {(worldPosition.z * 1000).toFixed(2)}
423
+ {(worldPose?.z ?? 0).toFixed(2)}
450
424
  </div>
451
425
  </div>
452
426
  </div>
@@ -457,19 +431,19 @@
457
431
  <div class="flex gap-3">
458
432
  <div>
459
433
  <span class="text-subtle-2">x</span>
460
- {worldOrientation.x.toFixed(2)}
434
+ {(worldPose?.oX ?? 0).toFixed(2)}
461
435
  </div>
462
436
  <div>
463
437
  <span class="text-subtle-2">y</span>
464
- {worldOrientation.y.toFixed(2)}
438
+ {(worldPose?.oY ?? 0).toFixed(2)}
465
439
  </div>
466
440
  <div>
467
441
  <span class="text-subtle-2">z</span>
468
- {worldOrientation.z.toFixed(2)}
442
+ {(worldPose?.oZ ?? 0).toFixed(2)}
469
443
  </div>
470
444
  <div>
471
445
  <span class="text-subtle-2">th</span>
472
- {MathUtils.radToDeg(worldOrientation.th).toFixed(2)}
446
+ {(worldPose?.theta ?? 0).toFixed(2)}
473
447
  </div>
474
448
  </div>
475
449
  </div>
@@ -505,7 +479,7 @@
505
479
  {/if}
506
480
  </div>
507
481
 
508
- {#if localPose.current}
482
+ {#if localPose}
509
483
  <div>
510
484
  <strong class="font-semibold">local position</strong>
511
485
  <span class="text-subtle-2">(mm)</span>
@@ -514,9 +488,9 @@
514
488
  <div aria-label="mutable local position">
515
489
  <Point
516
490
  value={{
517
- x: localPose.current.x,
518
- y: localPose.current.y,
519
- z: localPose.current.z,
491
+ x: localPose.x,
492
+ y: localPose.y,
493
+ z: localPose.z,
520
494
  }}
521
495
  on:change={handlePositionChange}
522
496
  />
@@ -526,17 +500,17 @@
526
500
  {@render ImmutableField({
527
501
  label: 'x',
528
502
  ariaLabel: 'local position x coordinate',
529
- value: localPose.current.x,
503
+ value: localPose.x,
530
504
  })}
531
505
  {@render ImmutableField({
532
506
  label: 'y',
533
507
  ariaLabel: 'local position y coordinate',
534
- value: localPose.current.y,
508
+ value: localPose.y,
535
509
  })}
536
510
  {@render ImmutableField({
537
511
  label: 'z',
538
512
  ariaLabel: 'local position z coordinate',
539
- value: localPose.current.z,
513
+ value: localPose.z,
540
514
  })}
541
515
  </div>
542
516
  {/if}
@@ -551,10 +525,10 @@
551
525
  <TabPage title="OV (deg)">
552
526
  <Point
553
527
  value={{
554
- x: localPose.current.oX,
555
- y: localPose.current.oY,
556
- z: localPose.current.oZ,
557
- w: localPose.current.theta,
528
+ x: localPose.oX,
529
+ y: localPose.oY,
530
+ z: localPose.oZ,
531
+ w: localPose.theta,
558
532
  }}
559
533
  on:change={handleOrientationOVChange}
560
534
  />
@@ -573,22 +547,22 @@
573
547
  {@render ImmutableField({
574
548
  label: 'x',
575
549
  ariaLabel: 'local orientation x coordinate',
576
- value: localPose.current.oX,
550
+ value: localPose.oX,
577
551
  })}
578
552
  {@render ImmutableField({
579
553
  label: 'y',
580
554
  ariaLabel: 'local orientation y coordinate',
581
- value: localPose.current.oY,
555
+ value: localPose.oY,
582
556
  })}
583
557
  {@render ImmutableField({
584
558
  label: 'z',
585
559
  ariaLabel: 'local orientation z coordinate',
586
- value: localPose.current.oZ,
560
+ value: localPose.oZ,
587
561
  })}
588
562
  {@render ImmutableField({
589
563
  label: 'th',
590
564
  ariaLabel: 'local orientation theta degrees',
591
- value: localPose.current.theta,
565
+ value: localPose.theta,
592
566
  })}
593
567
  </div>
594
568
  {/if}
@@ -611,6 +585,7 @@
611
585
  y: box.current.y,
612
586
  z: box.current.z,
613
587
  }}
588
+ min={0}
614
589
  on:change={handleBoxChange}
615
590
  />
616
591
  </div>
@@ -622,6 +597,7 @@
622
597
  <Slider
623
598
  label="r"
624
599
  value={sphere.current.r}
600
+ min={0}
625
601
  on:change={handleSphereRChange}
626
602
  />
627
603
  </div>
@@ -633,11 +609,13 @@
633
609
  <Slider
634
610
  label="r"
635
611
  value={capsule.current.r}
612
+ min={0}
636
613
  on:change={handleCapsuleRChange}
637
614
  />
638
615
  <Slider
639
616
  label="l"
640
617
  value={capsule.current.l}
618
+ min={0}
641
619
  on:change={handleCapsuleLChange}
642
620
  />
643
621
  </div>
@@ -1,20 +1,17 @@
1
+ import { Matrix4 } from 'three';
1
2
  import { hierarchy, traits } from '../../../../ecs';
3
+ import { createPose, poseToMatrix } from '../../../../transform';
4
+ // OV must be a unit vector — (0.6, 0.8, 0) magnitude 1 — so the matrix
5
+ // round-trip in Details.svelte returns the same components.
6
+ const buildMatrix = () => poseToMatrix(createPose({
7
+ x: 10,
8
+ y: 20,
9
+ z: 30,
10
+ oX: 0.6,
11
+ oY: 0.8,
12
+ oZ: 0,
13
+ theta: 0.4,
14
+ }), new Matrix4());
2
15
  export const createEntityFixture = (world) => {
3
- return world.spawn(...hierarchy.parentTraits('parent_frame'), traits.Name('Test Object'), traits.Pose({
4
- x: 10,
5
- y: 20,
6
- z: 30,
7
- oX: 0.1,
8
- oY: 0.2,
9
- oZ: 0.3,
10
- theta: 0.4,
11
- }), traits.EditedPose({
12
- x: 10,
13
- y: 20,
14
- z: 30,
15
- oX: 0.1,
16
- oY: 0.2,
17
- oZ: 0.3,
18
- theta: 0.4,
19
- }), traits.Box({ x: 0.01, y: 0.02, z: 0.03 }));
16
+ return world.spawn(...hierarchy.parentTraits('parent_frame'), traits.Name('Test Object'), traits.Matrix(buildMatrix()), traits.EditedMatrix(buildMatrix()), traits.Box({ x: 0.01, y: 0.02, z: 0.03 }));
20
17
  };
@@ -1,13 +1,15 @@
1
1
  <script lang="ts">
2
+ import type { Entity } from 'koota'
3
+
2
4
  import { normalizeProps, useMachine } from '@zag-js/svelte'
3
5
  import * as tree from '@zag-js/tree-view'
4
6
  import { VirtualList } from 'svelte-virtuallists'
5
7
  import { SvelteSet } from 'svelte/reactivity'
6
8
 
7
- import { traits } from '../../../ecs'
9
+ import { relations, traits } from '../../../ecs'
8
10
  import { useSelectedEntity } from '../../../hooks/useSelection.svelte'
9
11
 
10
- import type { TreeNode as TreeNodeType } from './buildTree'
12
+ import type { TreeNode as TreeNodeType } from './useTree.svelte'
11
13
 
12
14
  import TreeNode from './TreeNode.svelte'
13
15
 
@@ -15,12 +17,11 @@
15
17
 
16
18
  interface Props {
17
19
  rootNode: TreeNodeType
18
- nodeMap: Record<string, TreeNodeType | undefined>
19
20
  dragElement?: HTMLElement
20
21
  onSelectionChange?: (event: tree.SelectionChangeDetails) => void
21
22
  }
22
23
 
23
- let { rootNode, nodeMap, onSelectionChange, dragElement = $bindable() }: Props = $props()
24
+ let { rootNode, onSelectionChange, dragElement = $bindable() }: Props = $props()
24
25
 
25
26
  const collection = $derived(
26
27
  tree.collection<TreeNodeType>({
@@ -34,11 +35,10 @@
34
35
  const expandedValues = new SvelteSet<string>()
35
36
 
36
37
  $effect(() => {
37
- let name = selected.current?.get(traits.Name)
38
- let node = nodeMap[name ?? '']
39
- while (node) {
40
- expandedValues.add(`${node.entity}`)
41
- node = node.parent
38
+ let entity: Entity | undefined = selected.current
39
+ while (entity) {
40
+ expandedValues.add(`${entity}`)
41
+ entity = entity.targetFor(relations.ChildOf)
42
42
  }
43
43
  })
44
44
 
@@ -1,8 +1,7 @@
1
1
  import * as tree from '@zag-js/tree-view';
2
- import type { TreeNode as TreeNodeType } from './buildTree';
2
+ import type { TreeNode as TreeNodeType } from './useTree.svelte';
3
3
  interface Props {
4
4
  rootNode: TreeNodeType;
5
- nodeMap: Record<string, TreeNodeType | undefined>;
6
5
  dragElement?: HTMLElement;
7
6
  onSelectionChange?: (event: tree.SelectionChangeDetails) => void;
8
7
  }
@@ -1,36 +1,26 @@
1
1
  <script lang="ts">
2
2
  import { type Entity, IsExcluded } from 'koota'
3
3
 
4
- import { traits, useQuery, useWorld } from '../../../ecs'
5
- import { useFrames } from '../../../hooks/useFrames.svelte'
4
+ import { traits, useWorld } from '../../../ecs'
6
5
  import { useSelectedEntity } from '../../../hooks/useSelection.svelte'
7
6
 
8
7
  import FloatingPanel from '../FloatingPanel.svelte'
9
- import { buildTreeNodes, type TreeNode } from './buildTree'
10
8
  import Tree from './Tree.svelte'
11
9
  import { provideTreeExpandedContext } from './useExpanded.svelte'
10
+ import { type TreeNode, useTree } from './useTree.svelte'
12
11
 
13
12
  provideTreeExpandedContext()
14
13
 
15
14
  const selectedEntity = useSelectedEntity()
16
-
17
- const frames = useFrames()
18
15
  const world = useWorld()
19
16
 
20
17
  const worldEntity = world.spawn(IsExcluded, traits.Name('World'))
21
18
 
22
- const allEntities = useQuery(traits.Name)
23
-
24
- const { rootNodes, nodeMap } = $derived.by(() => {
25
- // This ensures the tree rebuilds when frame parent relationships change
26
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
27
- frames.current
28
- return buildTreeNodes(allEntities.current)
29
- })
19
+ const tree = useTree()
30
20
 
31
21
  const rootNode = $derived<TreeNode>({
32
22
  entity: worldEntity,
33
- children: rootNodes,
23
+ children: tree.current,
34
24
  })
35
25
  </script>
36
26
 
@@ -44,7 +34,6 @@
44
34
  >
45
35
  <Tree
46
36
  {rootNode}
47
- {nodeMap}
48
37
  onSelectionChange={(event) => {
49
38
  const value = event.selectedValue[0]
50
39
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { traits, useTrait } from '../../../ecs'
8
8
 
9
- import type { TreeNode } from './buildTree'
9
+ import type { TreeNode } from './useTree.svelte'
10
10
 
11
11
  import Self from './TreeNode.svelte'
12
12
 
@@ -1,5 +1,5 @@
1
1
  import type { Api } from '@zag-js/tree-view';
2
- import type { TreeNode } from './buildTree';
2
+ import type { TreeNode } from './useTree.svelte';
3
3
  interface Props {
4
4
  node: TreeNode;
5
5
  indexPath: number[];
@@ -0,0 +1,14 @@
1
+ import { type Entity } from 'koota';
2
+ export interface TreeNode {
3
+ entity: Entity;
4
+ children?: TreeNode[];
5
+ }
6
+ /**
7
+ * Reactive top-down tree built from `ChildOf` relations. Rebuilds when any
8
+ * named entity is added, removed, renamed, or gains/loses a `ChildOf` or
9
+ * `Orphan` edge. Orphans are hidden from the tree — they reappear once
10
+ * `provideHierarchy` resolves them to a real `ChildOf` parent.
11
+ */
12
+ export declare const useTree: () => {
13
+ readonly current: TreeNode[];
14
+ };
@@ -0,0 +1,63 @@
1
+ import { Not } from 'koota';
2
+ import { createSubscriber } from 'svelte/reactivity';
3
+ import { relations, traits, useWorld } from '../../../ecs';
4
+ const compareByName = (a, b) => (a.get(traits.Name) ?? '').localeCompare(b.get(traits.Name) ?? '');
5
+ const buildTree = (world) => {
6
+ const walk = (entity) => {
7
+ const node = { entity };
8
+ const children = world.query(relations.ChildOf(entity)).toSorted(compareByName);
9
+ if (children.length > 0) {
10
+ node.children = children.map((child) => walk(child));
11
+ }
12
+ return node;
13
+ };
14
+ const rootEntities = [];
15
+ for (const entity of world.query(traits.Name, Not(traits.Orphan))) {
16
+ if (entity.targetFor(relations.ChildOf))
17
+ continue;
18
+ rootEntities.push(entity);
19
+ }
20
+ rootEntities.sort(compareByName);
21
+ return rootEntities.map((entity) => walk(entity));
22
+ };
23
+ /**
24
+ * Reactive top-down tree built from `ChildOf` relations. Rebuilds when any
25
+ * named entity is added, removed, renamed, or gains/loses a `ChildOf` or
26
+ * `Orphan` edge. Orphans are hidden from the tree — they reappear once
27
+ * `provideHierarchy` resolves them to a real `ChildOf` parent.
28
+ */
29
+ export const useTree = () => {
30
+ const world = useWorld();
31
+ let cached;
32
+ let dirty = true;
33
+ const subscribe = createSubscriber((update) => {
34
+ const invalidate = () => {
35
+ dirty = true;
36
+ update();
37
+ };
38
+ const unsubs = [
39
+ world.onAdd(traits.Name, invalidate),
40
+ world.onRemove(traits.Name, invalidate),
41
+ world.onChange(traits.Name, invalidate),
42
+ world.onAdd(relations.ChildOf, invalidate),
43
+ world.onChange(relations.ChildOf, invalidate),
44
+ world.onRemove(relations.ChildOf, invalidate),
45
+ world.onAdd(traits.Orphan, invalidate),
46
+ world.onRemove(traits.Orphan, invalidate),
47
+ ];
48
+ return () => {
49
+ for (const unsub of unsubs)
50
+ unsub();
51
+ };
52
+ });
53
+ return {
54
+ get current() {
55
+ subscribe();
56
+ if (dirty || !cached) {
57
+ cached = buildTree(world);
58
+ dirty = false;
59
+ }
60
+ return cached;
61
+ },
62
+ };
63
+ };
package/dist/draw.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Vector3, Vector4 } from 'three';
1
+ import { Matrix4, Vector3, Vector4 } from 'three';
2
2
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
3
3
  import { UuidTool } from 'uuid-tool';
4
4
  import { createBufferGeometry, preAllocateBufferGeometry, updateBufferGeometry, writeBufferGeometryRange, } from './attribute';
@@ -6,7 +6,7 @@ import { asFloat32Array, asOpacity, asRGB, inMeters, isSingleColor, isVertexColo
6
6
  import { hierarchy, relations, traits } from './ecs';
7
7
  import { parsePcdInWorker } from './loaders/pcd';
8
8
  import { metadataFromStruct } from './metadata';
9
- import { createPose } from './transform';
9
+ import { createPose, poseToMatrix } from './transform';
10
10
  import { ColorFormat } from './buf/draw/v1/metadata_pb';
11
11
  import { isPointCloud } from './geometry';
12
12
  const vec3 = new Vector3();
@@ -38,7 +38,7 @@ const isModel = (drawing) => {
38
38
  export const drawTransform = (world, { referenceFrame, poseInObserverFrame, physicalObject, metadata, uuid }, api, { removable = true } = {}) => {
39
39
  const entityTraits = [
40
40
  traits.Name(referenceFrame),
41
- traits.Pose(createPose(poseInObserverFrame?.pose)),
41
+ traits.Matrix(poseToMatrix(createPose(poseInObserverFrame?.pose), new Matrix4())),
42
42
  api,
43
43
  ];
44
44
  const uuidStr = uuidBytesToString(uuid);
@@ -88,7 +88,7 @@ export const drawDrawing = (world, drawing, api, { removable = true } = {}) => {
88
88
  const uuidStr = uuidBytesToString(uuid);
89
89
  if (uuidStr)
90
90
  uuidTraits.push(traits.UUID(uuidStr));
91
- const entity = world.spawn(traits.Name(referenceFrame), traits.Pose(createPose(poseInObserverFrame?.pose)), api, ...hierarchy.parentTraits(poseInObserverFrame?.referenceFrame), ...uuidTraits);
91
+ const entity = world.spawn(traits.Name(referenceFrame), traits.Matrix(poseToMatrix(createPose(poseInObserverFrame?.pose), new Matrix4())), api, ...hierarchy.parentTraits(poseInObserverFrame?.referenceFrame), ...uuidTraits);
92
92
  if (removable)
93
93
  entity.add(traits.Removable);
94
94
  if (metadata?.showAxesHelper)
@@ -99,7 +99,14 @@ export const drawDrawing = (world, drawing, api, { removable = true } = {}) => {
99
99
  return { entity, relationships: metadata?.relationships };
100
100
  };
101
101
  export const updateTransform = (entity, { poseInObserverFrame, physicalObject, metadata }, { removable = true } = {}) => {
102
- entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
102
+ const matrix = entity.get(traits.Matrix);
103
+ if (matrix) {
104
+ poseToMatrix(createPose(poseInObserverFrame?.pose), matrix);
105
+ entity.changed(traits.Matrix);
106
+ }
107
+ else {
108
+ entity.add(traits.Matrix(poseToMatrix(createPose(poseInObserverFrame?.pose), new Matrix4())));
109
+ }
103
110
  hierarchy.setParent(entity, poseInObserverFrame?.referenceFrame);
104
111
  if (physicalObject) {
105
112
  traits.updateGeometryTrait(entity, physicalObject);
@@ -144,7 +151,14 @@ export const updateDrawing = (world, entity, drawing, { removable = true } = {})
144
151
  const { poseInObserverFrame, metadata } = drawing;
145
152
  if (!world.has(entity))
146
153
  return { entity, relationships: metadata?.relationships };
147
- entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
154
+ const matrix = entity.get(traits.Matrix);
155
+ if (matrix) {
156
+ poseToMatrix(createPose(poseInObserverFrame?.pose), matrix);
157
+ entity.changed(traits.Matrix);
158
+ }
159
+ else {
160
+ entity.add(traits.Matrix(poseToMatrix(createPose(poseInObserverFrame?.pose), new Matrix4())));
161
+ }
148
162
  hierarchy.setParent(entity, poseInObserverFrame?.referenceFrame);
149
163
  if (metadata?.showAxesHelper)
150
164
  entity.add(traits.ShowAxesHelper);
@@ -270,7 +284,7 @@ const drawModel = (world, model, api, { removable = true }) => {
270
284
  const { animationName, assets, scale } = physicalObject.geometryType.value;
271
285
  const baseTraits = [
272
286
  traits.Name(referenceFrame),
273
- traits.Pose(createPose(poseInObserverFrame?.pose)),
287
+ traits.Matrix(poseToMatrix(createPose(poseInObserverFrame?.pose), new Matrix4())),
274
288
  api,
275
289
  ...hierarchy.parentTraits(poseInObserverFrame?.referenceFrame),
276
290
  ];
@@ -4,6 +4,7 @@ export { useTrait } from './useTrait.svelte';
4
4
  export { useTarget } from './useTarget.svelte';
5
5
  export { useParentName } from './useParentName.svelte';
6
6
  export { provideHierarchy } from './provideHierarchy.svelte';
7
+ export { provideWorldMatrix } from './provideWorldMatrix.svelte';
7
8
  export * as traits from './traits';
8
9
  export * as relations from './relations';
9
10
  export * as hierarchy from './hierarchy';
package/dist/ecs/index.js CHANGED
@@ -4,6 +4,7 @@ export { useTrait } from './useTrait.svelte';
4
4
  export { useTarget } from './useTarget.svelte';
5
5
  export { useParentName } from './useParentName.svelte';
6
6
  export { provideHierarchy } from './provideHierarchy.svelte';
7
+ export { provideWorldMatrix } from './provideWorldMatrix.svelte';
7
8
  export * as traits from './traits';
8
9
  export * as relations from './relations';
9
10
  export * as hierarchy from './hierarchy';