@viamrobotics/motion-tools 1.26.1 → 1.27.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 (58) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +42 -29
  2. package/dist/assert.d.ts +13 -0
  3. package/dist/assert.js +20 -0
  4. package/dist/buf/common/v1/common_pb.d.ts +19 -0
  5. package/dist/buf/common/v1/common_pb.js +32 -0
  6. package/dist/components/BatchedArrows.svelte +43 -45
  7. package/dist/components/Entities/Arrows/Arrows.svelte +35 -29
  8. package/dist/components/Entities/Entities.svelte +3 -8
  9. package/dist/components/Entities/Frame.svelte +31 -32
  10. package/dist/components/Entities/Frame.svelte.d.ts +0 -2
  11. package/dist/components/Entities/GLTF.svelte +27 -36
  12. package/dist/components/Entities/Geometry.svelte +35 -24
  13. package/dist/components/Entities/Line.svelte +37 -43
  14. package/dist/components/Entities/Mesh.svelte +12 -18
  15. package/dist/components/Entities/Points.svelte +25 -28
  16. package/dist/components/Entities/Pose.svelte +17 -24
  17. package/dist/components/Entities/Pose.svelte.d.ts +1 -4
  18. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
  19. package/dist/components/Scene.svelte +7 -1
  20. package/dist/components/SceneProviders.svelte +2 -1
  21. package/dist/components/SelectedTransformControls.svelte +57 -34
  22. package/dist/components/StaticGeometries.svelte +1 -1
  23. package/dist/components/hover/HoveredEntity.svelte +33 -3
  24. package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
  25. package/dist/components/overlay/Details.svelte +72 -94
  26. package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
  27. package/dist/components/overlay/left-pane/Tree.svelte +9 -9
  28. package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
  29. package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
  30. package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
  31. package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
  32. package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
  33. package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
  34. package/dist/draw.js +24 -9
  35. package/dist/ecs/index.d.ts +1 -0
  36. package/dist/ecs/index.js +1 -0
  37. package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
  38. package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
  39. package/dist/ecs/traits.d.ts +41 -50
  40. package/dist/ecs/traits.js +57 -29
  41. package/dist/ecs/useTrait.svelte.d.ts +1 -6
  42. package/dist/ecs/useTrait.svelte.js +21 -13
  43. package/dist/ecs/worldMatrix.d.ts +10 -0
  44. package/dist/ecs/worldMatrix.js +138 -0
  45. package/dist/editing/FrameEditSession.js +31 -18
  46. package/dist/hooks/use3DModels.svelte.js +1 -1
  47. package/dist/hooks/useConfigFrames.svelte.js +12 -0
  48. package/dist/hooks/useDrawAPI.svelte.js +14 -6
  49. package/dist/hooks/useDrawService.svelte.js +4 -7
  50. package/dist/hooks/useFrames.svelte.js +23 -11
  51. package/dist/hooks/useGeometries.svelte.js +11 -3
  52. package/dist/hooks/usePartConfig.svelte.js +43 -6
  53. package/dist/hooks/useWorldState.svelte.js +10 -2
  54. package/dist/plugins/bvh.svelte.js +37 -26
  55. package/dist/transform.js +55 -21
  56. package/package.json +3 -3
  57. package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
  58. package/dist/components/overlay/left-pane/buildTree.js +0 -48
@@ -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
  ];
@@ -291,8 +305,9 @@ const drawModel = (world, model, api, { removable = true }) => {
291
305
  relations.ChildOf(root),
292
306
  api,
293
307
  ];
294
- if (scale)
295
- subEntityTraits.push(traits.Scale(scale));
308
+ if (scale) {
309
+ subEntityTraits.push(traits.Matrix(new Matrix4().makeScale(scale.x ?? 1, scale.y ?? 1, scale.z ?? 1)));
310
+ }
296
311
  if (metadata?.invisible)
297
312
  subEntityTraits.push(traits.Invisible);
298
313
  if (metadata?.showAxesHelper)
