@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.
- package/dist/FrameConfigUpdater.svelte.js +42 -29
- package/dist/buf/common/v1/common_pb.d.ts +19 -0
- package/dist/buf/common/v1/common_pb.js +32 -0
- package/dist/components/BatchedArrows.svelte +31 -15
- package/dist/components/Entities/Entities.svelte +3 -8
- package/dist/components/Entities/Frame.svelte +25 -9
- package/dist/components/Entities/Frame.svelte.d.ts +0 -2
- package/dist/components/Entities/GLTF.svelte +5 -4
- package/dist/components/Entities/Line.svelte +5 -4
- package/dist/components/Entities/Mesh.svelte +12 -18
- package/dist/components/Entities/Points.svelte +5 -4
- package/dist/components/Entities/Pose.svelte +17 -24
- package/dist/components/Entities/Pose.svelte.d.ts +1 -4
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +40 -41
- package/dist/components/SceneProviders.svelte +2 -1
- package/dist/components/SelectedTransformControls.svelte +57 -34
- package/dist/components/StaticGeometries.svelte +1 -1
- package/dist/components/hover/HoveredEntity.svelte +33 -3
- package/dist/components/hover/LinkedHoveredEntity.svelte +2 -3
- package/dist/components/overlay/Details.svelte +72 -94
- package/dist/components/overlay/__tests__/__fixtures__/entity.js +14 -17
- package/dist/components/overlay/left-pane/Tree.svelte +9 -9
- package/dist/components/overlay/left-pane/Tree.svelte.d.ts +1 -2
- package/dist/components/overlay/left-pane/TreeContainer.svelte +4 -15
- package/dist/components/overlay/left-pane/TreeNode.svelte +1 -1
- package/dist/components/overlay/left-pane/TreeNode.svelte.d.ts +1 -1
- package/dist/components/overlay/left-pane/useTree.svelte.d.ts +14 -0
- package/dist/components/overlay/left-pane/useTree.svelte.js +63 -0
- package/dist/draw.js +21 -7
- package/dist/ecs/index.d.ts +1 -0
- package/dist/ecs/index.js +1 -0
- package/dist/ecs/provideWorldMatrix.svelte.d.ts +8 -0
- package/dist/ecs/provideWorldMatrix.svelte.js +13 -0
- package/dist/ecs/traits.d.ts +41 -45
- package/dist/ecs/traits.js +57 -28
- package/dist/ecs/useTrait.svelte.d.ts +1 -6
- package/dist/ecs/useTrait.svelte.js +21 -13
- package/dist/ecs/worldMatrix.d.ts +10 -0
- package/dist/ecs/worldMatrix.js +148 -0
- package/dist/editing/FrameEditSession.js +31 -18
- package/dist/hooks/use3DModels.svelte.js +1 -1
- package/dist/hooks/useConfigFrames.svelte.js +12 -0
- package/dist/hooks/useDrawAPI.svelte.js +14 -6
- package/dist/hooks/useFrames.svelte.js +23 -11
- package/dist/hooks/useGeometries.svelte.js +4 -2
- package/dist/hooks/usePartConfig.svelte.js +38 -3
- package/dist/hooks/useWorldState.svelte.js +10 -2
- package/dist/transform.js +55 -21
- package/package.json +3 -3
- package/dist/components/overlay/left-pane/buildTree.d.ts +0 -13
- 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
|
+
};
|
package/dist/ecs/traits.d.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
/**
|
package/dist/ecs/traits.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
75
|
-
if (!name || !
|
|
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: { ...
|
|
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.
|
|
97
|
+
const current = entity.get(traits.EditedMatrix);
|
|
96
98
|
if (!current)
|
|
97
99
|
return;
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
120
|
-
if (
|
|
121
|
-
|
|
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
|
|
130
|
-
if (
|
|
131
|
-
|
|
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
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
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) {
|