@viamrobotics/motion-tools 1.26.1 → 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 (51) 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/BatchedArrows.svelte +31 -15
  5. package/dist/components/Entities/Entities.svelte +3 -8
  6. package/dist/components/Entities/Frame.svelte +25 -9
  7. package/dist/components/Entities/Frame.svelte.d.ts +0 -2
  8. package/dist/components/Entities/GLTF.svelte +5 -4
  9. package/dist/components/Entities/Line.svelte +5 -4
  10. package/dist/components/Entities/Mesh.svelte +12 -18
  11. package/dist/components/Entities/Points.svelte +5 -4
  12. package/dist/components/Entities/Pose.svelte +17 -24
  13. package/dist/components/Entities/Pose.svelte.d.ts +1 -4
  14. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
  15. package/dist/components/SceneProviders.svelte +2 -1
  16. package/dist/components/SelectedTransformControls.svelte +57 -34
  17. package/dist/components/StaticGeometries.svelte +1 -1
  18. package/dist/components/hover/HoveredEntity.svelte +33 -3
  19. package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
  20. package/dist/components/overlay/Details.svelte +72 -94
  21. package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
  22. package/dist/components/overlay/left-pane/Tree.svelte +9 -9
  23. package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
  24. package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
  25. package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
  26. package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
  27. package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
  28. package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
  29. package/dist/draw.js +21 -7
  30. package/dist/ecs/index.d.ts +1 -0
  31. package/dist/ecs/index.js +1 -0
  32. package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
  33. package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
  34. package/dist/ecs/traits.d.ts +41 -45
  35. package/dist/ecs/traits.js +57 -28
  36. package/dist/ecs/useTrait.svelte.d.ts +1 -6
  37. package/dist/ecs/useTrait.svelte.js +21 -13
  38. package/dist/ecs/worldMatrix.d.ts +10 -0
  39. package/dist/ecs/worldMatrix.js +148 -0
  40. package/dist/editing/FrameEditSession.js +31 -18
  41. package/dist/hooks/use3DModels.svelte.js +1 -1
  42. package/dist/hooks/useConfigFrames.svelte.js +12 -0
  43. package/dist/hooks/useDrawAPI.svelte.js +14 -6
  44. package/dist/hooks/useFrames.svelte.js +23 -11
  45. package/dist/hooks/useGeometries.svelte.js +4 -2
  46. package/dist/hooks/usePartConfig.svelte.js +38 -3
  47. package/dist/hooks/useWorldState.svelte.js +10 -2
  48. package/dist/transform.js +55 -21
  49. package/package.json +3 -3
  50. package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
  51. package/dist/components/overlay/left-pane/buildTree.js +0 -48
