@viamrobotics/motion-tools 1.24.0 → 1.25.1

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 (43) hide show
  1. package/dist/FrameConfigUpdater.svelte.js +5 -5
  2. package/dist/components/BatchedArrows.svelte +4 -4
  3. package/dist/components/Entities/Arrows/Arrows.svelte +2 -2
  4. package/dist/components/Entities/Capsule.svelte +137 -0
  5. package/dist/components/Entities/Capsule.svelte.d.ts +19 -0
  6. package/dist/components/Entities/Frame.svelte +2 -2
  7. package/dist/components/Entities/GLTF.svelte +2 -2
  8. package/dist/components/Entities/Geometry.svelte +2 -2
  9. package/dist/components/Entities/Line.svelte +2 -2
  10. package/dist/components/Entities/Mesh.svelte +121 -68
  11. package/dist/components/Entities/Points.svelte +2 -2
  12. package/dist/components/Entities/Pose.svelte +2 -2
  13. package/dist/components/SceneProviders.svelte +2 -0
  14. package/dist/components/Snapshot.svelte +20 -10
  15. package/dist/components/overlay/Details.svelte +3 -3
  16. package/dist/components/overlay/__tests__/__fixtures__/entity.js +2 -2
  17. package/dist/components/overlay/left-pane/buildTree.js +3 -3
  18. package/dist/draw.js +7 -8
  19. package/dist/ecs/hierarchy.d.ts +36 -0
  20. package/dist/ecs/hierarchy.js +80 -0
  21. package/dist/ecs/index.d.ts +4 -0
  22. package/dist/ecs/index.js +4 -0
  23. package/dist/ecs/provideHierarchy.svelte.d.ts +17 -0
  24. package/dist/ecs/provideHierarchy.svelte.js +31 -0
  25. package/dist/ecs/relations.d.ts +7 -0
  26. package/dist/ecs/relations.js +8 -1
  27. package/dist/ecs/traits.d.ts +9 -4
  28. package/dist/ecs/traits.js +8 -14
  29. package/dist/ecs/useParentName.svelte.d.ts +11 -0
  30. package/dist/ecs/useParentName.svelte.js +21 -0
  31. package/dist/ecs/useTarget.svelte.d.ts +10 -0
  32. package/dist/ecs/useTarget.svelte.js +42 -0
  33. package/dist/editing/FrameEditSession.js +6 -6
  34. package/dist/hooks/useArmKinematics.svelte.js +12 -8
  35. package/dist/hooks/useDrawAPI.svelte.js +4 -4
  36. package/dist/hooks/useFrames.svelte.js +3 -3
  37. package/dist/hooks/useGeometries.svelte.js +3 -2
  38. package/dist/hooks/usePointcloudObjects.svelte.js +3 -2
  39. package/dist/hooks/usePointclouds.svelte.js +3 -2
  40. package/dist/hooks/usePose.svelte.js +8 -6
  41. package/dist/snapshot.d.ts +7 -0
  42. package/dist/snapshot.js +74 -1
  43. package/package.json +3 -3
@@ -38,7 +38,7 @@
38
38
  } from 'svelte-tweakpane-ui'
39
39
 
40
40
  import AddRelationship from './AddRelationship.svelte'
41
- import { relations, traits, useTrait, useWorld } from '../../ecs'
41
+ import { hierarchy, relations, traits, useParentName, useTrait, useWorld } from '../../ecs'
42
42
  import { FrameConfigUpdater } from '../../FrameConfigUpdater.svelte'
43
43
  import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
44
44
  import { useCameraControls } from '../../hooks/useControls.svelte'
@@ -79,7 +79,7 @@
79
79
  const worldOrientation = $state({ x: 0, y: 0, z: 1, th: 0 })
80
80
  const linkedEntities = useLinkedEntities()
81
81
  const name = useTrait(() => entity, traits.Name)
82
- const parent = useTrait(() => entity, traits.Parent)
82
+ const parent = useParentName(() => entity)
83
83
  const localPose = useTrait(() => entity, traits.EditedPose)
84
84
  const box = useTrait(() => entity, traits.Box)
85
85
  const sphere = useTrait(() => entity, traits.Sphere)
@@ -220,7 +220,7 @@
220
220
  if (event.detail.origin !== 'internal' || !entity) return
221
221
  const value = event.detail.value as string
222
222
  if (value === parent.current) return
223
- traits.setParentTrait(entity, value)
223
+ hierarchy.setParent(entity, value)
224
224
  detailConfigUpdater.setFrameParent(entity, value)
225
225
  }
226
226
 
