@viamrobotics/motion-tools 1.28.1 → 1.29.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 (47) hide show
  1. package/dist/components/App.svelte +3 -12
  2. package/dist/components/App.svelte.d.ts +0 -2
  3. package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
  4. package/dist/components/Entities/Frame.svelte +1 -1
  5. package/dist/components/Entities/GLTF.svelte +1 -1
  6. package/dist/components/Entities/Geometry.svelte +1 -1
  7. package/dist/components/Entities/Line.svelte +1 -1
  8. package/dist/components/Entities/Points.svelte +1 -1
  9. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +1 -1
  10. package/dist/components/SceneProviders.svelte +3 -4
  11. package/dist/components/SelectedTransformControls.svelte +25 -16
  12. package/dist/components/overlay/AddRelationship.svelte +6 -20
  13. package/dist/components/overlay/Details.svelte +2 -12
  14. package/dist/components/overlay/left-pane/TreeNode.svelte +3 -2
  15. package/dist/ecs/traits.d.ts +7 -0
  16. package/dist/ecs/traits.js +7 -0
  17. package/dist/hooks/useInheritedInvisible.svelte.d.ts +3 -0
  18. package/dist/hooks/useInheritedInvisible.svelte.js +89 -0
  19. package/dist/index.d.ts +0 -3
  20. package/dist/index.js +0 -3
  21. package/dist/plugins/DrawService/DrawService.svelte +18 -0
  22. package/dist/plugins/DrawService/DrawService.svelte.d.ts +7 -0
  23. package/dist/plugins/DrawService/serverRelationships.d.ts +14 -0
  24. package/dist/plugins/DrawService/serverRelationships.js +105 -0
  25. package/dist/{hooks → plugins/DrawService}/useDrawAPI.svelte.js +9 -9
  26. package/dist/{hooks → plugins/DrawService}/useDrawService.svelte.d.ts +0 -2
  27. package/dist/{hooks → plugins/DrawService}/useDrawService.svelte.js +19 -45
  28. package/dist/{components/Selection/Tool.svelte → plugins/Selection/SelectionTool.svelte} +3 -3
  29. package/dist/{components/Selection/Tool.svelte.d.ts → plugins/Selection/SelectionTool.svelte.d.ts} +3 -3
  30. package/dist/plugins/index.d.ts +4 -0
  31. package/dist/plugins/index.js +6 -0
  32. package/package.json +2 -1
  33. /package/dist/{hooks → plugins/DrawService}/useDrawAPI.svelte.d.ts +0 -0
  34. /package/dist/{hooks → plugins/DrawService}/useDrawConnectionConfig.svelte.d.ts +0 -0
  35. /package/dist/{hooks → plugins/DrawService}/useDrawConnectionConfig.svelte.js +0 -0
  36. /package/dist/{components → plugins}/Selection/Debug.svelte +0 -0
  37. /package/dist/{components → plugins}/Selection/Debug.svelte.d.ts +0 -0
  38. /package/dist/{components → plugins}/Selection/Ellipse.svelte +0 -0
  39. /package/dist/{components → plugins}/Selection/Ellipse.svelte.d.ts +0 -0
  40. /package/dist/{components → plugins}/Selection/Lasso.svelte +0 -0
  41. /package/dist/{components → plugins}/Selection/Lasso.svelte.d.ts +0 -0
  42. /package/dist/{components → plugins}/Selection/traits.d.ts +0 -0
  43. /package/dist/{components → plugins}/Selection/traits.js +0 -0
  44. /package/dist/{components → plugins}/Selection/useSelectionPlugin.svelte.d.ts +0 -0
  45. /package/dist/{components → plugins}/Selection/useSelectionPlugin.svelte.js +0 -0
  46. /package/dist/{components → plugins}/Selection/utils.d.ts +0 -0
  47. /package/dist/{components → plugins}/Selection/utils.js +0 -0
@@ -8,6 +8,7 @@
8
8
  import { PortalTarget } from '@threlte/extras'
9
9
  import { useXR } from '@threlte/xr'
10
10
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
11
+ import { primeTheme } from '@viamrobotics/tweakpane-config'
11
12
  import { ThemeUtils } from 'svelte-tweakpane-ui'
12
13
 
13
14
  import Controls from './overlay/controls/Controls.svelte'
@@ -18,10 +19,6 @@
18
19
  import XR from './xr/XR.svelte'
19
20
  import { provideWorld } from '../ecs'
20
21
  import { type CameraPose, provideCameraControls } from '../hooks/useControls.svelte'
21
- import {
22
- type DrawConnectionConfig,
23
- provideDrawConnectionConfig,
24
- } from '../hooks/useDrawConnectionConfig.svelte'
25
22
  import { provideEnvironment } from '../hooks/useEnvironment.svelte'
26
23
  import { providePartConfig } from '../hooks/usePartConfig.svelte'
27
24
  import { createPartIDContext } from '../hooks/usePartID.svelte'
@@ -51,7 +48,6 @@
51
48
  partID?: string
52
49
  inputBindingsEnabled?: boolean
53
50
  localConfigProps?: LocalConfigProps
54
- drawConnectionConfig?: DrawConnectionConfig
55
51
 
