@viamrobotics/motion-tools 1.34.5 → 1.34.6

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 (39) hide show
  1. package/dist/components/App.svelte +14 -2
  2. package/dist/components/App.svelte.d.ts +6 -2
  3. package/dist/components/AxesHelper.svelte +3 -1
  4. package/dist/components/AxesHelper.svelte.d.ts +1 -1
  5. package/dist/components/Entities/Arrows/Arrows.svelte +0 -9
  6. package/dist/components/Entities/AxesHelper.svelte +38 -0
  7. package/dist/components/Entities/AxesHelper.svelte.d.ts +8 -0
  8. package/dist/components/Entities/AxesHelpers.svelte +13 -0
  9. package/dist/{plugins/LLMSceneBuilder/AISettings.svelte.d.ts → components/Entities/AxesHelpers.svelte.d.ts} +6 -14
  10. package/dist/components/Entities/Boxes.svelte +290 -0
  11. package/dist/components/Entities/Boxes.svelte.d.ts +14 -0
  12. package/dist/components/Entities/Entities.svelte +10 -5
  13. package/dist/components/Entities/GLTF.svelte +0 -9
  14. package/dist/components/Entities/Line.svelte +0 -9
  15. package/dist/components/Entities/Mesh.svelte +5 -23
  16. package/dist/components/Entities/Points.svelte +1 -9
  17. package/dist/components/Entities/composeBoxMatrix.d.ts +12 -0
  18. package/dist/components/Entities/composeBoxMatrix.js +29 -0
  19. package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +27 -0
  20. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +87 -39
  21. package/dist/components/Scene.svelte +0 -1
  22. package/dist/components/Selected.svelte +14 -3
  23. package/dist/components/SelectedTransformControls.svelte +3 -5
  24. package/dist/components/overlay/Details.svelte +9 -4
  25. package/dist/hooks/plugins/bvh.svelte.js +9 -0
  26. package/dist/hooks/useConfigFrames.svelte.js +5 -3
  27. package/dist/hooks/useFragmentInfo.svelte.d.ts +24 -0
  28. package/dist/hooks/useFragmentInfo.svelte.js +86 -0
  29. package/dist/hooks/useFramelessComponents.svelte.js +3 -1
  30. package/dist/hooks/usePartConfig.svelte.d.ts +0 -6
  31. package/dist/hooks/usePartConfig.svelte.js +5 -60
  32. package/dist/plugins/Focus/FocusBox.svelte +12 -1
  33. package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.d.ts +9 -2
  34. package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.js +65 -10
  35. package/dist/plugins/LLMSceneBuilder/useSceneBuilder.svelte.js +29 -5
  36. package/dist/three/OBBHelper.d.ts +8 -1
  37. package/dist/three/OBBHelper.js +11 -1
  38. package/package.json +2 -1
  39. package/dist/plugins/LLMSceneBuilder/AISettings.svelte +0 -0
@@ -2,15 +2,13 @@
2
2
  module
3
3
  lang="ts"
4
4
  >
5
- import { BoxGeometry, EdgesGeometry, SphereGeometry } from 'three'
5
+ import { EdgesGeometry, SphereGeometry } from 'three'
6
6
 
7
7
  /**
8
8
  * Shared unit geometries — every mesh references these and sets
9
9
  * dimensions through `mesh.scale`, so resizing never rebuilds GPU buffers.
10
10
  */
11
- const unitBox = new BoxGeometry(1, 1, 1)
12
11
  const unitSphere = new SphereGeometry(1, 16, 12)
13
- const unitBoxEdges = new EdgesGeometry(unitBox, 0)
14
12
  const unitSphereEdges = new EdgesGeometry(unitSphere, 0)
15
13
  </script>
16
14
 
@@ -27,7 +25,6 @@
27
25
  import { traits, useTrait } from '../../ecs'
28
26
  import { poseToObject3d } from '../../transform'
29
27
 
30
- import AxesHelper from '../AxesHelper.svelte'
31
28
  import Capsule from './Capsule.svelte'
32
29
 