@@ -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
  /**
@@ -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
  /**
@@ -174,30 +194,39 @@ export const updateGeometryTrait = (entity, geometry) => {
174
194
  return;
175
195
  }
176
196
  if (geometry.geometryType.case === 'box') {
197
+ const next = createBox(geometry.geometryType.value);
177
198
  if (entity.has(Box)) {
178
- entity.set(Box, createBox(geometry.geometryType.value));
199
+ const cur = entity.get(Box);
200
+ if (cur.x !== next.x || cur.y !== next.y || cur.z !== next.z)
201
+ entity.set(Box, next);
179
202
  }
180
203
  else {
181
204
  entity.remove(Capsule, Sphere, BufferGeometry);
182
- entity.add(Box(createBox(geometry.geometryType.value)));
205
+ entity.add(Box(next));
183
206
  }
184
207
  }
185
208
  else if (geometry.geometryType.case === 'capsule') {
209
+ const next = createCapsule(geometry.geometryType.value);
186
210
  if (entity.has(Capsule)) {
187
- entity.set(Capsule, createCapsule(geometry.geometryType.value));
211
+ const cur = entity.get(Capsule);
212
+ if (cur.r !== next.r || cur.l !== next.l)
213
+ entity.set(Capsule, next);
188
214
  }
189
215
  else {
190
216
  entity.remove(Box, Sphere, BufferGeometry);
191
- entity.add(Capsule(createCapsule(geometry.geometryType.value)));
217
+ entity.add(Capsule(next));
192
218
  }
193
219
  }
194
220
  else if (geometry.geometryType.case === 'sphere') {
221
+ const next = createSphere(geometry.geometryType.value);
195
222
  if (entity.has(Sphere)) {
196
- entity.set(Sphere, createSphere(geometry.geometryType.value));
223
+ const cur = entity.get(Sphere);
224
+ if (cur.r !== next.r)
225
+ entity.set(Sphere, next);
197
226
  }
198
227
  else {
199
228
  entity.remove(Box, Capsule, BufferGeometry);
200
- entity.add(Sphere(createSphere(geometry.geometryType.value)));
229
+ entity.add(Sphere(next));
201
230
  }
202
231
  }
203
232
  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
  };
@@ -0,0 +1,10 @@
1
+ import { type World } from 'koota';
2
+ /**
3
+ * Wire up listeners that maintain `WorldMatrix` reactively. Subscribes to
4
+ * add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, `Scale`, and
5
+ * `ChildOf`; enqueues affected entities and flushes on the next microtask.
6
+ *
7
+ * Returns an unsubscribe function. Plain function (not a rune hook) so tests
8
+ * can drive the lifecycle without mounting Svelte.
9
+ */
10
+ export declare const installWorldMatrixListeners: (world: World) => (() => void);
@@ -0,0 +1,148 @@
1
+ import {} from 'koota';
2
+ import { Matrix4, Vector3 } from 'three';
3
+ import { composeLocalMatrix } from '../transform';
4
+ import { ChildOf } from './relations';
5
+ import { EditedMatrix, LiveMatrix, Matrix, Scale, WorldMatrix } from './traits';
6
+ const scaleVec3 = new Vector3();
7
+ /**
8
+ * Compute the entity's local-to-parent transform into `out`. Mirrors the
9
+ * blend used by `Frame.svelte` so `WorldMatrix` agrees with the displayed
10
+ * scenegraph.
11
+ *
12
+ * - All three matrix traits present: `live × baseline⁻¹ × edited`.
13
+ * - Otherwise: prefer `EditedMatrix` over `Matrix`.
14
+ *
15
+ * Returns `true` after writing to `out`; returns `false` and leaves `out`
16
+ * untouched when the entity has no matrix-shaped trait.
17
+ */
18
+ const toLocalMatrix = (entity, out) => {
19
+ const matrix = entity.get(Matrix);
20
+ const editedMatrix = entity.get(EditedMatrix);
21
+ const liveMatrix = entity.get(LiveMatrix);
22
+ if (liveMatrix && matrix && editedMatrix) {
23
+ composeLocalMatrix(liveMatrix, matrix, editedMatrix, out);
24
+ return true;
25
+ }
26
+ if (editedMatrix) {
27
+ out.copy(editedMatrix);
28
+ return true;
29
+ }
30
+ if (matrix) {
31
+ out.copy(matrix);
32
+ return true;
33
+ }
34
+ return false;
35
+ };
36
+ /**
37
+ * Synchronously compute and write `WorldMatrix` for every entity in `dirty`
38
+ * and every descendant via `ChildOf`. Memoizes per-entity world matrices in
39
+ * `cache` so siblings reuse a parent's result. Caller passes a fresh `cache`
40
+ * map per flush.
41
+ */
42
+ const recomputeWorldMatrix = (world, entity, cache) => {
43
+ if (!entity.isAlive())
44
+ return undefined;
45
+ const cached = cache.get(entity);
46
+ if (cached)
47
+ return cached;
48
+ // Reuse the entity's existing `WorldMatrix` storage when present so a
49
+ // flush doesn't allocate a throwaway matrix per entity. First-time
50
+ // entities get a fresh `Matrix4` that's added as the trait below.
51
+ const out = entity.get(WorldMatrix) ?? new Matrix4();
52
+ const hasLocal = toLocalMatrix(entity, out);
53
+ if (!hasLocal)
54
+ out.identity();
55
+ const scale = entity.get(Scale);
56
+ if (scale) {
57
+ out.scale(scaleVec3.copy(scale));
58
+ }
59
+ const parent = entity.targetFor(ChildOf);
60
+ if (parent && parent.isAlive()) {
61
+ const parentWorld = recomputeWorldMatrix(world, parent, cache);
62
+ if (parentWorld)
63
+ out.premultiply(parentWorld);
64
+ }
65
+ cache.set(entity, out);
66
+ return out;
67
+ };
68
+ const flushDirty = (world, dirty) => {
69
+ if (dirty.size === 0)
70
+ return;
71
+ const cache = new Map();
72
+ const expanded = new Set();
73
+ const collect = (entity) => {
74
+ if (expanded.has(entity))
75
+ return;
76
+ expanded.add(entity);
77
+ for (const child of world.query(ChildOf(entity))) {
78
+ collect(child);
79
+ }
80
+ };
81
+ for (const entity of dirty)
82
+ collect(entity);
83
+ dirty.clear();
84
+ for (const entity of expanded) {
85
+ if (!entity.isAlive())
86
+ continue;
87
+ const worldMat = recomputeWorldMatrix(world, entity, cache);
88
+ if (!worldMat)
89
+ continue;
90
+ if (entity.has(WorldMatrix)) {
91
+ entity.changed(WorldMatrix);
92
+ }
93
+ else {
94
+ entity.add(WorldMatrix(worldMat));
95
+ }
96
+ }
97
+ };
98
+ /**
99
+ * Wire up listeners that maintain `WorldMatrix` reactively. Subscribes to
100
+ * add/change/remove on `Matrix`, `EditedMatrix`, `LiveMatrix`, `Scale`, and
101
+ * `ChildOf`; enqueues affected entities and flushes on the next microtask.
102
+ *
103
+ * Returns an unsubscribe function. Plain function (not a rune hook) so tests
104
+ * can drive the lifecycle without mounting Svelte.
105
+ */
106
+ export const installWorldMatrixListeners = (world) => {
107
+ const dirty = new Set();
108
+ let scheduled = false;
109
+ const enqueue = (entity) => {
110
+ dirty.add(entity);
111
+ if (scheduled)
112
+ return;
113
+ scheduled = true;
114
+ queueMicrotask(() => {
115
+ scheduled = false;
116
+ flushDirty(world, dirty);
117
+ });
118
+ };
119
+ for (const entity of world.query(Matrix))
120
+ enqueue(entity);
121
+ for (const entity of world.query(EditedMatrix))
122
+ enqueue(entity);
123
+ for (const entity of world.query(LiveMatrix))
124
+ enqueue(entity);
125
+ for (const entity of world.query(Scale))
126
+ enqueue(entity);
127
+ const unsubs = [
128
+ world.onAdd(Matrix, enqueue),
129
+ world.onChange(Matrix, enqueue),
130
+ world.onRemove(Matrix, enqueue),
131
+ world.onAdd(EditedMatrix, enqueue),
132
+ world.onChange(EditedMatrix, enqueue),
133
+ world.onRemove(EditedMatrix, enqueue),
134
+ world.onAdd(LiveMatrix, enqueue),
135
+ world.onChange(LiveMatrix, enqueue),
136
+ world.onRemove(LiveMatrix, enqueue),
137
+ world.onAdd(Scale, enqueue),
138
+ world.onChange(Scale, enqueue),
139
+ world.onRemove(Scale, enqueue),
140
+ world.onAdd(ChildOf, enqueue),
141
+ world.onChange(ChildOf, enqueue),
142
+ world.onRemove(ChildOf, enqueue),
143
+ ];
144
+ return () => {
145
+ for (const unsub of unsubs)
146
+ unsub();
147
+ };
148
+ };
@@ -1,5 +1,6 @@
1
1
  import { hierarchy, traits } from '../ecs';