56
52
  /**
57
53
  * Snippet for THREE objects
@@ -79,7 +75,6 @@
79
75
  inputBindingsEnabled = true,
80
76
  localConfigProps,
81
77
  cameraPose,
82
- drawConnectionConfig,
83
78
  children: appChildren,
84
79
  dashboard,
85
80
  details,
@@ -95,7 +90,7 @@
95
90
 
96
91
  provideCameraControls(() => cameraPose)
97
92
  createPartIDContext(() => partID)
98
- provideDrawConnectionConfig(() => drawConnectionConfig)
93
+
99
94
  provideWeblabs()
100
95
  provideToast()
101
96
 
@@ -112,11 +107,7 @@
112
107
  })
113
108
 
114
109
  $effect(() => {
115
- ThemeUtils.setGlobalDefaultTheme({
116
- ...ThemeUtils.presets.light,
117
- baseBackgroundColor: '#fbfbfc',
118
- baseShadowColor: 'transparent',
119
- })
110
+ ThemeUtils.setGlobalDefaultTheme(primeTheme)
120
111
  })
121
112
  </script>
122
113
 
@@ -2,7 +2,6 @@ import type { Struct } from '@viamrobotics/sdk';
2
2
  import type { Entity } from 'koota';
3
3
  import type { Snippet } from 'svelte';
4
4
  import { type CameraPose } from '../hooks/useControls.svelte';
5
- import { type DrawConnectionConfig } from '../hooks/useDrawConnectionConfig.svelte';
6
5
  interface LocalConfigProps {
7
6
  current: Struct;
8
7
  isDirty: boolean;
@@ -13,7 +12,6 @@ interface Props {
13
12
  partID?: string;
14
13
  inputBindingsEnabled?: boolean;
15
14
  localConfigProps?: LocalConfigProps;
16
- drawConnectionConfig?: DrawConnectionConfig;
17
15
  /**
18
16
  * Snippet for THREE objects
19
17
  */
@@ -20,7 +20,7 @@
20
20
 
21
21
  const { invalidate } = useThrelte()
22
22
  const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
23
- const invisible = useTrait(() => entity, traits.Invisible)
23
+ const invisible = useTrait(() => entity, traits.InheritedInvisible)
24
24
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
25
25
 
26
26
  const events = useEntityEvents(() => entity)
@@ -39,7 +39,7 @@ Renders a Viam Frame object
39
39
  const entityColor = useTrait(() => entity, traits.Color)
40
40
  const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
41
41
  const center = useTrait(() => entity, traits.Center)
42
- const invisible = useTrait(() => entity, traits.Invisible)
42
+ const invisible = useTrait(() => entity, traits.InheritedInvisible)
43
43
 
44
44
  const events = useEntityEvents(() => entity)
45
45
 
@@ -35,7 +35,7 @@
35
35
 
36
36
  const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
37
37
  const gltfTrait = useTrait(() => entity, traits.GLTF)
38
- const invisible = useTrait(() => entity, traits.Invisible)
38
+ const invisible = useTrait(() => entity, traits.InheritedInvisible)
39
39
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
40
40
  const events = useEntityEvents(() => entity)
41
41
 
@@ -33,7 +33,7 @@ Renders a Viam Geometry object
33
33
  const name = useTrait(() => entity, traits.Name)
34
34
  const worldMatrix = useTrait(() => entity, traits.WorldMatrix)
35
35
  const center = useTrait(() => entity, traits.Center)
36
- const invisible = useTrait(() => entity, traits.Invisible)
36
+ const invisible = useTrait(() => entity, traits.InheritedInvisible)
37
37
 