@@ -1,6 +1,6 @@
1
- import { traits } from '../../../../ecs';
1
+ import { hierarchy, traits } from '../../../../ecs';
2
2
  export const createEntityFixture = (world) => {
3
- return world.spawn(traits.Parent('parent_frame'), traits.Name('Test Object'), traits.Pose({
3
+ return world.spawn(...hierarchy.parentTraits('parent_frame'), traits.Name('Test Object'), traits.Pose({
4
4
  x: 10,
5
5
  y: 20,
6
6
  z: 30,
@@ -1,4 +1,4 @@
1
- import { traits } from '../../../ecs';
1
+ import { hierarchy, traits } from '../../../ecs';
2
2
  function sortNodes(nodes) {
3
3
  nodes.sort((a, b) => a.entity.get(traits.Name)?.localeCompare(b.entity.get(traits.Name) ?? '') ?? 0);
4
4
  }
@@ -10,7 +10,7 @@ export const buildTreeNodes = (entities) => {
10
10
  const rootNodes = [];
11
11
  const childNodes = [];
12
12
  for (const entity of entities) {
13
- const parent = entity.get(traits.Parent);
13
+ const parent = hierarchy.getParentName(entity);
14
14
  const name = entity.get(traits.Name) ?? '';
15
15
  const node = { entity };
16
16
  nodeMap[name] = node;
@@ -22,7 +22,7 @@ export const buildTreeNodes = (entities) => {
22
22
  }
23
23
  }
24
24
  for (const node of childNodes) {
25
- const parent = node.entity.get(traits.Parent);
25
+ const parent = hierarchy.getParentName(node.entity);
26
26
  if (parent) {
27
27
  const parentNode = nodeMap[parent];
28
28
  node.parent = parentNode;
package/dist/draw.js CHANGED
@@ -3,7 +3,7 @@ import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
3
3
  import { UuidTool } from 'uuid-tool';
4
4
  import { createBufferGeometry, preAllocateBufferGeometry, updateBufferGeometry, writeBufferGeometryRange, } from './attribute';
5
5
  import { asFloat32Array, asOpacity, asRGB, inMeters, isSingleColor, isVertexColors, STRIDE, } from './buffer';
6
- import { relations, traits } from './ecs';
6
+ import { hierarchy, relations, traits } from './ecs';
7
7
  import { parsePcdInWorker } from './loaders/pcd';
8
8
  import { metadataFromStruct } from './metadata';
9
9
  import { createPose } from './transform';
@@ -55,7 +55,7 @@ export const drawTransform = (world, { referenceFrame, poseInObserverFrame, phys
55
55
  }
56
56
  if (removable)
57
57
  entityTraits.push(traits.Removable);
58
- entityTraits.push(...traits.getParentTrait(poseInObserverFrame?.referenceFrame));
58
+ entityTraits.push(...hierarchy.parentTraits(poseInObserverFrame?.referenceFrame));
59
59
  const parsedMetadata = metadataFromStruct(metadata?.fields);
60
60
  if (parsedMetadata.showAxesHelper)
61
61
  entityTraits.push(traits.ShowAxesHelper);
@@ -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, ...traits.getParentTrait(poseInObserverFrame?.referenceFrame), ...uuidTraits);
91
+ const entity = world.spawn(traits.Name(referenceFrame), traits.Pose(createPose(poseInObserverFrame?.pose)), api, ...hierarchy.parentTraits(poseInObserverFrame?.referenceFrame), ...uuidTraits);
92
92
  if (removable)
93
93
  entity.add(traits.Removable);
94
94
  if (metadata?.showAxesHelper)
@@ -100,7 +100,7 @@ export const drawDrawing = (world, drawing, api, { removable = true } = {}) => {
100
100
  };
101
101
  export const updateTransform = (entity, { poseInObserverFrame, physicalObject, metadata }, { removable = true } = {}) => {
102
102
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
103
- traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
103
+ hierarchy.setParent(entity, poseInObserverFrame?.referenceFrame);
104
104
  if (physicalObject) {
105
105
  traits.updateGeometryTrait(entity, physicalObject);
106
106
  const center = physicalObject.center;
@@ -145,7 +145,7 @@ export const updateDrawing = (world, entity, drawing, { removable = true } = {})
145
145
  if (!world.has(entity))
146
146
  return { entity, relationships: metadata?.relationships };
147
147
  entity.set(traits.Pose, createPose(poseInObserverFrame?.pose));
148
- traits.setParentTrait(entity, poseInObserverFrame?.referenceFrame);
148
+ hierarchy.setParent(entity, poseInObserverFrame?.referenceFrame);
149
149
  if (metadata?.showAxesHelper)
150
150
  entity.add(traits.ShowAxesHelper);
151
151
  if (!metadata?.showAxesHelper)
@@ -163,7 +163,7 @@ export const updateDrawing = (world, entity, drawing, { removable = true } = {})
163
163
  };
164
164
  export const updateModel = (world, entity, drawing, api, { removable = true } = {}) => {
165
165
  if (world.has(entity))
166
- entity.destroy();
166
+ hierarchy.destroyEntityTree(world, entity);
167
167
  return drawDrawing(world, drawing, api, { removable });
168
168
  };
169
169
  const applyShape = (entity, { physicalObject, metadata }) => {
@@ -272,7 +272,7 @@ const drawModel = (world, model, api, { removable = true }) => {
272
272
  traits.Name(referenceFrame),
273
273
  traits.Pose(createPose(poseInObserverFrame?.pose)),
274
274
  api,
275
- ...traits.getParentTrait(poseInObserverFrame?.referenceFrame),
275
+ ...hierarchy.parentTraits(poseInObserverFrame?.referenceFrame),
276
276
  ];
277
277
  const uuidStr = uuidBytesToString(uuid);
278
278
  if (uuidStr)
@@ -288,7 +288,6 @@ const drawModel = (world, model, api, { removable = true }) => {
288
288
  for (const asset of assets) {
289
289
  const subEntityTraits = [
290
290
  traits.Name(`${referenceFrame} model ${i++}`),
291
- traits.Parent(referenceFrame),
292
291
  relations.ChildOf(root),
293
292
  api,
294
293
  ];
@@ -0,0 +1,36 @@
1
+ import { type ConfigurableTrait, type Entity, type QueryResult, type Trait, type World } from 'koota';
2
+ /**
3
+ * Trait list for `world.spawn(...)`. Always emits `Orphan(name)` for non-root
4
+ * parents; the hierarchy resolver (`provideHierarchy`) swaps it to
5
+ * `ChildOf(parentEntity)` once a frame with that name exists. Returns `[]`
6
+ * for the world root (`undefined`, `''`, or `'world'`).
7
+ */
8
+ export declare const parentTraits: (name: string | undefined) => ConfigurableTrait[];
9
+ /**
10
+ * Set or clear an entity's parent. Strips any existing `ChildOf` or `Orphan`,
11
+ * then writes `Orphan(name)` (the resolver converts it to `ChildOf` on the
12
+ * next reactive flush). Pass `undefined` or `'world'` to detach to root.
13
+ */
14
+ export declare const setParent: (entity: Entity, name: string | undefined) => void;
15
+ /** The parent entity, or `undefined` at the world root or while orphaned. */
16
+ export declare const getParentEntity: (entity: Entity) => Entity | undefined;
17
+ /**
18
+ * The parent's name string. Reads through `ChildOf` first, falls back to
19
+ * `Orphan(name)` while the parent isn't present. Returns `undefined` for
20
+ * world-root entities.
21
+ */
22
+ export declare const getParentName: (entity: Entity) => string | undefined;
23
+ /**
24
+ * Destroy an entity and every `ChildOf` descendant, depth-first. Use for
25
+ * sub-trees whose lifetimes are tied together (e.g. a model root and its
26
+ * GLTF assets). General frame removal should use `entity.destroy()` so
27
+ * children survive as orphans.
28
+ */
29
+ export declare const destroyEntityTree: (world: World, entity: Entity) => void;
30
+ /**
31
+ * Synchronously resolve every `Orphan` whose desired parent now exists in
32
+ * the world. Called by `provideHierarchy` when the orphan/named query sets
33
+ * change or when a `Name` is renamed; also exposed for tests so they can
34
+ * drive resolution without mounting a component.
35
+ */
36
+ export declare const resolveOrphans: (named: QueryResult<[Trait<() => string>]>, orphans: QueryResult<[Trait<() => string>]>) => void;
@@ -0,0 +1,80 @@
1
+ import {} from 'koota';
2
+ import { ChildOf } from './relations';
3
+ import { Name, Orphan } from './traits';
4
+ /**
5
+ * Trait list for `world.spawn(...)`. Always emits `Orphan(name)` for non-root
6
+ * parents; the hierarchy resolver (`provideHierarchy`) swaps it to
7
+ * `ChildOf(parentEntity)` once a frame with that name exists. Returns `[]`
8
+ * for the world root (`undefined`, `''`, or `'world'`).
9
+ */
10
+ export const parentTraits = (name) => {
11
+ if (!name || name === 'world')
12
+ return [];
13
+ return [Orphan(name)];
14
+ };
15
+ /**
16
+ * Set or clear an entity's parent. Strips any existing `ChildOf` or `Orphan`,
17
+ * then writes `Orphan(name)` (the resolver converts it to `ChildOf` on the
18
+ * next reactive flush). Pass `undefined` or `'world'` to detach to root.
19
+ */
20
+ export const setParent = (entity, name) => {
21
+ const target = entity.targetFor(ChildOf);
22
+ if (target)
23
+ entity.remove(ChildOf(target));
24
+ entity.remove(Orphan);
25
+ if (!name || name === 'world')
26
+ return;
27
+ entity.add(Orphan(name));
28
+ };
29
+ /** The parent entity, or `undefined` at the world root or while orphaned. */
30
+ export const getParentEntity = (entity) => entity.targetFor(ChildOf);
31
+ /**
32
+ * The parent's name string. Reads through `ChildOf` first, falls back to
33
+ * `Orphan(name)` while the parent isn't present. Returns `undefined` for
34
+ * world-root entities.
35
+ */
36
+ export const getParentName = (entity) => {
37
+ const parent = entity.targetFor(ChildOf);
38
+ if (parent && parent.isAlive())
39
+ return parent.get(Name);
40
+ const orphanFor = entity.get(Orphan);
41
+ return orphanFor || undefined;
42
+ };
43
+ /**
44
+ * Destroy an entity and every `ChildOf` descendant, depth-first. Use for
45
+ * sub-trees whose lifetimes are tied together (e.g. a model root and its
46
+ * GLTF assets). General frame removal should use `entity.destroy()` so
47
+ * children survive as orphans.
48
+ */
49
+ export const destroyEntityTree = (world, entity) => {
50
+ if (!entity.isAlive())
51
+ return;
52
+ for (const child of world.query(ChildOf(entity))) {
53
+ destroyEntityTree(world, child);
54
+ }
55
+ entity.destroy();
56
+ };
57
+ /**
58
+ * Synchronously resolve every `Orphan` whose desired parent now exists in
59
+ * the world. Called by `provideHierarchy` when the orphan/named query sets
60
+ * change or when a `Name` is renamed; also exposed for tests so they can
61
+ * drive resolution without mounting a component.
62
+ */
63
+ export const resolveOrphans = (named, orphans) => {
64
+ const index = new Map();
65
+ for (const entity of named) {
66
+ const name = entity.get(Name);
67
+ if (name)
68
+ index.set(name, entity);
69
+ }
70
+ for (const orphan of orphans) {
71
+ const wantedName = orphan.get(Orphan);
72
+ if (!wantedName)
73
+ continue;
74
+ const parent = index.get(wantedName);
75
+ if (!parent)
76
+ continue;
77
+ orphan.remove(Orphan);
78
+ orphan.add(ChildOf(parent));
79
+ }
80
+ };
@@ -1,5 +1,9 @@
1
1
  export { provideWorld, useWorld } from './useWorld';
2
2
  export { useQuery } from './useQuery.svelte';
3
3
  export { useTrait } from './useTrait.svelte';
4
+ export { useTarget } from './useTarget.svelte';
5
+ export { useParentName } from './useParentName.svelte';
6
+ export { provideHierarchy } from './provideHierarchy.svelte';
4
7
  export * as traits from './traits';
5
8
  export * as relations from './relations';
9
+ export * as hierarchy from './hierarchy';
package/dist/ecs/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  export { provideWorld, useWorld } from './useWorld';
2
2
  export { useQuery } from './useQuery.svelte';
3
3
  export { useTrait } from './useTrait.svelte';
4
+ export { useTarget } from './useTarget.svelte';
5
+ export { useParentName } from './useParentName.svelte';
6
+ export { provideHierarchy } from './provideHierarchy.svelte';
4
7
  export * as traits from './traits';
5
8
  export * as relations from './relations';
9
+ export * as hierarchy from './hierarchy';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Mounts the hierarchy resolver: each tick, any entity with `Orphan(name)`
3
+ * whose desired parent is now in the world is converted to
4
+ * `ChildOf(parentEntity)`. Call once at the top of the app, alongside the
5
+ * other `provide*` hooks.
6
+ *
7
+ * Reactive on:
8
+ * - `useQuery(Orphan)` membership — new orphans appear or resolve away
9
+ * - `useQuery(Name)` membership — new candidate parents appear/disappear
10
+ * - `world.onChange(Name)` — an existing entity is renamed into a name
11
+ * that some orphan is waiting for
12
+ *
13
+ * Children whose `ChildOf` parent is destroyed are *not* automatically
14
+ * re-orphaned. Call `hierarchy.setParent` on the affected children
15
+ * explicitly if you need them to reattach to a same-named replacement.
16
+ */
17
+ export declare const provideHierarchy: () => void;
@@ -0,0 +1,31 @@
1
+ import { resolveOrphans } from './hierarchy';
2
+ import { Name, Orphan } from './traits';
3
+ import { useQuery } from './useQuery.svelte';
4
+ import { useWorld } from './useWorld';
5
+ /**
6
+ * Mounts the hierarchy resolver: each tick, any entity with `Orphan(name)`
7
+ * whose desired parent is now in the world is converted to
8
+ * `ChildOf(parentEntity)`. Call once at the top of the app, alongside the
9
+ * other `provide*` hooks.
10
+ *
11
+ * Reactive on:
12
+ * - `useQuery(Orphan)` membership — new orphans appear or resolve away
13
+ * - `useQuery(Name)` membership — new candidate parents appear/disappear
14
+ * - `world.onChange(Name)` — an existing entity is renamed into a name
15
+ * that some orphan is waiting for
16
+ *
17
+ * Children whose `ChildOf` parent is destroyed are *not* automatically
18
+ * re-orphaned. Call `hierarchy.setParent` on the affected children
19
+ * explicitly if you need them to reattach to a same-named replacement.
20
+ */
21
+ export const provideHierarchy = () => {
22
+ const world = useWorld();
23
+ const orphans = useQuery(Orphan);
24
+ const named = useQuery(Name);
25
+ $effect(() => {
26
+ resolveOrphans(named.current, orphans.current);
27
+ });
28
+ $effect(() => {
29
+ return world.onChange(Name, () => resolveOrphans(named.current, orphans.current));
30
+ });
31
+ };
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Parent → child hierarchy relation. `exclusive: true` because each entity has
3
+ * at most one parent. Cascade-on-orphan is intentionally OFF: when a parent is
4
+ * destroyed, children survive and gain an `Orphan(parentName)` trait so they
5
+ * reattach if a frame with that name reappears. Sub-trees that should be torn
6
+ * down together (e.g. model roots + assets) call `destroyEntityTree` instead.
7
+ */
1
8
  export declare const ChildOf: import("koota").Relation<import("koota").Trait<Record<string, never>>>;
2
9
  export declare const SubEntityLinkType: {
3
10
  readonly HoverLink: "HoverLink";
@@ -1,5 +1,12 @@
1
1
  import { relation } from 'koota';
2
- export const ChildOf = relation({ exclusive: true, autoDestroy: 'orphan' });
2
+ /**
3
+ * Parent → child hierarchy relation. `exclusive: true` because each entity has
4
+ * at most one parent. Cascade-on-orphan is intentionally OFF: when a parent is
5
+ * destroyed, children survive and gain an `Orphan(parentName)` trait so they
6
+ * reattach if a frame with that name reappears. Sub-trees that should be torn
7
+ * down together (e.g. model roots + assets) call `destroyEntityTree` instead.
8
+ */
9
+ export const ChildOf = relation({ exclusive: true });
3
10
  export const SubEntityLinkType = {
4
11
  HoverLink: 'HoverLink',
5
12
  };
@@ -1,10 +1,17 @@
1
1
  import type { GLTF as ThreeGltf } from 'three/examples/jsm/loaders/GLTFLoader.js';
2
2
  import { Geometry as ViamGeometry } from '@viamrobotics/sdk';
3
- import { type ConfigurableTrait, type Entity } from 'koota';
3
+ import { type Entity } from 'koota';
4
4
  import { BufferGeometry as ThreeBufferGeometry } from 'three';
5
5
  export declare const Name: import("koota").Trait<() => string>;
6
- export declare const Parent: import("koota").Trait<() => string>;
7
6
  export declare const UUID: import("koota").Trait<() => string>;
7
+ /**
8
+ * Set on an entity whose desired parent (by name) doesn't yet exist in the
9
+ * world. Replaced with `relations.ChildOf(parentEntity)` once a frame with
10
+ * the matching `Name` is added. Managed by the hierarchy module — call sites
11
+ * should use `hierarchy.setParent` / `hierarchy.parentTraits` rather than
12
+ * adding this trait directly.
13
+ */
14
+ export declare const Orphan: import("koota").Trait<() => string>;
8
15
  export declare const Pose: import("koota").Trait<{
9
16
  x: number;
10
17
  y: number;
@@ -223,6 +230,4 @@ export declare const Geometry: (geometry: ViamGeometry) => import("koota").Trait
223
230
  }>, Partial<{
224
231
  r: number;
225
232
  }>] | [import("koota").Trait<() => ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>>, ThreeBufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>];
226
- export declare const getParentTrait: (parent: string | undefined) => ConfigurableTrait[];
227
- export declare const setParentTrait: (entity: Entity, parent: string | undefined) => void;
228
233
  export declare const updateGeometryTrait: (entity: Entity, geometry?: ViamGeometry) => void;
@@ -7,8 +7,15 @@ import { createBox, createCapsule, createSphere } from '../geometry';
7
7
  import { parsePcdInWorker } from '../loaders/pcd';
8
8
  import { parsePlyInput } from '../ply';
9
9
  export const Name = trait(() => '');
10
- export const Parent = trait(() => 'world');
11
10
  export const UUID = trait(() => '');
11
+ /**
12
+ * Set on an entity whose desired parent (by name) doesn't yet exist in the
13
+ * world. Replaced with `relations.ChildOf(parentEntity)` once a frame with
14
+ * the matching `Name` is added. Managed by the hierarchy module — call sites
15
+ * should use `hierarchy.setParent` / `hierarchy.parentTraits` rather than
16
+ * adding this trait directly.
17
+ */
18
+ export const Orphan = trait(() => '');
12
19
  export const Pose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
13
20
  export const EditedPose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
14
21
  export const LivePose = trait({ x: 0, y: 0, z: 0, oX: 0, oY: 0, oZ: 1, theta: 0 });
@@ -161,19 +168,6 @@ export const Geometry = (geometry) => {
161
168
  }
162
169
  return ReferenceFrame;
163
170
  };
164
- export const getParentTrait = (parent) => !parent || parent === 'world' ? [] : [Parent(parent)];
165
- export const setParentTrait = (entity, parent) => {
166
- if (!parent || parent === 'world') {
167
- entity.remove(Parent);
168
- return;
169
- }
170
- if (entity.has(Parent)) {
171
- entity.set(Parent, parent);
172
- }
173
- else {
174
- entity.add(Parent(parent));
175
- }
176
- };
177
171
  export const updateGeometryTrait = (entity, geometry) => {
178
172
  if (!geometry) {
179
173
  entity.remove(Box, Capsule, Sphere, BufferGeometry);
@@ -0,0 +1,11 @@
1
+ import type { Entity } from 'koota';
2
+ /**
3
+ * Reactive view of an entity's parent name — the string consumed by Threlte
4
+ * `<Portal id={...}>` and other lookups that key off the parent's `Name`.
5
+ *
6
+ * Reads through `ChildOf` to the parent's `Name` when the parent is alive,
7
+ * else falls back to `Orphan(parentName)`.
8
+ */
9
+ export declare const useParentName: (target: () => Entity | undefined) => {
10
+ readonly current: string | undefined;
11
+ };
@@ -0,0 +1,21 @@
1
+ import { ChildOf } from './relations';
2
+ import { Name, Orphan } from './traits';
3
+ import { useTarget } from './useTarget.svelte';
4
+ import { useTrait } from './useTrait.svelte';
5
+ /**
6
+ * Reactive view of an entity's parent name — the string consumed by Threlte
7
+ * `<Portal id={...}>` and other lookups that key off the parent's `Name`.
8
+ *
9
+ * Reads through `ChildOf` to the parent's `Name` when the parent is alive,
10
+ * else falls back to `Orphan(parentName)`.
11
+ */
12
+ export const useParentName = (target) => {
13
+ const parent = useTarget(target, ChildOf);
14
+ const parentName = useTrait(() => parent.current, Name);
15
+ const orphan = useTrait(target, Orphan);
16
+ return {
17
+ get current() {
18
+ return parentName.current || orphan.current || undefined;
19
+ },
20
+ };
21
+ };
@@ -0,0 +1,10 @@
1
+ import { type Entity, type Relation, type Trait, type World } from 'koota';
2
+ /**
3
+ * Reactive view of an entity's target for an exclusive relation. Mirrors the
4
+ * forthcoming `useTarget` from the upstream `@koota/svelte` package — kept
5
+ * here until that package is published. See:
6
+ * https://github.com/michealparks/koota/tree/svelte/packages/svelte
7
+ */
8
+ export declare const useTarget: <T extends Trait>(target: () => Entity | World | undefined | null, relation: Relation<T>) => {
9
+ readonly current: Entity | undefined;
10
+ };
@@ -0,0 +1,42 @@
1
+ import { $internal as internal } from 'koota';
2
+ import { isWorld } from './useTrait.svelte';
3
+ import { useWorld } from './useWorld';
4
+ /**
5
+ * Reactive view of an entity's target for an exclusive relation. Mirrors the
6
+ * forthcoming `useTarget` from the upstream `@koota/svelte` package — kept
7
+ * here until that package is published. See:
8
+ * https://github.com/michealparks/koota/tree/svelte/packages/svelte
9
+ */
10
+ export const useTarget = (target, relation) => {
11
+ const contextWorld = useWorld();
12
+ const targetEntity = $derived(target());
13
+ const world = $derived(isWorld(targetEntity) ? targetEntity : contextWorld);
14
+ const entity = $derived(isWorld(targetEntity) ? targetEntity[internal].worldEntity : targetEntity);
15
+ let value = $derived(entity?.targetFor(relation));
16
+ $effect(() => {
17
+ if (!entity)
18
+ return;
19
+ const onAddUnsub = world.onAdd(relation, (e) => {
20
+ if (e === entity)
21
+ value = entity.targetFor(relation);
22
+ });
23
+ const onRemoveUnsub = world.onRemove(relation, (e) => {
24
+ if (e === entity)
25
+ value = undefined;
26
+ });
27
+ const onChangeUnsub = world.onChange(relation, (e) => {
28
+ if (e === entity)
29
+ value = entity.targetFor(relation);
30
+ });
31
+ return () => {
32
+ onAddUnsub();
33
+ onRemoveUnsub();
34
+ onChangeUnsub();
35
+ };
36
+ });
37
+ return {
38
+ get current() {
39
+ return value;
40
+ },
41
+ };
42
+ };
@@ -1,4 +1,4 @@
1
- import { traits } from '../ecs';
1
+ import { hierarchy, traits } from '../ecs';
2
2
  import { isFinitePose } from '../transform';
3
3
  const captureGeometry = (entity) => {
4
4
  const box = entity.get(traits.Box);
@@ -76,7 +76,7 @@ export class FrameEditSession {
76
76
  continue;
77
77
  this.snapshots.set(entity, {
78
78
  name,
79
- parent: entity.get(traits.Parent) ?? 'world',
79
+ parent: hierarchy.getParentName(entity) ?? 'world',
80
80
  editedPose: { ...editedPose },
81
81
  geometry: captureGeometry(entity),
82
82
  });
@@ -97,7 +97,7 @@ export class FrameEditSession {
97
97
  return;
98
98
  const next = { ...current, ...pose };
99
99
  entity.set(traits.EditedPose, next);
100
- this.updateFrame(snap.name, entity.get(traits.Parent) ?? 'world', next, liveGeometry(entity));
100
+ this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', next, liveGeometry(entity));
101
101
  };
102
102
  stageGeometry = (entity, geometry) => {
103
103
  const snap = this.snapshots.get(entity);
@@ -118,14 +118,14 @@ export class FrameEditSession {
118
118
  }
119
119
  const editedPose = entity.get(traits.EditedPose);
120
120
  if (editedPose) {
121
- this.updateFrame(snap.name, entity.get(traits.Parent) ?? 'world', editedPose, geometry);
121
+ this.updateFrame(snap.name, hierarchy.getParentName(entity) ?? 'world', editedPose, geometry);
122
122
  }
123
123
  };
124
124
  stageParent = (entity, parent) => {
125
125
  const snap = this.snapshots.get(entity);
126
126
  if (!snap || this.#closed)
127
127
  return;
128
- traits.setParentTrait(entity, parent === 'world' ? undefined : parent);
128
+ hierarchy.setParent(entity, parent === 'world' ? undefined : parent);
129
129
  const editedPose = entity.get(traits.EditedPose);
130
130
  if (editedPose) {
131
131
  this.updateFrame(snap.name, parent, editedPose, liveGeometry(entity));
@@ -164,7 +164,7 @@ export class FrameEditSession {
164
164
  for (const [entity, snap] of this.snapshots) {
165
165
  if (entity.isAlive()) {
166
166
  entity.set(traits.EditedPose, snap.editedPose);
167
- traits.setParentTrait(entity, snap.parent === 'world' ? undefined : snap.parent);
167
+ hierarchy.setParent(entity, snap.parent === 'world' ? undefined : snap.parent);
168
168
  restoreGeometryTrait(entity, snap.geometry);
169
169
  }
170
170
  this.updateFrame(snap.name, snap.parent, snap.editedPose, snapshotToFrameGeometry(snap.geometry));
@@ -9,14 +9,18 @@ export const provideArmKinematics = (partID) => {
9
9
  const names = $derived(arms.current.map((arm) => arm.name));
10
10
  const clients = $derived(arms.current.map((arm) => createResourceClient(ArmClient, partID, () => arm.name)));
11
11
  const kinematicsQueries = $derived(clients.map((client) => [client.current?.name, createResourceQuery(client, 'getKinematics', () => options)]));
12
- const kinematics = $derived(Object.fromEntries(kinematicsQueries.map(([name, query]) => [
13
- name,
14
- query.data?.joints.map((j) => ({
15
- id: j.id,
16
- min: j.min,
17
- max: j.max,
18
- })),
19
- ])));
12
+ const kinematics = $derived(Object.fromEntries(kinematicsQueries.map(([name, query]) => {
13
+ const data = query.data;
14
+ const joints = data && ('joints' in data ? data.joints : data.kinematicsData.joints);
15
+ return [
16
+ name,
17
+ joints?.map((j) => ({
18
+ id: j.id,
19
+ min: j.min,
20
+ max: j.max,
21
+ })),
22
+ ];
23
+ })));
20
24
  setContext(key, {
21
25
  get names() {
22
26
  return names;
@@ -8,7 +8,7 @@ import { UuidTool } from 'uuid-tool';
8
8
  import { createBufferGeometry, updateBufferGeometry } from '../attribute';
9
9
  import { ColorFormat } from '../buf/draw/v1/metadata_pb';
10
10
  import { asRGB, STRIDE } from '../buffer';
11
- import { traits, useWorld } from '../ecs';
11
+ import { hierarchy, traits, useWorld } from '../ecs';
12
12
  import { createBox, createCapsule, createSphere } from '../geometry';
13
13
  import { parsePlyInput } from '../ply';
14
14
  import { createPose, createPoseFromFrame } from '../transform';
@@ -127,7 +127,7 @@ export const provideDrawAPI = () => {
127
127
  const existing = entities.get(name);
128
128
  if (existing) {
129
129
  existing.set(traits.Pose, pose);
130
- traits.setParentTrait(existing, parent);
130
+ hierarchy.setParent(existing, parent);
131
131
  continue;
132
132
  }
133
133
  const geometryTrait = () => {
@@ -142,7 +142,7 @@ export const provideDrawAPI = () => {
142
142
  }
143
143
  return traits.ReferenceFrame;
144
144
  };
145
- const entityTraits = [...traits.getParentTrait(parent)];
145
+ const entityTraits = [...hierarchy.parentTraits(parent)];
146
146
  if (frame.geometry) {
147
147
  entityTraits.push(geometryTrait());
148
148
  }
@@ -178,7 +178,7 @@ export const provideDrawAPI = () => {
178
178
  };
179
179
  const entityTraits = [
180
180
  traits.Name(data.label ?? ++geometryIndex),
181
- ...traits.getParentTrait(parent),
181
+ ...hierarchy.parentTraits(parent),
182
182
  traits.Pose(pose),
183
183
  traits.Color(colorUtil.set(color)),
184
184
  geometryTrait(),
@@ -3,7 +3,7 @@ import { createRobotQuery, useConnectionStatus, useMachineStatus, useRobotClient
3
3
  import {} from 'koota';
4
4
  import { getContext, setContext, untrack } from 'svelte';
5
5
  import { resourceNameToColor, subtypeToColor } from '../color';
6
- import { traits, useWorld } from '../ecs';
6
+ import { hierarchy, traits, useWorld } from '../ecs';
7
7
  import { createPose } from '../transform';
8
8
  import { useConfigFrames } from './useConfigFrames.svelte';
9
9
  import { useEnvironment } from './useEnvironment.svelte';
@@ -169,7 +169,7 @@ export const provideFrames = (partID) => {
169
169
  if (editSession.current?.owns(existing)) {
170
170
  continue;
171
171
  }
172
- traits.setParentTrait(existing, parent);
172
+ hierarchy.setParent(existing, parent);
173
173
  if (color) {
174
174
  existing.set(traits.Color, color);
175
175
  }
@@ -202,7 +202,7 @@ export const provideFrames = (partID) => {
202
202
  traits.FramesAPI,
203
203
  traits.Transformable,
204
204
  traits.ShowAxesHelper,
205
- ...traits.getParentTrait(parent),
205
+ ...hierarchy.parentTraits(parent),
206
206
  ];
207
207
  if (color) {
208
208
  entityTraits.push(traits.Color(color));