@@ -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';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Mount the world-matrix reactor: keeps `WorldMatrix` in sync with the
3
+ * cumulative `parent.WorldMatrix × local rendered` for every entity whose
4
+ * `Matrix` / `EditedMatrix` / `LiveMatrix` / `Scale` / `ChildOf` changes.
5
+ * Microtask-deferred so a burst of changes (e.g. one `useFrames` reconcile
6
+ * tick) coalesces into a single subtree walk.
7
+ */
8
+ export declare const provideWorldMatrix: () => void;
@@ -0,0 +1,13 @@
1
+ import { useWorld } from './useWorld';
2
+ import { installWorldMatrixListeners } from './worldMatrix';
3
+ /**
4
+ * Mount the world-matrix reactor: keeps `WorldMatrix` in sync with the
5
+ * cumulative `parent.WorldMatrix × local rendered` for every entity whose
6
+ * `Matrix` / `EditedMatrix` / `LiveMatrix` / `Scale` / `ChildOf` changes.
7
+ * Microtask-deferred so a burst of changes (e.g. one `useFrames` reconcile
8
+ * tick) coalesces into a single subtree walk.
9
+ */
10
+ export const provideWorldMatrix = () => {
11
+ const world = useWorld();
12
+ $effect(() => installWorldMatrixListeners(world));
13
+ };
@@ -1,7 +1,7 @@
1
1
  import type { GLTF as ThreeGltf } from 'three/examples/jsm/loaders/GLTFLoader.js';
2
2
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
3
3
  import { type Entity } from 'koota';
4
- import { BufferGeometry as ThreeBufferGeometry } from 'three';
4
+ import { Matrix4, BufferGeometry as ThreeBufferGeometry } from 'three';
5
5
  export declare const Name: import("koota").Trait<() => string>;
6
6
  export declare const UUID: import("koota").Trait<() => string>;
7
7
  /**
@@ -12,33 +12,12 @@ export declare const UUID: import("koota").Trait<() => string>;
12
12
  * adding this trait directly.
13
13
  */
14
14
  export declare const Orphan: import("koota").Trait<() => string>;