2
- import { isFinitePose } from '../transform';
2
+ import { createPose, isFinitePose, matrixToPose, poseToMatrix } from '../transform';
3
+ const tempPose = createPose();
3
4
  const captureGeometry = (entity) => {
4
5
  const box = entity.get(traits.Box);
5
6
  if (box)
@@ -71,13 +72,14 @@ export class FrameEditSession {
71
72
  this.onClose = onClose;
72
73
  for (const entity of entities) {
73
74
  const name = entity.get(traits.Name);
74
- const editedPose = entity.get(traits.EditedPose);
75
- if (!name || !editedPose)
75
+ const editedMatrix = entity.get(traits.EditedMatrix);
76
+ if (!name || !editedMatrix)
76
77
  continue;
78
+ matrixToPose(editedMatrix, tempPose);
77
79
  this.snapshots.set(entity, {
78
80
  name,
79
81
  parent: hierarchy.getParentName(entity) ?? 'world',
80
- editedPose: { ...editedPose },
82
+ editedPose: { ...tempPose },
81
83
  geometry: captureGeometry(entity),
82
84
  });
83
85
  }
@@ -92,11 +94,13 @@ export class FrameEditSession {
92
94
  const snap = this.snapshots.get(entity);
93
95
  if (!snap || this.#closed)
94
96
  return;
95
- const current = entity.get(traits.EditedPose);
97
+ const current = entity.get(traits.EditedMatrix);
96
98
  if (!current)
97
99
  return;
98
- const next = { ...current, ...pose };
99
- entity.set(traits.EditedPose, next);
100
+ matrixToPose(current, tempPose);
101
+ const next = { ...tempPose, ...pose };
102
+ poseToMatrix(next, current);
103
+ entity.changed(traits.EditedMatrix);
100
104
  this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', next, liveGeometry(entity));
101
105
  };
102
106
  stageGeometry = (entity, geometry) => {
@@ -116,9 +120,10 @@ export class FrameEditSession {
116
120
  else if (geometry.type === 'capsule') {
117
121
  restoreGeometryTrait(entity, { type: 'capsule', capsule: { r: geometry.r, l: geometry.l } });
118
122
  }
119
- const editedPose = entity.get(traits.EditedPose);
120
- if (editedPose) {
121
- this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', editedPose, geometry);
123
+ const editedMatrix = entity.get(traits.EditedMatrix);
124
+ if (editedMatrix) {
125
+ matrixToPose(editedMatrix, tempPose);
126
+ this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', { ...tempPose }, geometry);
122
127
  }
123
128
  };
124
129
  stageParent = (entity, parent) => {
@@ -126,9 +131,10 @@ export class FrameEditSession {
126
131
  if (!snap || this.#closed)
127
132
  return;
128
133
  hierarchy.setParent(entity, parent === 'world' ? undefined : parent);
129
- const editedPose = entity.get(traits.EditedPose);
130
- if (editedPose) {
131
- this.updateFrame(snap.name, parent, editedPose, liveGeometry(entity));
134
+ const editedMatrix = entity.get(traits.EditedMatrix);
135
+ if (editedMatrix) {
136
+ matrixToPose(editedMatrix, tempPose);
137
+ this.updateFrame(snap.name, parent, { ...tempPose }, liveGeometry(entity));
132
138
  }
133
139
  };
134
140
  stageDelete = (entity) => {
@@ -145,10 +151,13 @@ export class FrameEditSession {
145
151
  if (this.#closed)
146
152
  return false;
147
153
  for (const [entity] of this.snapshots) {
148
- const pose = entity.get(traits.EditedPose);
149
- if (pose && !isFinitePose(pose)) {
150
- this.abort();
151
- return false;
154
+ const matrix = entity.get(traits.EditedMatrix);
155
+ if (matrix) {
156
+ matrixToPose(matrix, tempPose);
157
+ if (!isFinitePose(tempPose)) {
158
+ this.abort();
159
+ return false;
160
+ }
152
161
  }
153
162
  }
154
163
  this.#close();
@@ -163,7 +172,11 @@ export class FrameEditSession {
163
172
  return;
164
173
  for (const [entity, snap] of this.snapshots) {
165
174
  if (entity.isAlive()) {
166
- entity.set(traits.EditedPose, snap.editedPose);
175
+ const matrix = entity.get(traits.EditedMatrix);
176
+ if (matrix) {
177
+ poseToMatrix(snap.editedPose, matrix);
178
+ entity.changed(traits.EditedMatrix);
179
+ }
167
180
  hierarchy.setParent(entity, snap.parent === 'world' ? undefined : snap.parent);
168
181
  restoreGeometryTrait(entity, snap.geometry);
169
182
  }
@@ -48,13 +48,13 @@ export const provide3DModels = (partID) => {
48
48
  }
49
49
  });
50
50
  }
51
- current = next;
52
51
  }
53
52
  catch (error) {
54
53
  // some arms may not implement this api yet
55
54
  console.warn(`${client.current.name} returned an error: ${error} when getting 3D models`);
56
55
  }
57
56
  }
57
+ current = next;
58
58
  };
59
59
  $effect(() => {
60
60
  const shouldFetchModels = settings.isLoaded && settings.current.renderArmModels.includes('model');
@@ -57,6 +57,18 @@ export const provideConfigFrames = () => {
57
57
  const frameValues = $derived(Object.values(frames));
58
58
  const getParentFrameOptions = (componentName) => {
59
59
  const validFrames = new Set(frameValues.map((frame) => frame.referenceFrame));
60
+ /**
61
+ * Fragment components without a mod don't appear in frameValues (we only
62
+ * track frames with explicit $set mods), but the fragment itself supplies
63
+ * their frame so they render in the scene and are valid parents. Exclude
64
+ * any whose frame the user has $unset.
65
+ */
66
+ const unsetFragmentNames = new Set(fragmentUnsetFrameNames);
67
+ for (const name of Object.keys(partConfig.componentNameToFragmentId)) {
68
+ if (!unsetFragmentNames.has(name)) {
69
+ validFrames.add(name);
70
+ }
71
+ }
60
72
  validFrames.add('world');
61
73
  const frameNameQueue = [componentName];
62
74
  while (frameNameQueue.length > 0) {