33
30
  interface Props extends Omit<ThrelteProps<Mesh>, 'ref'> {
@@ -46,11 +43,9 @@
46
43
  const entityColors = useTrait(() => entity, traits.Colors)
47
44
  const entityColor = useTrait(() => entity, traits.Color)
48
45
  const opacity = useTrait(() => entity, traits.Opacity)
49
- const box = useTrait(() => entity, traits.Box)
50
46
  const capsule = useTrait(() => entity, traits.Capsule)
51
47
  const sphere = useTrait(() => entity, traits.Sphere)
52
48
  const bufferGeometry = useTrait(() => entity, traits.BufferGeometry)
53
- const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
54
49
  const materialProps = useTrait(() => entity, traits.Material)
55
50
  const renderOrder = useTrait(() => entity, traits.RenderOrder)
56
51
 
@@ -98,10 +93,7 @@
98
93
  })
99
94
 
100
95
  $effect(() => {
101
- if (box.current) {
102
- const { x, y, z } = box.current
103
- mesh.scale.set(x * 0.001, y * 0.001, z * 0.001)
104
- } else if (sphere.current) {
96
+ if (sphere.current) {
105
97
  mesh.scale.setScalar((sphere.current.r ?? 0) * 0.001)
106
98
  } else {
107
99
  mesh.scale.set(1, 1, 1)
@@ -137,9 +129,7 @@
137
129
  renderOrder={renderOrder.current}
138
130
  {...rest}
139
131
  >
140
- {#if box.current || sphere.current}
141
- {@const meshGeometry = box.current ? unitBox : unitSphere}
142
- {@const edgesGeometry = box.current ? unitBoxEdges : unitSphereEdges}
132
+ {#if sphere.current}
143
133
  <!--
144
134
  Switch via a derived `is` on the same <T> so `useAttach`'s effect
145
135
  cleanup runs before the new attach. Splitting these across two
@@ -148,7 +138,7 @@
148
138
  it to the pre-attach value (null), leaving the mesh geometryless.
149
139
  -->
150
140
  <T
151
- is={meshGeometry}
141
+ is={unitSphere}
152
142
  dispose={false}
153
143
  />
154
144
  <T.LineSegments
@@ -156,7 +146,7 @@
156
146
  bvh={{ enabled: false }}
157
147
  >
158
148
  <T
159
- is={edgesGeometry}
149
+ is={unitSphereEdges}
160
150
  dispose={false}
161
151
  />
162
152
  <T.LineBasicMaterial color={darkenColor(color, 10)} />
@@ -193,11 +183,3 @@
193
183
  {@render children?.()}
194
184
  </T>
195
185
  {/if}
196
-
197
- {#if showAxesHelper.current}
198
- <AxesHelper
199
- name={entity}
200
- width={3}
201
- length={0.1}
202
- />
203
- {/if}
@@ -9,7 +9,6 @@
9
9
  import { traits, useTrait } from '../../ecs'
10
10
  import { useSettings } from '../../hooks/useSettings.svelte'
11
11
 
12
- import AxesHelper from '../AxesHelper.svelte'
13
12
  import { useEntityEvents } from './hooks/useEntityEvents.svelte'
14
13
 
15
14
  interface Props {
@@ -29,7 +28,6 @@
29
28
  const entityPointSize = useTrait(() => entity, traits.PointSize)
30
29
  const opacity = useTrait(() => entity, traits.Opacity)
31
30
  const invisible = useTrait(() => entity, traits.InheritedInvisible)
32
- const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
33
31
  const renderOrder = useTrait(() => entity, traits.RenderOrder)
34
32
  const materialProps = useTrait(() => entity, traits.Material)
35
33
 
@@ -134,13 +132,7 @@
134
132
  >
135
133
  <T is={geometry.current} />
136
134
  <T is={material} />
137
- {#if showAxesHelper.current}
138
- <AxesHelper
139
- name={entity}
140
- width={3}
141
- length={0.1}
142
- />
143
- {/if}
135
+
144
136
  {@render children?.()}
145
137
  </T>
146
138
  {/if}
@@ -0,0 +1,12 @@
1
+ import type { Entity } from 'koota';
2
+ import { Matrix4 } from 'three';
3
+ /**
4
+ * Compose a box entity's full render transform into `out`:
5
+ * `WorldMatrix × Center pose × box dimensions (mm → m)` — the same
6
+ * composition the per-entity path produced by nesting a dimension-scaled,
7
+ * center-offset mesh inside a `WorldMatrix`-driven group.
8
+ *
9
+ * Returns `false` (leaving `out` untouched) when the entity is missing the
10
+ * traits needed to place a box.
11
+ */
12
+ export declare const composeBoxMatrix: (entity: Entity, out: Matrix4) => boolean;
@@ -0,0 +1,29 @@
1
+ import { Matrix4, Vector3 } from 'three';
2
+ import { traits } from '../../ecs';
3
+ import { poseToMatrix } from '../../transform';
4
+ const centerMatrix = new Matrix4();
5
+ const dimensions = new Vector3();
6
+ const MM_TO_M = 0.001;
7
+ /**
8
+ * Compose a box entity's full render transform into `out`:
9
+ * `WorldMatrix × Center pose × box dimensions (mm → m)` — the same
10
+ * composition the per-entity path produced by nesting a dimension-scaled,
11
+ * center-offset mesh inside a `WorldMatrix`-driven group.
12
+ *
13
+ * Returns `false` (leaving `out` untouched) when the entity is missing the
14
+ * traits needed to place a box.
15
+ */
16
+ export const composeBoxMatrix = (entity, out) => {
17
+ const box = entity.get(traits.Box);
18
+ const worldMatrix = entity.get(traits.WorldMatrix);
19
+ if (!box || !worldMatrix) {
20
+ return false;
21
+ }
22
+ out.copy(worldMatrix);
23
+ const center = entity.get(traits.Center);
24
+ if (center) {
25
+ out.multiply(poseToMatrix(center, centerMatrix));
26
+ }
27
+ out.scale(dimensions.set(box.x * MM_TO_M, box.y * MM_TO_M, box.z * MM_TO_M));
28
+ return true;
29
+ };
@@ -1,5 +1,17 @@
1
1
  import type { Entity } from 'koota';
2
2
  import { type IntersectionEvent } from '@threlte/extras';
3
+ /**
4
+ * Pointer handlers for a renderer that draws a single entity — every event
5
+ * targets the closed-over entity.
6
+ *
7
+ * Layers invisibility on top of the shared handlers: enter/move/down/click are
8
+ * suppressed while the entity is invisible (raycasting still hits the visible
9
+ * leaf mesh of Frame/Geometry/GLTF, so the scene's visibility filter can't
10
+ * block them — added in #577, migrated to InheritedInvisible in #710).
11
+ * `onpointerleave` is intentionally left active. The effect tears down a stale
12
+ * Hovered/InstancedMatrix for an entity that turns invisible while hovered,
13
+ * since the guarded handlers can no longer fire to clean it up.
14
+ */
3
15
  export declare const useEntityEvents: (entity: () => Entity | undefined) => {
4
16
  onpointerenter: (event: IntersectionEvent<MouseEvent>) => void;
5
17
  onpointermove: (event: IntersectionEvent<MouseEvent>) => void;
@@ -7,3 +19,18 @@ export declare const useEntityEvents: (entity: () => Entity | undefined) => {
7
19
  onpointerdown: (event: IntersectionEvent<MouseEvent>) => void;
8
20
  onclick: (event: IntersectionEvent<MouseEvent>) => void;
9
21
  };
22
+ /**
23
+ * Pointer handlers for an instanced renderer that draws many entities through
24
+ * one object — `entityForEvent` maps each event back to the entity it targets
25
+ * (typically via `event.instanceId`). Threlte keys hover identity by object
26
+ * uuid + instance id, so enter/leave fire per instance with the id on the
27
+ * event. No invisibility watcher: invisible instances are skipped by the
28
+ * instanced raycast, so they never receive events.
29
+ */
30
+ export declare const useInstancedEntityEvents: (entityForEvent: (event: IntersectionEvent<MouseEvent>) => Entity | undefined) => {
31
+ onpointerenter: (event: IntersectionEvent<MouseEvent>) => void;
32
+ onpointermove: (event: IntersectionEvent<MouseEvent>) => void;
33
+ onpointerleave: (event: IntersectionEvent<MouseEvent>) => void;
34
+ onpointerdown: (event: IntersectionEvent<MouseEvent>) => void;
35
+ onclick: (event: IntersectionEvent<MouseEvent>) => void;
36
+ };
@@ -12,43 +12,49 @@ const infoToLocalMatrix = (info, out) => {
12
12
  out.makeRotationFromQuaternion(hoverQuat);
13
13
  out.setPosition(info.x, info.y, info.z);
14
14
  };
15
- export const useEntityEvents = (entity) => {
15
+ /**
16
+ * Shared pointer handlers behind `useEntityEvents` and
17
+ * `useInstancedEntityEvents`. `entityForEvent` maps an event to the entity it
18
+ * targets. No invisibility handling lives here: single-entity renderers layer
19
+ * that on in `useEntityEvents`; instanced renderers don't need it because
20
+ * invisible instances are skipped by the instanced raycast.
21
+ */
22
+ const createEntityEvents = (entityForEvent, cursor) => {
16
23
  const down = new Vector2();
17
24
  const world = useWorld();
18
- const cursor = useCursor();
19
- const invisible = useTrait(entity, traits.InheritedInvisible);
25
+ const hoverEntity = (currentEntity, event) => {
26
+ const hoverInfo = updateHoverInfo(currentEntity, event);
27
+ if (hoverInfo) {
28
+ infoToLocalMatrix(hoverInfo, tempHoverMatrix);
29
+ const worldMatrix = currentEntity.get(traits.WorldMatrix);
30
+ const composed = new Matrix4();
31
+ if (worldMatrix) {
32
+ composed.copy(worldMatrix).multiply(tempHoverMatrix);
33
+ }
34
+ else {
35
+ composed.copy(tempHoverMatrix);
36
+ }
37
+ currentEntity.add(traits.InstancedMatrix({
38
+ matrix: composed,
39
+ index: hoverInfo.index,
40
+ }));
41
+ }
42
+ currentEntity.add(traits.Hovered);
43
+ };
20
44
  const onpointerenter = (event) => {
21
- if (invisible.current)
22
- return;
23
45
  event.stopPropagation();
24
46
  cursor.onPointerEnter();
25
- const currentEntity = entity();
47
+ const currentEntity = entityForEvent(event);
26
48
  if (currentEntity && !currentEntity.has(traits.Hovered)) {
27
- const hoverInfo = updateHoverInfo(currentEntity, event);
28
- if (hoverInfo) {
29
- infoToLocalMatrix(hoverInfo, tempHoverMatrix);
30
- const worldMatrix = currentEntity.get(traits.WorldMatrix);
31
- const composed = new Matrix4();
32
- if (worldMatrix) {
33
- composed.copy(worldMatrix).multiply(tempHoverMatrix);
34
- }
35
- else {
36
- composed.copy(tempHoverMatrix);
37
- }
38
- currentEntity.add(traits.InstancedMatrix({
39
- matrix: composed,
40
- index: hoverInfo.index,
41
- }));
42
- }
43
- currentEntity.add(traits.Hovered);
49
+ hoverEntity(currentEntity, event);
44
50
  }
45
51
  };
46
52
  const onpointermove = (event) => {
47
- if (invisible.current)
48
- return;
49
53
  event.stopPropagation();
50
- const currentEntity = entity();
51
- if (currentEntity?.has(traits.Hovered)) {
54
+ const currentEntity = entityForEvent(event);
55
+ if (!currentEntity)
56
+ return;
57
+ if (currentEntity.has(traits.Hovered)) {
52
58
  const hoverInfo = updateHoverInfo(currentEntity, event);
53
59
  if (!hoverInfo)
54
60
  return;
@@ -66,11 +72,17 @@ export const useEntityEvents = (entity) => {
66
72
  instanced.index = hoverInfo.index;
67
73
  currentEntity.changed(traits.InstancedMatrix);
68
74
  }
75
+ else {
76
+ // A move can target an entity that never got an enter event — e.g.
77
+ // an instanced renderer recycled an instance id to a new entity
78
+ // under a motionless cursor — so promote the move to a hover.
79
+ hoverEntity(currentEntity, event);
80
+ }
69
81
  };
70
82
  const onpointerleave = (event) => {
71
83
  event.stopPropagation();
72
84
  cursor.onPointerLeave();
73
- const currentEntity = entity();
85
+ const currentEntity = entityForEvent(event);
74
86
  if (currentEntity?.has(traits.Hovered)) {
75
87
  currentEntity.remove(traits.Hovered);
76
88
  }
@@ -79,19 +91,14 @@ export const useEntityEvents = (entity) => {
79
91
  }
80
92
  };
81
93
  const onpointerdown = (event) => {
82
- if (invisible.current)
83
- return;
84
94
  down.copy(event.pointer);
85
95
  };
86
96
  const onclick = (event) => {
87
- if (invisible.current) {
88
- return;
89
- }
90
97
  event.stopPropagation();
91
98
  if (down.distanceToSquared(event.pointer) >= 0.1) {
92
99
  return;
93
100
  }
94
- const currentEntity = entity();
101
+ const currentEntity = entityForEvent(event);
95
102
  if (!currentEntity)
96
103
  return;
97
104
  if (event.nativeEvent.shiftKey) {
@@ -116,6 +123,35 @@ export const useEntityEvents = (entity) => {
116
123
  currentEntity.add(traits.InstanceId(event.instanceId ?? event.batchId));
117
124
  }
118
125
  };
126
+ return {
127
+ onpointerenter,
128
+ onpointermove,
129
+ onpointerleave,
130
+ onpointerdown,
131
+ onclick,
132
+ };
133
+ };
134
+ /**
135
+ * Pointer handlers for a renderer that draws a single entity — every event
136
+ * targets the closed-over entity.
137
+ *
138
+ * Layers invisibility on top of the shared handlers: enter/move/down/click are
139
+ * suppressed while the entity is invisible (raycasting still hits the visible
140
+ * leaf mesh of Frame/Geometry/GLTF, so the scene's visibility filter can't
141
+ * block them — added in #577, migrated to InheritedInvisible in #710).
142
+ * `onpointerleave` is intentionally left active. The effect tears down a stale
143
+ * Hovered/InstancedMatrix for an entity that turns invisible while hovered,
144
+ * since the guarded handlers can no longer fire to clean it up.
145
+ */
146
+ export const useEntityEvents = (entity) => {
147
+ const cursor = useCursor();
148
+ const invisible = useTrait(entity, traits.InheritedInvisible);
149
+ const events = createEntityEvents(entity, cursor);
150
+ const whenVisible = (handler) => (event) => {
151
+ if (invisible.current)
152
+ return;
153
+ handler(event);
154
+ };
119
155
  $effect(() => {
120
156
  if (invisible.current) {
121
157
  cursor.onPointerLeave();
@@ -129,10 +165,22 @@ export const useEntityEvents = (entity) => {
129
165
  }
130
166
  });
131
167
  return {
132
- onpointerenter,
133
- onpointermove,
134
- onpointerleave,
135
- onpointerdown,
136
- onclick,
168
+ onpointerenter: whenVisible(events.onpointerenter),
169
+ onpointermove: whenVisible(events.onpointermove),
170
+ onpointerleave: events.onpointerleave,
171
+ onpointerdown: whenVisible(events.onpointerdown),
172
+ onclick: whenVisible(events.onclick),
137
173
  };
138
174
  };
175
+ /**
176
+ * Pointer handlers for an instanced renderer that draws many entities through
177
+ * one object — `entityForEvent` maps each event back to the entity it targets
178
+ * (typically via `event.instanceId`). Threlte keys hover identity by object
179
+ * uuid + instance id, so enter/leave fire per instance with the id on the
180
+ * event. No invisibility watcher: invisible instances are skipped by the
181
+ * instanced raycast, so they never receive events.
182
+ */
183
+ export const useInstancedEntityEvents = (entityForEvent) => {
184
+ const cursor = useCursor();
185
+ return createEntityEvents(entityForEvent, cursor);
186
+ };
@@ -79,7 +79,6 @@
79
79
  plane="xy"
80
80
  sectionColor="#333"
81
81
  infiniteGrid
82
- renderOrder={999}
83
82
  cellSize={settings.current.gridCellSize}
84
83
  sectionSize={settings.current.gridSectionSize}
85
84
  fadeOrigin={[0, 0, 0]}
@@ -1,13 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { T, useTask, useThrelte } from '@threlte/core'
3
- import { BatchedMesh, Box3 } from 'three'
3
+ import { BatchedMesh, Box3, Matrix4 } from 'three'
4
4
  import { OBB } from 'three/addons/math/OBB.js'
5
5
 
6
+ import { composeBoxMatrix } from './Entities/composeBoxMatrix'
6
7
  import { traits, useQuery } from '../ecs'
7
8
  import { OBBHelper } from '../three/OBBHelper'
8
9
 
9
10
  const box3 = new Box3()
10
11
  const obb = new OBB()
12
+ const boxMatrix = new Matrix4()
11
13
 
12
14
  const { scene, invalidate } = useThrelte()
13
15
  const selected = useQuery(traits.Selected)
@@ -17,12 +19,21 @@
17
19
  useTask(
18
20
  () => {
19
21
  for (const [entity, obbHelper] of obbHelpers) {
22
+ /**
23
+ * Boxes render instanced, so the entity's named scene object
24
+ * carries no geometry — derive the OBB straight from traits.
25
+ */
26
+ if (composeBoxMatrix(entity, boxMatrix)) {
27
+ obbHelper.setFromMatrix4(boxMatrix)
28
+ continue
29
+ }
30
+
20
31
  const object = scene.getObjectByName(entity as unknown as string)
21
32
  if (!object) continue
22
33
 
23
34
  const instance = entity.get(traits.InstanceId)
24
- if (instance !== undefined && instance >= 0) {
25
- ;(object as BatchedMesh).getBoundingBoxAt(instance, box3)
35
+ if (instance !== undefined && instance >= 0 && object instanceof BatchedMesh) {
36
+ object.getBoundingBoxAt(instance, box3)
26
37
  obb.fromBox3(box3)
27
38
  obbHelper.setFromOBB(obb)
28
39
  } else {
@@ -8,15 +8,15 @@
8
8
  import { relations, traits, useQuery, useTrait } from '../ecs'
9
9
  import { useTransformControls } from '../hooks/useControls.svelte'
10
10
  import { useEnvironment } from '../hooks/useEnvironment.svelte'
11
+ import { useFragmentInfo } from '../hooks/useFragmentInfo.svelte'
11
12
  import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
12
- import { usePartConfig } from '../hooks/usePartConfig.svelte'
13
13
  import { useSettings } from '../hooks/useSettings.svelte'
14
14
  import { createPose, matrixToPose, poseToMatrix, solveEditedMatrix } from '../transform'
15
15
 
16
16
  const { scene } = useThrelte()
17
17
  const settings = useSettings()
18
18
  const environment = useEnvironment()
19
- const partConfig = usePartConfig()
19
+ const fragmentInfo = useFragmentInfo()
20
20
  const transformControls = useTransformControls()
21
21
  const sessions = useFrameEditSession()
22
22
  const selected = useQuery(traits.Selected)
@@ -36,9 +36,7 @@
36
36
  box.current !== undefined || sphere.current !== undefined || capsule.current !== undefined
37
37
  )
38
38
  const isFragmentComponentWithVariables = $derived(
39
- name.current &&
40
- Object.keys(partConfig.componentNameToFragmentInfo?.[name.current]?.variables ?? {}).length >
41
- 0
39
+ name.current && Object.keys(fragmentInfo.current?.[name.current]?.variables ?? {}).length > 0
42
40
  )
43
41
 
44
42
  // Mesh sets name={entity} on its inner mesh, so getObjectByName resolves
@@ -46,6 +46,7 @@
46
46
  import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
47
47
  import { useCameraControls } from '../../hooks/useControls.svelte'
48
48
  import { useEnvironment } from '../../hooks/useEnvironment.svelte'
49
+ import { useFragmentInfo } from '../../hooks/useFragmentInfo.svelte'
49
50
  import { useLinkedEntities } from '../../hooks/useLinked.svelte'
50
51
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
51
52
  import { usePartID } from '../../hooks/usePartID.svelte'
@@ -66,6 +67,7 @@
66
67
  const resourceByName = useResourceByName()
67
68
  const configFrames = useConfigFrames()
68
69
  const partConfig = usePartConfig()
70
+ const fragmentInfo = useFragmentInfo()
69
71
  const partID = usePartID()
70
72
  const settings = useSettings()
71
73
  const environment = useEnvironment()
@@ -104,9 +106,7 @@
104
106
  const isFrameNode = $derived(!!framesAPI.current)
105
107
  const isGeometry = $derived(!!geometriesAPI.current)
106
108
  const isFragmentComponentWithVariables = $derived(
107
- name.current &&
108
- Object.keys(partConfig.componentNameToFragmentInfo?.[name.current]?.variables ?? {}).length >
109
- 0
109
+ name.current && Object.keys(fragmentInfo.current?.[name.current]?.variables ?? {}).length > 0
110
110
  )
111
111
  const showEditFrameOptions = $derived(
112
112
  isFrameNode && partConfig.hasEditPermissions && !isFragmentComponentWithVariables
@@ -435,7 +435,12 @@
435
435
  </p>
436
436
  {/if}
437
437
 
438
- <h3 class="text-subtle-2 pt-3 pb-2">Details</h3>
438
+ <h3
439
+ class="text-subtle-2 pt-3 pb-2"
440
+ data-testid="details-header"
441
+ >
442
+ Details
443
+ </h3>
439
444
 
440
445
  <div class="flex flex-col gap-2.5">
441
446
  {#if !customDetails.current}
@@ -25,6 +25,15 @@ export const bvh = (raycaster, options) => {
25
25
  return;
26
26
  if (opts.enabled === false)
27
27
  return;
28
+ /**
29
+ * `InstancedMesh2` brings its own per-instance BVH raycast (and a
30
+ * real `bvh` field, so it can't even take a `bvh` opt-out prop —
31
+ * Threlte would assign the prop onto the object and clobber it).
32
+ * Patching it with three-mesh-bvh's `acceleratedRaycast` would
33
+ * test only the unit geometry, so skip it entirely.
34
+ */
35
+ if (ref.isInstancedMesh2)
36
+ return;
28
37
  if (isInstanceOf(ref, 'Points') &&
29
38
  /**
30
39
  * This check is necessary, there are some strange cases where points are coming in from PCDs without any position data
@@ -2,11 +2,13 @@ import { Transform } from '@viamrobotics/sdk';
2
2
  import { getContext, setContext } from 'svelte';
3
3
  import { createTransformFromFrame } from '../frame';
4
4
  import { useEnvironment } from './useEnvironment.svelte';
5
+ import { useFragmentInfo } from './useFragmentInfo.svelte';
5
6
  import { usePartConfig } from './usePartConfig.svelte';
6
7
  const key = Symbol('config-frames-context');
7
8
  export const provideConfigFrames = () => {
8
9
  const environment = useEnvironment();
9
10
  const partConfig = usePartConfig();
11
+ const fragmentInfo = useFragmentInfo();
10
12
  $effect(() => {
11
13
  environment.current.viewerMode = partConfig.isDirty ? 'edit' : 'monitor';
12
14
  });
@@ -25,12 +27,12 @@ export const provideConfigFrames = () => {
25
27
  });
26
28
  const [fragmentFrames, fragmentUnsetFrameNames] = $derived.by(() => {
27
29
  const { fragment_mods: fragmentMods = [] } = partConfig.current;
28
- const fragmentDefinedComponents = Object.keys(partConfig.componentNameToFragmentInfo ?? {});
30
+ const fragmentDefinedComponents = Object.keys(fragmentInfo.current ?? {});
29
31
  const results = {};
30
32
  const unsetResults = [];
31
33
  // deal with fragment defined components
32
34
  for (const fragmentComponentName of fragmentDefinedComponents || []) {
33
- const fragmentId = partConfig.componentNameToFragmentInfo[fragmentComponentName].id;
35
+ const fragmentId = fragmentInfo.current[fragmentComponentName].id;
34
36
  const fragmentMod = fragmentMods?.find((mod) => mod.fragment_id === fragmentId);
35
37
  if (!fragmentMod) {
36
38
  continue;
@@ -64,7 +66,7 @@ export const provideConfigFrames = () => {
64
66
  * any whose frame the user has $unset.
65
67
  */
66
68
  const unsetFragmentNames = new Set(fragmentUnsetFrameNames);
67
- for (const name of Object.keys(partConfig.componentNameToFragmentInfo)) {
69
+ for (const name of Object.keys(fragmentInfo.current)) {
68
70
  if (!unsetFragmentNames.has(name)) {
69
71
  validFrames.add(name);
70
72
  }
@@ -0,0 +1,24 @@
1
+ import type { Frame } from '../frame';
2
+ export interface FragmentInfo {
3
+ id: string;
4
+ frame?: Frame;
5
+ variables: Record<string, string>;
6
+ }
7
+ interface FragmentInfoContext {
8
+ /** componentName -> the fragment that defines it ({ id, variables }) */
9
+ current: Record<string, FragmentInfo>;
10
+ }
11
+ /**
12
+ * Single source of truth for which components are defined by a fragment.
13
+ *
14
+ * Embedded hosts own this knowledge and pass it as a top-level App prop; in
15
+ * standalone we derive it from the part's `getRobotPart` -> `getFragment`
16
+ * queries. Mode is fixed for the session (the prop is either always defined or
17
+ * always undefined), mirroring `providePartConfig`.
18
+ *
19
+ * Must be provided BEFORE `providePartConfig`, whose frame-edit routing
20
+ * consumes `useFragmentInfo()` to choose part-frame vs fragment-mod writes.
21
+ */
22
+ export declare const provideFragmentInfo: (partID: () => string, embeddedMap: () => Record<string, FragmentInfo> | undefined) => void;
23
+ export declare const useFragmentInfo: () => FragmentInfoContext;
24
+ export {};
@@ -0,0 +1,86 @@
1
+ import { createAppQuery } from '@viamrobotics/svelte-sdk';
2
+ import { getContext, setContext } from 'svelte';
3
+ const key = Symbol('fragment-info-context');
4
+ /**
5
+ * Single source of truth for which components are defined by a fragment.
6
+ *
7
+ * Embedded hosts own this knowledge and pass it as a top-level App prop; in
8
+ * standalone we derive it from the part's `getRobotPart` -> `getFragment`
9
+ * queries. Mode is fixed for the session (the prop is either always defined or
10
+ * always undefined), mirroring `providePartConfig`.
11
+ *
12
+ * Must be provided BEFORE `providePartConfig`, whose frame-edit routing
13
+ * consumes `useFragmentInfo()` to choose part-frame vs fragment-mod writes.
14
+ */
15
+ export const provideFragmentInfo = (partID, embeddedMap) => {
16
+ const embedded = $derived(embeddedMap());
17
+ const config = $derived(embedded ? { current: embedded } : useStandaloneFragmentInfo(partID));
18
+ setContext(key, {
19
+ get current() {
20
+ return config.current;
21
+ },
22
+ });
23
+ };
24
+ export const useFragmentInfo = () => {
25
+ return getContext(key);
26
+ };
27
+ const useStandaloneFragmentInfo = (partID) => {
28
+ const partQuery = createAppQuery('getRobotPart', () => [partID()], {
29
+ refetchInterval: false,
30
+ });
31
+ const networkPartConfig = $derived(partQuery.data?.part?.robotConfig);
32
+ const configJSON = $derived.by(() => {
33
+ if (!networkPartConfig) {
34
+ return undefined;
35
+ }
36
+ try {
37
+ return networkPartConfig.toJson();
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ });
43
+ const fragmentQueries = $derived((configJSON?.fragments ?? []).map((fragmentId) => {
44
+ const id = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
45
+ return createAppQuery('getFragment', () => [id], { refetchInterval: false });
46
+ }));
47
+ const fragmentIdToVariables = $derived.by(() => {
48
+ const results = {};
49
+ for (const fragment of configJSON?.fragments ?? []) {
50
+ const id = typeof fragment === 'string' ? fragment : fragment.id;
51
+ const variables = typeof fragment === 'string' ? {} : (fragment.variables ?? {});
52
+ results[id] = variables;
53
+ }
54
+ return results;
55
+ });
56
+ const componentNameToFragmentInfo = $derived.by(() => {
57
+ const results = {};
58
+ for (const query of fragmentQueries) {
59
+ if (!query.data) {
60
+ continue;
61
+ }
62
+ const fragmentId = query.data.id;
63
+ const components = query.data?.fragment?.fields['components']?.kind;
64
+ if (components?.case === 'listValue') {
65
+ for (const component of components.value.values) {
66
+ if (component.kind.case === 'structValue') {
67
+ const componentName = component.kind.value.fields['name']?.kind;
68
+ if (componentName?.case === 'stringValue') {
69
+ results[componentName.value] = {
70
+ id: fragmentId,
71
+ frame: component.kind.value.fields['frame'].toJson(),
72
+ variables: fragmentIdToVariables[fragmentId] ?? {},
73
+ };
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ return results;
80
+ });
81
+ return {
82
+ get current() {
83
+ return componentNameToFragmentInfo;
84
+ },
85
+ };
86
+ };