15
- export declare const Pose: import("koota").Trait<{
16
- x: number;
17
- y: number;
18
- z: number;
19
- oX: number;
20
- oY: number;
21
- oZ: number;
22
- theta: number;
23
- }>;
24
- export declare const EditedPose: import("koota").Trait<{
25
- x: number;
26
- y: number;
27
- z: number;
28
- oX: number;
29
- oY: number;
30
- oZ: number;
31
- theta: number;
32
- }>;
33
- export declare const LivePose: import("koota").Trait<{
34
- x: number;
35
- y: number;
36
- z: number;
37
- oX: number;
38
- oY: number;
39
- oZ: number;
40
- theta: number;
41
- }>;
15
+ /**
16
+ * Static positional offset (e.g. center of a geometry). Stored as a Pose
17
+ * for the rare cases that need OV+theta semantics (currently unused).
18
+ * Never composed through the parent chain — the `WorldMatrix` system
19
+ * doesn't read it.
20
+ */
42
21
  export declare const Center: import("koota").Trait<{
43
22
  x: number;
44
23
  y: number;
@@ -48,25 +27,42 @@ export declare const Center: import("koota").Trait<{
48
27
  oZ: number;
49
28
  theta: number;
50
29
  }>;
51
- export declare const InstancedPose: import("koota").Trait<{
52
- x: number;
53
- y: number;
54
- z: number;
55
- oX: number;
56
- oY: number;
57
- oZ: number;
58
- theta: number;
30
+ /**
31
+ * Local-to-parent transform. Stored AoS — one `Matrix4` instance per entity —
32
+ * not as 16 SoA fields. Every consumer reads all 16 elements of one entity at
33
+ * a time (`Object3D.matrix.copy`, batched-mesh per-instance writes, the
34
+ * world-matrix walk). SoA would allocate a fresh 16-field object on every
35
+ * `entity.get(Matrix)`; AoS returns the `Matrix4` reference, zero allocation
36
+ * per read, and plugs straight into Three.js. The trade-off — losing
37
+ * column-iteration locality — is fine because no system iterates a single
38
+ * matrix element across entities.
39
+ *
40
+ * Update pattern: read the `Matrix4` and mutate in place, then call
41
+ * `entity.changed(Matrix)` so `onChange` listeners (the `WorldMatrix` system,
42
+ * etc.) fire. Allocate a fresh `Matrix4` only on add.
43
+ */
44
+ export declare const Matrix: import("koota").Trait<() => Matrix4>;
45
+ /** User-staged local transform during a `FrameEditSession`. */
46
+ export declare const EditedMatrix: import("koota").Trait<() => Matrix4>;
47
+ /**
48
+ * Live local transform from the robot's kinematics. Composed with `Matrix`
49
+ * (network baseline) and `EditedMatrix` to produce the rendered transform.
50
+ */
51
+ export declare const LiveMatrix: import("koota").Trait<() => Matrix4>;
52
+ /**
53
+ * Cumulative world-space transform — `parent.WorldMatrix × local rendered`.
54
+ * Maintained by `provideWorldMatrix`. Read by hover label placement,
55
+ * batched-mesh population, and any other consumer that needs world-space.
56
+ */
57
+ export declare const WorldMatrix: import("koota").Trait<() => Matrix4>;
58
+ /**
59
+ * World-space transform of a hovered instance inside a points/arrows batch,
60
+ * paired with the instance index in the parent batched mesh.
61
+ */
62
+ export declare const InstancedMatrix: import("koota").Trait<() => {
63
+ matrix: Matrix4;
59
64
  index: number;
60
65
  }>;
61
- export declare const WorldPose: import("koota").Trait<{
62
- x: number;
63
- y: number;
64
- z: number;
65
- oX: number;
66
- oY: number;
67
- oZ: number;
68
- theta: number;
69
- }>;
70
66
  export declare const Hovered: import("koota").Trait<() => boolean>;
71
67
  export declare const Invisible: import("koota").Trait<() => boolean>;
72
68
  /**
@@ -147,11 +143,6 @@ export declare const GLTF: import("koota").Trait<() => {
147
143
  };
148
144
  animationName: string;
149
145
  }>;
150
- export declare const Scale: import("koota").Trait<{
151
- x: number;
152
- y: number;
153
- z: number;
154
- }>;
155
146
  export declare const FramesAPI: import("koota").Trait<() => boolean>;
156
147
  export declare const GeometriesAPI: import("koota").Trait<() => boolean>;
157
148
  export declare const DrawAPI: import("koota").Trait<() => boolean>;
@@ -1,6 +1,6 @@
1
1
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
2
2
  import { trait } from 'koota';
3
- import { BufferGeometry as ThreeBufferGeometry } from 'three';
3
+ import { Matrix4, BufferGeometry as ThreeBufferGeometry } from 'three';
4
4
  import { createBufferGeometry, updateBufferGeometry } from '../attribute';
5
5
  import { ColorFormat } from '../buf/draw/v1/metadata_pb';
6
6
  import { createBox, createCapsule, createSphere } from '../geometry';
@@ -16,29 +16,49 @@ export const UUID = trait(() => '');
16
16
  * adding this trait directly.
17
17
  */
18
18
  export const Orphan = trait(() => '');
19
- export const Pose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
20
- export const EditedPose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
21
- export const LivePose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
19
+ /**
20
+ * Static positional offset (e.g. center of a geometry). Stored as a Pose
21
+ * for the rare cases that need OV+theta semantics (currently unused).
22
+ * Never composed through the parent chain — the `WorldMatrix` system
23
+ * doesn't read it.
24
+ */
22
25
  export const Center = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
23
- export const InstancedPose = trait({
24
- x: 0,
25
- y: 0,
26
- z: 0,
27
- oX: 0,
28
- oY: 0,
29
- oZ: 1,
30
- theta: 0,
26
+ /**
27
+ * Local-to-parent transform. Stored AoS — one `Matrix4` instance per entity —
28
+ * not as 16 SoA fields. Every consumer reads all 16 elements of one entity at
29
+ * a time (`Object3D.matrix.copy`, batched-mesh per-instance writes, the
30
+ * world-matrix walk). SoA would allocate a fresh 16-field object on every
31
+ * `entity.get(Matrix)`; AoS returns the `Matrix4` reference, zero allocation
32
+ * per read, and plugs straight into Three.js. The trade-off — losing
33
+ * column-iteration locality — is fine because no system iterates a single
34
+ * matrix element across entities.
35
+ *
36
+ * Update pattern: read the `Matrix4` and mutate in place, then call
37
+ * `entity.changed(Matrix)` so `onChange` listeners (the `WorldMatrix` system,
38
+ * etc.) fire. Allocate a fresh `Matrix4` only on add.
39
+ */
40
+ export const Matrix = trait(() => new Matrix4());
41
+ /** User-staged local transform during a `FrameEditSession`. */
42
+ export const EditedMatrix = trait(() => new Matrix4());
43
+ /**
44
+ * Live local transform from the robot's kinematics. Composed with `Matrix`
45
+ * (network baseline) and `EditedMatrix` to produce the rendered transform.
46
+ */
47
+ export const LiveMatrix = trait(() => new Matrix4());
48
+ /**
49
+ * Cumulative world-space transform — `parent.WorldMatrix × local rendered`.
50
+ * Maintained by `provideWorldMatrix`. Read by hover label placement,
51
+ * batched-mesh population, and any other consumer that needs world-space.
52
+ */
53
+ export const WorldMatrix = trait(() => new Matrix4());
54
+ /**
55
+ * World-space transform of a hovered instance inside a points/arrows batch,
56
+ * paired with the instance index in the parent batched mesh.
57
+ */
58
+ export const InstancedMatrix = trait(() => ({
59
+ matrix: new Matrix4(),
31
60
  index: -1,
32
- });
33
- export const WorldPose = trait({
34
- x: 0,
35
- y: 0,
36
- z: 0,
37
- oX: 0,
38
- oY: 0,
39
- oZ: 1,
40
- theta: 0,
41
- });
61
+ }));
42
62
  export const Hovered = trait(() => true);
43
63
  export const Invisible = trait(() => true);
44
64
  /**
@@ -100,7 +120,6 @@ export const GLTF = trait(() => ({
100
120
  source: { url: '' },
101
121
  animationName: '',
102
122
  }));
103
- export const Scale = trait({ x: 1, y: 1, z: 1 });
104
123
  export const FramesAPI = trait(() => true);
105
124
  export const GeometriesAPI = trait(() => true);
106
125
  export const DrawAPI = trait(() => true);
@@ -174,30 +193,39 @@ export const updateGeometryTrait = (entity, geometry) => {
174
193
  return;
175
194
  }
176
195
  if (geometry.geometryType.case === 'box') {
196
+ const next = createBox(geometry.geometryType.value);
177
197
  if (entity.has(Box)) {
178
- entity.set(Box, createBox(geometry.geometryType.value));
198
+ const cur = entity.get(Box);
199
+ if (cur.x !== next.x || cur.y !== next.y || cur.z !== next.z)
200
+ entity.set(Box, next);
179
201
  }
180
202
  else {
181
203
  entity.remove(Capsule, Sphere, BufferGeometry);
182
- entity.add(Box(createBox(geometry.geometryType.value)));
204
+ entity.add(Box(next));
183
205
  }
184
206
  }
185
207
  else if (geometry.geometryType.case === 'capsule') {
208
+ const next = createCapsule(geometry.geometryType.value);
186
209
  if (entity.has(Capsule)) {
187
- entity.set(Capsule, createCapsule(geometry.geometryType.value));
210
+ const cur = entity.get(Capsule);
211
+ if (cur.r !== next.r || cur.l !== next.l)
212
+ entity.set(Capsule, next);
188
213
  }
189
214
  else {
190
215
  entity.remove(Box, Sphere, BufferGeometry);
191
- entity.add(Capsule(createCapsule(geometry.geometryType.value)));
216
+ entity.add(Capsule(next));
192
217
  }
193
218
  }
194
219
  else if (geometry.geometryType.case === 'sphere') {
220
+ const next = createSphere(geometry.geometryType.value);
195
221
  if (entity.has(Sphere)) {
196
- entity.set(Sphere, createSphere(geometry.geometryType.value));
222
+ const cur = entity.get(Sphere);
223
+ if (cur.r !== next.r)
224
+ entity.set(Sphere, next);
197
225
  }
198
226
  else {
199
227
  entity.remove(Box, Capsule, BufferGeometry);
200
- entity.add(Sphere(createSphere(geometry.geometryType.value)));
228
+ entity.add(Sphere(next));
201
229
  }
202
230
  }
203
231
  else if (geometry.geometryType.case === 'mesh') {
@@ -6,14 +6,9 @@ type Schema = {
6
6
  type TraitRecordFromSchema<T extends Schema> = T extends AoSFactory ? ReturnType<T> : {
7
7
  [P in keyof T]: T[P] extends (...args: never[]) => unknown ? ReturnType<T[P]> : T[P];
8
8
  };
9
- /**
10
- * The record of a trait.
11
- * For SoA it is a snapshot of the state for a single entity.
12
- * For AoS it is the state instance for a single entity.
13
- */
14
9
  type TraitRecord<T extends Trait | Schema> = T extends Trait ? TraitRecordFromSchema<T['schema']> : TraitRecordFromSchema<T>;
15
10
  export declare function isWorld(target: Entity | World | null | undefined): target is World;
16
11
  export declare function useTrait<T extends Trait>(target: () => Entity | World | undefined | null, trait: T): {
17
- current: TraitRecord<T> | undefined;
12
+ readonly current: TraitRecord<T> | undefined;
18
13
  };
19
14
  export {};
@@ -1,30 +1,37 @@
1
1
  import { $internal as internal } from 'koota';
2
+ import { untrack } from 'svelte';
2
3
  import { useWorld } from './useWorld';
3
4
  export function isWorld(target) {
4
5
  return typeof target?.spawn === 'function';
5
6
  }
6
7
  export function useTrait(target, trait) {
7
8
  const contextWorld = useWorld();
8
- const targetEntity = $derived(target());
9
- const world = $derived(isWorld(targetEntity) ? targetEntity : contextWorld);
10
- const entity = $derived(isWorld(targetEntity) ? targetEntity[internal].worldEntity : targetEntity);
11
- // Initialize the state with the current value of the trait.
12
- let value = $derived(entity?.get(trait));
9
+ let value = $state.raw();
10
+ // Version counter to force reactivity when the value reference is the same (AoS traits).
11
+ // Only read in the getter, never in the effect.
12
+ let version = $state(0);
13
13
  $effect(() => {
14
- const onAddUnsub = world.onAdd(trait, (e) => {
14
+ const t = target();
15
+ if (!t) {
16
+ value = undefined;
17
+ return;
18
+ }
19
+ const world = isWorld(t) ? t : contextWorld;
20
+ const entity = isWorld(t) ? t[internal].worldEntity : t;
21
+ value = entity.has(trait) ? entity.get(trait) : undefined;
22
+ const onChangeUnsub = world.onChange(trait, (e) => {
15
23
  if (e === entity) {
16
24
  value = e.get(trait);
25
+ untrack(() => version++);
17
26
  }
18
27
  });
28
+ const onAddUnsub = world.onAdd(trait, (e) => {
29
+ if (e === entity)
30
+ value = e.get(trait);
31
+ });
19
32
  const onRemoveUnsub = world.onRemove(trait, (e) => {
20
- if (e === entity) {
33
+ if (e === entity)
21
34
  value = undefined;
22
- }
23
- });
24
- const onChangeUnsub = world.onChange(trait, (e) => {
25
- if (e === entity) {
26
- value = e.get(trait);
27
- }
28
35
  });
29
36
  return () => {
30
37
  onChangeUnsub();
@@ -34,6 +41,7 @@ export function useTrait(target, trait) {
34
41
  });
35
42
  return {
36
43
  get current() {
44
+ void version;
37
45
  return value;
38
46
  },
39
47
  };