38
38
  const model = $derived.by(() => {
39
39
  if (!settings.current.renderArmModels.includes('model')) {
@@ -34,7 +34,7 @@
34
34
  const renderOrder = useTrait(() => entity, traits.RenderOrder)
35
35
  const opacity = useTrait(() => entity, traits.Opacity)
36
36
  const screenSpace = useTrait(() => entity, traits.ScreenSpace)
37
- const invisible = useTrait(() => entity, traits.Invisible)
37
+ const invisible = useTrait(() => entity, traits.InheritedInvisible)
38
38
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
39
39
 
40
40
  const events = useEntityEvents(() => entity)
@@ -28,7 +28,7 @@
28
28
  const colors = useTrait(() => entity, traits.Colors)
29
29
  const entityPointSize = useTrait(() => entity, traits.PointSize)
30
30
  const opacity = useTrait(() => entity, traits.Opacity)
31
- const invisible = useTrait(() => entity, traits.Invisible)
31
+ const invisible = useTrait(() => entity, traits.InheritedInvisible)
32
32
  const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
33
33
  const renderOrder = useTrait(() => entity, traits.RenderOrder)
34
34
  const materialProps = useTrait(() => entity, traits.Material)
@@ -18,7 +18,7 @@ export const useEntityEvents = (entity) => {
18
18
  const selectedEntity = useSelectedEntity();
19
19
  const focusedEntity = useFocusedEntity();
20
20
  const cursor = useCursor();
21
- const invisible = useTrait(entity, traits.Invisible);
21
+ const invisible = useTrait(entity, traits.InheritedInvisible);
22
22
  const onpointerenter = (event) => {
23
23
  if (invisible.current)
24
24
  return;
@@ -7,12 +7,11 @@
7
7
  import { provideArmKinematics } from '../hooks/useArmKinematics.svelte'
8
8
  import { provideConfigFrames } from '../hooks/useConfigFrames.svelte'
9
9
  import { provideTransformControls } from '../hooks/useControls.svelte'
10
- import { provideDrawAPI } from '../hooks/useDrawAPI.svelte'
11
- import { provideDrawService } from '../hooks/useDrawService.svelte'
12
10
  import { provideFrameEditSession } from '../hooks/useFrameEditSession.svelte'
13
11
  import { provideFramelessComponents } from '../hooks/useFramelessComponents.svelte'
14
12
  import { provideFrames } from '../hooks/useFrames.svelte'
15
13
  import { provideGeometries } from '../hooks/useGeometries.svelte'
14
+ import { provideInheritedInvisible } from '../hooks/useInheritedInvisible.svelte'
16
15
  import { provideLinkedEntities } from '../hooks/useLinked.svelte'
17
16
  import { provideLogs } from '../hooks/useLogs.svelte'
18
17
  import { usePartID } from '../hooks/usePartID.svelte'
@@ -38,10 +37,10 @@
38
37
 
39
38
  provideHierarchy()
40
39
  provideWorldMatrix()
40
+ provideInheritedInvisible()
41
41
  provideOrigin()
42
- provideDrawAPI()
42
+
43
43
  provideRelationships()
44
- provideDrawService()
45
44
 
46
45
  provideResourceByName(() => partID.current)
47
46
  provideConfigFrames()
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { FrameEditSession } from '../editing/FrameEditSession'
6
6
 
7
- import { traits, useTrait } from '../ecs'
7
+ import { relations, traits, useTrait } from '../ecs'
8
8
  import { useTransformControls } from '../hooks/useControls.svelte'
9
9
  import { useEnvironment } from '../hooks/useEnvironment.svelte'
10
10
  import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
@@ -60,6 +60,7 @@
60
60
  const refPose = createPose()
61
61
  const tempRefMatrix = new Matrix4()
62
62
  const tempEditedMatrix = new Matrix4()
63
+ const tempParentInverse = new Matrix4()
63
64
  const tempPose = createPose()
64
65
 
65
66
  let session: FrameEditSession | undefined
@@ -183,30 +184,40 @@
183
184
  }
184
185
 
185
186
  /**
186
- * Frame.svelte renders frame entities by blending M(live) × M(config)⁻¹ × M(edited)
187
- * so for the user's drag to render where they pulled the gizmo to,
188
- * EditedMatrix must satisfy M(edited) = M(config) × M(live)⁻¹ × M(ref)
189
- * where M(ref) is the gizmo-driven group's parent-relative matrix in mm.
187
+ * Frame.svelte renders frame entities by writing the entity's WorldMatrix
188
+ * into group.matrix and decomposing it into position/quaternion. The gizmo's
189
+ * Three.js parent has identity world, so `ref.position` / `ref.quaternion`
190
+ * are world-space values. Matrix and EditedMatrix store local-to-parent
191
+ * transforms, so we left-multiply by the parent's inverted WorldMatrix
192
+ * before staging — otherwise WorldMatrix recomposition (parent × edited)
193
+ * re-applies the parent's rotation/translation and the frame ends up at
194
+ * parent × where-the-user-pulled-it.
190
195
  *
191
- * When live config (no kinematic offset), this collapses to
192
- * M(edited) = M(ref) the same as the naive writeback. When they diverge
193
- * (e.g. an arm whose joints have moved away from its config pose), this
194
- * composition is what keeps the rendering anchored to the user's pointer
195
- * instead of shearing through the live × baseline⁻¹ offset.
196
+ * With a kinematic offset (LiveMatrix + Matrix both present), the local
197
+ * target M(local) feeds solveEditedMatrix to back out the EditedMatrix
198
+ * that satisfies live × baseline⁻¹ × edited = local.
196
199
  */
197
-
198
200
  const stageFrameTransform = () => {
199
201
  if (!ref || !entity) return
200
202
 
201
- vector3ToPose(ref.position, refPose)
202
- quaternionToPose(ref.quaternion, refPose)
203
+ tempRefMatrix.makeRotationFromQuaternion(ref.quaternion)
204
+ tempRefMatrix.setPosition(ref.position)
205
+
206
+ const parentEntity = entity.targetFor(relations.ChildOf)
207
+ const parentWorld = parentEntity?.get(traits.WorldMatrix)
208
+ if (parentWorld) {
209
+ tempParentInverse.copy(parentWorld).invert()
210
+ tempRefMatrix.premultiply(tempParentInverse)
211
+ }
212
+
213
+ matrixToPose(tempRefMatrix, refPose)
203
214
 
204
215
  const live = liveMatrix.current
205
216
  const config = configMatrix.current
206
217
 
207
218
  if (!live || !config) {
208
219
  // No live matrix available — Frame.svelte's blend short-circuits to
209
- // editedMatrix, so naive writeback is correct.
220
+ // editedMatrix, so the parent-relative target is what we stage.
210
221
  if (activeMode === 'translate') {
211
222
  session?.stagePose(entity, {
212
223
  x: refPose.x,
@@ -224,8 +235,6 @@
224
235
  return
225
236
  }
226
237
 
227
- poseToMatrix(refPose, tempRefMatrix)
228
-
229
238
  solveEditedMatrix(config, live, tempRefMatrix, tempEditedMatrix)
230
239
  matrixToPose(tempEditedMatrix, tempPose)
231
240
  session?.stagePose(entity, { ...tempPose })
@@ -4,14 +4,12 @@
4
4
 
5
5
  import { relations, traits, useQuery, useTrait } from '../../ecs'
6
6
  import { SubEntityLinkType } from '../../ecs/relations'
7
- import { useDrawService } from '../../hooks/useDrawService.svelte'
8
7
 
9
8
  interface Props {
10
9
  entity: Entity | undefined
11
10
  }
12
11
 
13
12
  const { entity }: Props = $props()
14
- const drawService = useDrawService()
15
13
 
16
14
  const allEntities = useQuery(traits.Name)
17
15
  const name = useTrait(() => entity, traits.Name)
@@ -55,24 +53,12 @@
55
53
  )
56
54
  if (!selectedEntity) return
57
55
 
58
- if (isServiceManaged) {
59
- const sourceUuid = entity.get(traits.UUID)
60
- const targetUuid = selectedEntity.get(traits.UUID)
61
- if (!sourceUuid || !targetUuid) return
62
- void drawService.createRelationship(
63
- sourceUuid,
64
- targetUuid,
65
- linkType ?? '',
66
- relationshipFormula
67
- )
68
- } else {
69
- entity.add(
70
- relations.SubEntityLink(selectedEntity, {
71
- indexMapping: relationshipFormula,
72
- type: linkType,
73
- })
74
- )
75
- }
56
+ entity.add(
57
+ relations.SubEntityLink(selectedEntity, {
58
+ indexMapping: relationshipFormula,
59
+ type: linkType,
60
+ })
61
+ )
76
62
  showRelationshipOptions = false
77
63
  resetForm()
78
64
  }
@@ -41,7 +41,6 @@
41
41
  import { FrameConfigUpdater } from '../../FrameConfigUpdater.svelte'
42
42
  import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
43
43
  import { useCameraControls } from '../../hooks/useControls.svelte'
44
- import { useDrawService } from '../../hooks/useDrawService.svelte'
45
44
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
46
45
  import { useLinkedEntities } from '../../hooks/useLinked.svelte'
47
46
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
@@ -64,7 +63,6 @@
64
63
 
65
64
  const world = useWorld()
66
65
  const { invalidate } = useThrelte()
67
- const drawService = useDrawService()
68
66
  const controls = useCameraControls()
69
67
  const resourceByName = useResourceByName()
70
68
  const configFrames = useConfigFrames()
@@ -309,7 +307,7 @@
309
307
  {#if entity}
310
308
  <div
311
309
  id="details-panel"
312
- class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 w-70 border p-2 text-xs dark:text-white"
310
+ class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 w-70 border p-2 text-xs"
313
311
  use:draggable={{
314
312
  bounds: 'body',
315
313
  handle: dragElement,
@@ -743,15 +741,7 @@
743
741
  <Icon
744
742
  name="trash-can-outline"
745
743
  class="h-6 cursor-pointer px-2 py-1 text-xs text-red-500"
746
- onclick={() => {
747
- const sourceUuid = entity.get(traits.UUID)
748
- const targetUuid = linkedEntity.get(traits.UUID)
749
- if (sourceUuid && targetUuid) {
750
- void drawService.deleteRelationship(sourceUuid, targetUuid)
751
- } else {
752
- entity.remove(relations.SubEntityLink(linkedEntity))
753
- }
754
- }}
744
+ onclick={() => entity.remove(relations.SubEntityLink(linkedEntity))}
755
745
  />
756
746
  </div>
757
747
  {/each}
@@ -20,6 +20,7 @@
20
20
 
21
21
  const name = useTrait(() => node.entity, traits.Name)
22
22
  const invisible = useTrait(() => node.entity, traits.Invisible)
23
+ const inheritedInvisible = useTrait(() => node.entity, traits.InheritedInvisible)
23
24
  const chunkProgress = useTrait(() => node.entity, traits.ChunkProgress)
24
25
  const loading = $derived(chunkProgress.current !== undefined)
25
26
  const progress = $derived(
@@ -55,7 +56,7 @@
55
56
  class={[
56
57
  'w-full',
57
58
  {
58
- 'text-disabled': invisible.current,
59
+ 'text-disabled': inheritedInvisible.current,
59
60
  'bg-medium': nodeState.selected,
60
61
  sticky: true,
61
62
  },
@@ -129,7 +130,7 @@
129
130
  <div
130
131
  class={{
131
132
  'flex justify-between': true,
132
- 'text-disabled': invisible.current,
133
+ 'text-disabled': inheritedInvisible.current,
133
134
  'bg-medium': nodeState.selected,
134
135
  }}
135
136
  {...api.getItemProps(nodeProps)}
@@ -65,6 +65,13 @@ export declare const InstancedMatrix: import("koota").Trait<() => {
65
65
  }>;
66
66
  export declare const Hovered: import("koota").Trait<() => boolean>;
67
67
  export declare const Invisible: import("koota").Trait<() => boolean>;
68
+ /**
69
+ * True when the entity itself, or any of its parents up the `ChildOf`
70
+ * chain, has `Invisible`. Maintained by `provideInheritedInvisible`;
71
+ * don't add or remove it by hand — toggle `Invisible` and the cascade
72
+ * follows.
73
+ */
74
+ export declare const InheritedInvisible: import("koota").Trait<() => boolean>;
68
75
  /**
69
76
  * Represents that an entity is composed of many instances, so that the treeview and
70
77
  * details panel may display all instances
@@ -61,6 +61,13 @@ export const InstancedMatrix = trait(() => ({
61
61
  }));
62
62
  export const Hovered = trait(() => true);
63
63
  export const Invisible = trait(() => true);
64
+ /**
65
+ * True when the entity itself, or any of its parents up the `ChildOf`
66
+ * chain, has `Invisible`. Maintained by `provideInheritedInvisible`;
67
+ * don't add or remove it by hand — toggle `Invisible` and the cascade
68
+ * follows.
69
+ */
70
+ export const InheritedInvisible = trait(() => true);
64
71
  /**
65
72
  * Represents that an entity is composed of many instances, so that the treeview and
66
73
  * details panel may display all instances
@@ -0,0 +1,3 @@
1
+ import { type World } from 'koota';
2
+ export declare const addInheritedInvisibleListeners: (world: World) => () => void;
3
+ export declare const provideInheritedInvisible: () => void;
@@ -0,0 +1,89 @@
1
+ import {} from 'koota';
2
+ import { ChildOf } from '../ecs/relations';
3
+ import { InheritedInvisible, Invisible } from '../ecs/traits';
4
+ import { useWorld } from '../ecs/useWorld';
5
+ /**
6
+ * Walks up `ChildOf` and returns true if the entity itself or any
7
+ * ancestor has `Invisible`. Memoizes via `cache` so siblings in the
8
+ * same flush reuse a parent's result.
9
+ */
10
+ const hasInherited = (entity, cache) => {
11
+ const cached = cache.get(entity);
12
+ if (cached !== undefined)
13
+ return cached;
14
+ if (!entity.isAlive())
15
+ return false;
16
+ if (entity.has(Invisible)) {
17
+ cache.set(entity, true);
18
+ return true;
19
+ }
20
+ const parent = entity.targetFor(ChildOf);
21
+ const inherited = parent && parent.isAlive() ? hasInherited(parent, cache) : false;
22
+ cache.set(entity, inherited);
23
+ return inherited;
24
+ };
25
+ const flushDirty = (world, dirty) => {
26
+ if (dirty.size === 0)
27
+ return;
28
+ const cache = new Map();
29
+ const allEntities = new Set();
30
+ const collectChildren = (entity) => {
31
+ if (allEntities.has(entity))
32
+ return;
33
+ allEntities.add(entity);
34
+ for (const child of world.query(ChildOf(entity))) {
35
+ collectChildren(child);
36
+ }
37
+ };
38
+ for (const entity of dirty) {
39
+ collectChildren(entity);
40
+ }
41
+ dirty.clear();
42
+ for (const entity of allEntities) {
43
+ if (!entity.isAlive()) {
44
+ continue;
45
+ }
46
+ const hasInheritedTrait = hasInherited(entity, cache);
47
+ const hasTrait = entity.has(InheritedInvisible);
48
+ if (hasInheritedTrait && !hasTrait) {
49
+ entity.add(InheritedInvisible);
50
+ }
51
+ else if (!hasInheritedTrait && hasTrait) {
52
+ entity.remove(InheritedInvisible);
53
+ }
54
+ }
55
+ };
56
+ export const addInheritedInvisibleListeners = (world) => {
57
+ const dirty = new Set();
58
+ let scheduled = false;
59
+ const enqueue = (entity) => {
60
+ dirty.add(entity);
61
+ if (scheduled)
62
+ return;
63
+ scheduled = true;
64
+ // Microtask-deferred so a burst of changes is grouped into one subtree walk.
65
+ queueMicrotask(() => {
66
+ scheduled = false;
67
+ flushDirty(world, dirty);
68
+ });
69
+ };
70
+ for (const entity of world.query(Invisible)) {
71
+ enqueue(entity);
72
+ }
73
+ const unsubs = [
74
+ world.onAdd(Invisible, enqueue),
75
+ world.onRemove(Invisible, enqueue),
76
+ world.onAdd(ChildOf, enqueue),
77
+ world.onChange(ChildOf, enqueue),
78
+ world.onRemove(ChildOf, enqueue),
79
+ ];
80
+ return () => {
81
+ for (const unsub of unsubs) {
82
+ unsub();
83
+ }
84
+ };
85
+ };
86
+ export const provideInheritedInvisible = () => {
87
+ const world = useWorld();
88
+ $effect(() => addInheritedInvisibleListeners(world));
89
+ };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  /** @deprecated MotionTools has been renamed to Visualizer. This export will be removed in v2. */
2
2
  export { default as MotionTools } from './components/App.svelte';
3
3
  export { default as Visualizer } from './components/App.svelte';
4
- export { default as SelectionTool } from './components/Selection/Tool.svelte';
5
4
  export { default as PCD } from './components/PCD.svelte';
6
5
  export * as relations from './ecs/relations';
7
6
  export * as traits from './ecs/traits';
8
- export * as selectionTraits from './components/Selection/traits';
9
- export { useSelectionPlugin as useSelection } from './components/Selection/useSelectionPlugin.svelte';
10
7
  export { default as FloatingPanel } from './components/overlay/FloatingPanel.svelte';
11
8
  export { provideWorld, useWorld } from './ecs/useWorld';
12
9
  export { useQuery } from './ecs/useQuery.svelte';
package/dist/index.js CHANGED
@@ -2,13 +2,10 @@
2
2
  export { default as MotionTools } from './components/App.svelte';
3
3
  export { default as Visualizer } from './components/App.svelte';
4
4
  // Plugins
5
- export { default as SelectionTool } from './components/Selection/Tool.svelte';
6
5
  export { default as PCD } from './components/PCD.svelte';
7
6
  // ECS
8
7
  export * as relations from './ecs/relations';
9
8
  export * as traits from './ecs/traits';
10
- export * as selectionTraits from './components/Selection/traits';
11
- export { useSelectionPlugin as useSelection } from './components/Selection/useSelectionPlugin.svelte';
12
9
  export { default as FloatingPanel } from './components/overlay/FloatingPanel.svelte';
13
10
  export { provideWorld, useWorld } from './ecs/useWorld';
14
11
  export { useQuery } from './ecs/useQuery.svelte';
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { provideDrawAPI } from './useDrawAPI.svelte'
3
+ import {
4
+ type DrawConnectionConfig,
5
+ provideDrawConnectionConfig,
6
+ } from './useDrawConnectionConfig.svelte'
7
+ import { provideDrawService } from './useDrawService.svelte'
8
+
9
+ interface Props {
10
+ config: DrawConnectionConfig
11
+ }
12
+
13
+ let { config }: Props = $props()
14
+
15
+ provideDrawConnectionConfig(() => config)
16
+ provideDrawAPI()
17
+ provideDrawService()
18
+ </script>
@@ -0,0 +1,7 @@
1
+ import { type DrawConnectionConfig } from './useDrawConnectionConfig.svelte';
2
+ interface Props {
3
+ config: DrawConnectionConfig;
4
+ }
5
+ declare const DrawService: import("svelte").Component<Props, {}, "">;
6
+ type DrawService = ReturnType<typeof DrawService>;
7
+ export default DrawService;
@@ -0,0 +1,14 @@
1
+ import { type Entity } from 'koota';
2
+ import type { Relationship } from '../../metadata';
3
+ /**
4
+ * Diffs incoming server relationships against the last-known set per entity,
5
+ * so only links the draw service itself authored are added or removed.
6
+ * Client-added links (e.g. interactive HoverLinks from the Details overlay)
7
+ * are left alone.
8
+ */
9
+ export declare const createServerRelationships: () => {
10
+ apply(sourceEntity: Entity, sourceUuid: string, relationships: Relationship[] | undefined): void;
11
+ forget(sourceUuid: string): void;
12
+ reset(): void;
13
+ dispose(): void;
14
+ };
@@ -0,0 +1,105 @@
1
+ import { trait } from 'koota';
2
+ import { uuidBytesToString } from '../../draw';
3
+ import { relations, traits, useWorld } from '../../ecs';
4
+ /**
5
+ * Forward-references waiting on their target entity to arrive. Lives on the
6
+ * source entity so it dies with it — no plugin-side bookkeeping to leak when
7
+ * a source is destroyed before its targets show up.
8
+ */
9
+ const PendingLinks = trait(() => []);
10
+ /**
11
+ * Diffs incoming server relationships against the last-known set per entity,
12
+ * so only links the draw service itself authored are added or removed.
13
+ * Client-added links (e.g. interactive HoverLinks from the Details overlay)
14
+ * are left alone.
15
+ */
16
+ export const createServerRelationships = () => {
17
+ const world = useWorld();
18
+ const cache = new Map();
19
+ const lookupByUuid = (uuid) => world.query(traits.UUID).find((entity) => entity.get(traits.UUID) === uuid);
20
+ const enqueue = (sourceEntity, entry) => {
21
+ const existing = sourceEntity.get(PendingLinks) ?? [];
22
+ sourceEntity.set(PendingLinks, [...existing, entry]);
23
+ };
24
+ const unsubAdd = world.onAdd(traits.UUID, (target) => {
25
+ if (!target.isAlive())
26
+ return;
27
+ const targetUuid = target.get(traits.UUID);
28
+ if (!targetUuid)
29
+ return;
30
+ for (const source of world.query(PendingLinks)) {
31
+ if (!source.isAlive())
32
+ continue;
33
+ const queue = source.get(PendingLinks) ?? [];
34
+ const remaining = [];
35
+ let drained = false;
36
+ for (const entry of queue) {
37
+ if (entry.targetUuid === targetUuid) {
38
+ source.add(relations.SubEntityLink(target, { type: entry.type, indexMapping: entry.indexMapping }));
39
+ drained = true;
40
+ }
41
+ else {
42
+ remaining.push(entry);
43
+ }
44
+ }
45
+ if (!drained)
46
+ continue;
47
+ if (remaining.length === 0)
48
+ source.remove(PendingLinks);
49
+ else
50
+ source.set(PendingLinks, remaining);
51
+ }
52
+ });
53
+ return {
54
+ apply(sourceEntity, sourceUuid, relationships) {
55
+ const desired = new Map();
56
+ for (const relationship of relationships ?? []) {
57
+ const targetUuid = uuidBytesToString(relationship.targetUuid);
58
+ if (!targetUuid)
59
+ continue;
60
+ desired.set(targetUuid, {
61
+ targetUuid,
62
+ type: relationship.type,
63
+ indexMapping: relationship.indexMapping ?? 'index',
64
+ });
65
+ }
66
+ const previous = cache.get(sourceUuid) ?? new Map();
67
+ for (const targetUuid of previous.keys()) {
68
+ if (desired.has(targetUuid))
69
+ continue;
70
+ const target = lookupByUuid(targetUuid);
71
+ if (target?.isAlive()) {
72
+ sourceEntity.remove(relations.SubEntityLink(target));
73
+ }
74
+ }
75
+ for (const [targetUuid, link] of desired) {
76
+ const before = previous.get(targetUuid);
77
+ if (before?.type === link.type && before?.indexMapping === link.indexMapping) {
78
+ continue;
79
+ }
80
+ const target = lookupByUuid(targetUuid);
81
+ if (!target) {
82
+ enqueue(sourceEntity, link);
83
+ continue;
84
+ }
85
+ sourceEntity.add(relations.SubEntityLink(target, { type: link.type, indexMapping: link.indexMapping }));
86
+ }
87
+ if (desired.size === 0) {
88
+ cache.delete(sourceUuid);
89
+ }
90
+ else {
91
+ cache.set(sourceUuid, desired);
92
+ }
93
+ },
94
+ forget(sourceUuid) {
95
+ cache.delete(sourceUuid);
96
+ },
97
+ reset() {
98
+ cache.clear();
99
+ },
100
+ dispose() {
101
+ unsubAdd();
102
+ cache.clear();
103
+ },
104
+ };
105
+ };
@@ -5,16 +5,16 @@ import { Color, Matrix4, Vector3, Vector4 } from 'three';
5
5
  import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js';
6
6
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
7
7
  import { UuidTool } from 'uuid-tool';
8
- import { createBufferGeometry, updateBufferGeometry } from '../attribute';
9
- import { ColorFormat } from '../buf/draw/v1/metadata_pb';
10
- import { asRGB, STRIDE } from '../buffer';
11
- import { hierarchy, traits, useWorld } from '../ecs';
12
- import { createBox, createCapsule, createSphere } from '../geometry';
13
- import { parsePlyInput } from '../ply';
14
- import { createPose, createPoseFromFrame, poseToMatrix } from '../transform';
15
- import { useCameraControls } from './useControls.svelte';
8
+ import { createBufferGeometry, updateBufferGeometry } from '../../attribute';
9
+ import { ColorFormat } from '../../buf/draw/v1/metadata_pb';
10
+ import { asRGB, STRIDE } from '../../buffer';
11
+ import { hierarchy, traits, useWorld } from '../../ecs';
12
+ import { createBox, createCapsule, createSphere } from '../../geometry';
13
+ import { useCameraControls } from '../../hooks/useControls.svelte';
14
+ import { useLogs } from '../../hooks/useLogs.svelte';
15
+ import { parsePlyInput } from '../../ply';
16
+ import { createPose, createPoseFromFrame, poseToMatrix } from '../../transform';
16
17
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
17
- import { useLogs } from './useLogs.svelte';
18
18
  const colorUtil = new Color();
19
19
  const rgb = { r: 0, g: 0, b: 0 };
20
20
  const bufferTypes = {
@@ -6,8 +6,6 @@ declare const ConnectionStatus: {
6
6
  type ConnectionStatusType = (typeof ConnectionStatus)[keyof typeof ConnectionStatus];
7
7
  interface Context {
8
8
  connectionStatus: ConnectionStatusType;
9
- createRelationship: (sourceUuid: string, targetUuid: string, type: string, indexMapping?: string) => Promise<void>;
10
- deleteRelationship: (sourceUuid: string, targetUuid: string) => Promise<void>;
11
9
  }
12
10
  export declare function provideDrawService(): void;
13
11
  export declare function useDrawService(): Context;
@@ -5,15 +5,15 @@ import { useThrelte } from '@threlte/core';
5
5
  import {} from 'koota';
6
6
  import { getContext, setContext } from 'svelte';
7
7
  import { UuidTool } from 'uuid-tool';
8
- import { writeBufferGeometryRange } from '../attribute';
9
- import { DrawService } from '../buf/draw/v1/service_connect';
10
- import { CreateRelationshipRequest, DeleteRelationshipRequest, EntityChangeType, StreamEntityChangesResponse, } from '../buf/draw/v1/service_pb';
11
- import { asFloat32Array, inMeters, STRIDE } from '../buffer';
12
- import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, uuidStringToBytes, } from '../draw';
13
- import { hierarchy, traits, useWorld } from '../ecs';
14
- import { useCameraControls } from './useControls.svelte';
8
+ import { writeBufferGeometryRange } from '../../attribute';
9
+ import { DrawService } from '../../buf/draw/v1/service_connect';
10
+ import { EntityChangeType, StreamEntityChangesResponse } from '../../buf/draw/v1/service_pb';
11
+ import { asFloat32Array, inMeters, STRIDE } from '../../buffer';
12
+ import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, } from '../../draw';
13
+ import { hierarchy, traits, useWorld } from '../../ecs';
14
+ import { useCameraControls } from '../../hooks/useControls.svelte';
15
+ import { createServerRelationships } from './serverRelationships';
15
16
  import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
16
- import { useRelationships } from './useRelationships.svelte';
17
17
  const DRAW_SERVICE_KEY = Symbol('draw-service-context');
18
18
  const FLOAT32_SIZE = 4;
19
19
  const ConnectionStatus = {
@@ -26,7 +26,7 @@ export function provideDrawService() {
26
26
  const world = useWorld();
27
27
  const cameraControls = useCameraControls();
28
28
  const drawConnectionConfig = useDrawConnectionConfig();
29
- const relationships = useRelationships();
29
+ const serverRelationships = createServerRelationships();
30
30
  let connectionStatus = $state(ConnectionStatus.DISCONNECTED);
31
31
  const url = $derived(drawConnectionConfig.current?.backendIP
32
32
  ? `http://${drawConnectionConfig.current.backendIP}:3030`
@@ -67,25 +67,24 @@ export function provideDrawService() {
67
67
  if (changeType === EntityChangeType.ADDED) {
68
68
  if (!transformEntities.has(uuid)) {
69
69
  const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
70
- relationships.apply(spawned.entity, spawned.relationships);
70
+ serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
71
71
  transformEntities.set(uuid, spawned.entity);
72
- relationships.flush(uuid);
73
72
  }
74
73
  }
75
74
  else if (changeType === EntityChangeType.REMOVED) {
75
+ serverRelationships.forget(uuid);
76
76
  destroyTransform(uuid);
77
77
  }
78
78
  else if (changeType === EntityChangeType.UPDATED) {
79
79
  const existing = transformEntities.get(uuid);
80
80
  if (existing) {
81
81
  const updated = updateTransform(existing, transform);
82
- relationships.apply(updated.entity, updated.relationships);
82
+ serverRelationships.apply(updated.entity, uuid, updated.relationships);
83
83
  }
84
84
  else {
85
85
  const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
86
- relationships.apply(spawned.entity, spawned.relationships);
86
+ serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
87
87
  transformEntities.set(uuid, spawned.entity);
88
- relationships.flush(uuid);
89
88
  }
90
89
  }
91
90
  };
@@ -155,9 +154,8 @@ export function provideDrawService() {
155
154
  if (changeType === EntityChangeType.ADDED) {
156
155
  if (!drawingEntities.has(uuid)) {
157
156
  const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
158
- relationships.apply(spawned.entity, spawned.relationships);
157
+ serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
159
158
  drawingEntities.set(uuid, spawned.entity);
160
- relationships.flush(uuid);
161
159
  if (isChunkedDrawing(drawing) && activeClient && activeSignal) {
162
160
  const chunk = getChunkInfo(drawing);
163
161
  if (chunk) {
@@ -169,6 +167,7 @@ export function provideDrawService() {
169
167
  }
170
168
  }
171
169
  else if (changeType === EntityChangeType.REMOVED) {
170
+ serverRelationships.forget(uuid);
172
171
  destroyDrawing(uuid);
173
172
  }
174
173
  else if (changeType === EntityChangeType.UPDATED) {
@@ -178,14 +177,13 @@ export function provideDrawService() {
178
177
  const result = isModel
179
178
  ? updateModel(world, existing, drawing, traits.DrawServiceAPI)
180
179
  : updateDrawing(world, existing, drawing);
181
- relationships.apply(result.entity, result.relationships);
180
+ serverRelationships.apply(result.entity, uuid, result.relationships);
182
181
  drawingEntities.set(uuid, result.entity);
183
182
  }
184
183
  else {
185
184
  const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
186
- relationships.apply(spawned.entity, spawned.relationships);
185
+ serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
187
186
  drawingEntities.set(uuid, spawned.entity);
188
- relationships.flush(uuid);
189
187
  }
190
188
  }
191
189
  };
@@ -289,29 +287,6 @@ export function provideDrawService() {
289
287
  }
290
288
  }
291
289
  };
292
- const createRelationship = async (sourceUuid, targetUuid, type, indexMapping) => {
293
- if (!activeClient)
294
- return;
295
- const rel = {
296
- targetUuid: uuidStringToBytes(targetUuid),
297
- type,
298
- };
299
- if (indexMapping !== undefined) {
300
- rel.indexMapping = indexMapping;
301
- }
302
- await activeClient.createRelationship(new CreateRelationshipRequest({
303
- sourceUuid: uuidStringToBytes(sourceUuid),
304
- relationship: rel,
305
- }));
306
- };
307
- const deleteRelationship = async (sourceUuid, targetUuid) => {
308
- if (!activeClient)
309
- return;
310
- await activeClient.deleteRelationship(new DeleteRelationshipRequest({
311
- sourceUuid: uuidStringToBytes(sourceUuid),
312
- targetUuid: uuidStringToBytes(targetUuid),
313
- }));
314
- };
315
290
  $effect(() => {
316
291
  if (!url) {
317
292
  connectionStatus = ConnectionStatus.DISCONNECTED;
@@ -340,15 +315,14 @@ export function provideDrawService() {
340
315
  hierarchy.destroyEntityTree(world, entity);
341
316
  }
342
317
  drawingEntities.clear();
343
- relationships.clear();
318
+ serverRelationships.reset();
344
319
  };
345
320
  });
321
+ $effect(() => () => serverRelationships.dispose());
346
322
  setContext(DRAW_SERVICE_KEY, {
347
323
  get connectionStatus() {
348
324
  return connectionStatus;
349
325
  },
350
- createRelationship,
351
- deleteRelationship,
352
326
  });
353
327
  }
354
328
  export function useDrawService() {
@@ -6,12 +6,12 @@
6
6
  import { Portal } from '@threlte/extras'
7
7
  import { ElementRect } from 'runed'
8
8
 
9
- import DashboardButton from '../overlay/dashboard/Button.svelte'
9
+ import DashboardButton from '../../components/overlay/dashboard/Button.svelte'
10
+ import Popover from '../../components/overlay/Popover.svelte'
11
+ import ToggleGroup from '../../components/overlay/ToggleGroup.svelte'
10
12
  import { useSelectedEntity } from '../../hooks/useSelection.svelte'
11
13
  import { useSettings } from '../../hooks/useSettings.svelte'
12
14
 
13
- import Popover from '../overlay/Popover.svelte'
14
- import ToggleGroup from '../overlay/ToggleGroup.svelte'
15
15
  import Ellipse from './Ellipse.svelte'
16
16
  import Lasso from './Lasso.svelte'
17
17
  import { provideSelectionPlugin } from './useSelectionPlugin.svelte'
@@ -5,6 +5,6 @@ interface Props {
5
5
  autoSelectNewEntities?: boolean;
6
6
  children?: Snippet;
7
7
  }
8
- declare const Tool: import("svelte").Component<Props, {}, "">;
9
- type Tool = ReturnType<typeof Tool>;
10
- export default Tool;
8
+ declare const SelectionTool: import("svelte").Component<Props, {}, "">;
9
+ type SelectionTool = ReturnType<typeof SelectionTool>;
10
+ export default SelectionTool;
@@ -1 +1,5 @@
1
+ export { default as SelectionTool } from './Selection/SelectionTool.svelte';
2
+ export * as selectionTraits from './Selection/traits';
3
+ export { useSelectionPlugin } from './Selection/useSelectionPlugin.svelte';
4
+ export { default as DrawService } from './DrawService/DrawService.svelte';
1
5
  export { default as Skybox } from './Skybox/Skybox.svelte';
@@ -1,2 +1,8 @@
1
+ // Selection
2
+ export { default as SelectionTool } from './Selection/SelectionTool.svelte';
3
+ export * as selectionTraits from './Selection/traits';
4
+ export { useSelectionPlugin } from './Selection/useSelectionPlugin.svelte';
5
+ // DrawService
6
+ export { default as DrawService } from './DrawService/DrawService.svelte';
1
7
  // Skybox
2
8
  export { default as Skybox } from './Skybox/Skybox.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.28.1",
3
+ "version": "1.29.1",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -39,6 +39,7 @@
39
39
  "@viamrobotics/prime-core": "0.1.5",
40
40
  "@viamrobotics/sdk": "0.69.0",
41
41
  "@viamrobotics/svelte-sdk": "1.2.2",
42
+ "@viamrobotics/tweakpane-config": "0.1.1",
42
43
  "@vitest/browser": "3.2.4",
43
44
  "@vitest/coverage-v8": "^3.2.4",
44
45
  "@zag-js/collapsible": "1.22.1",
File without changes