@viamrobotics/motion-tools 1.27.0 → 1.28.0

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 +22 -8
  2. package/dist/components/App.svelte.d.ts +1 -1
  3. package/dist/components/Camera.svelte +1 -1
  4. package/dist/components/CameraControls.svelte +1 -15
  5. package/dist/components/Focus.svelte +5 -19
  6. package/dist/components/MeasureTool/MeasureTool.svelte +2 -1
  7. package/dist/components/Scene.svelte +1 -1
  8. package/dist/components/SceneProviders.svelte +2 -8
  9. package/dist/components/SceneProviders.svelte.d.ts +0 -2
  10. package/dist/components/Selection/Ellipse.svelte +10 -8
  11. package/dist/components/Selection/Lasso.svelte +10 -8
  12. package/dist/components/overlay/Details.svelte +37 -12
  13. package/dist/components/overlay/FloatingPanel.svelte +8 -3
  14. package/dist/components/overlay/FloatingPanel.svelte.d.ts +5 -0
  15. package/dist/components/overlay/controls/Controls.svelte +40 -0
  16. package/dist/components/overlay/controls/Controls.svelte.d.ts +3 -0
  17. package/dist/components/overlay/dashboard/Button.svelte +3 -3
  18. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  19. package/dist/components/overlay/dashboard/Dashboard.svelte +15 -38
  20. package/dist/components/overlay/widgets/FramePov.svelte +202 -0
  21. package/dist/components/overlay/widgets/FramePov.svelte.d.ts +6 -0
  22. package/dist/ecs/hierarchy.d.ts +16 -0
  23. package/dist/ecs/hierarchy.js +36 -14
  24. package/dist/ecs/traits.js +2 -0
  25. package/dist/ecs/worldMatrix.js +18 -5
  26. package/dist/hooks/useControls.svelte.d.ts +3 -2
  27. package/dist/hooks/useControls.svelte.js +13 -5
  28. package/dist/hooks/useGeometries.svelte.js +9 -5
  29. package/dist/hooks/useSettings.svelte.d.ts +1 -0
  30. package/dist/hooks/useSettings.svelte.js +1 -0
  31. package/dist/plugins/Skybox/Skybox.svelte +54 -0
  32. package/dist/plugins/Skybox/Skybox.svelte.d.ts +12 -0
  33. package/dist/plugins/index.d.ts +1 -0
  34. package/dist/plugins/index.js +2 -0
  35. package/dist/three/OBBHelper.js +4 -1
  36. package/dist/three/arrow.js +2 -0
  37. package/package.json +6 -2
  38. /package/dist/{plugins → hooks/plugins}/bvh.svelte.d.ts +0 -0
  39. /package/dist/{plugins → hooks/plugins}/bvh.svelte.js +0 -0
@@ -0,0 +1,202 @@
1
+ <script lang="ts">
2
+ import { useTask, useThrelte } from '@threlte/core'
3
+ import { Slider, type SliderChangeEvent } from 'svelte-tweakpane-ui'
4
+ import { Matrix4, OrthographicCamera, PerspectiveCamera, WebGLRenderer } from 'three'
5
+
6
+ import { traits, useQuery } from '../../../ecs'
7
+ import { usePartID } from '../../../hooks/usePartID.svelte'
8
+ import { useSettings } from '../../../hooks/useSettings.svelte'
9
+
10
+ import { useOrigin } from '../../xr/useOrigin.svelte'
11
+ import Button from '../dashboard/Button.svelte'
12
+ import FloatingPanel from '../FloatingPanel.svelte'
13
+
14
+ interface Props {
15
+ frameName: string
16
+ }
17
+
18
+ const { frameName }: Props = $props()
19
+
20
+ const { scene, renderer: mainRenderer, renderStage, invalidate } = useThrelte()
21
+ const settings = useSettings()
22
+ const partID = usePartID()
23
+ const origin = useOrigin()
24
+
25
+ // Three.js cameras look down -Z; Viam camera frames conventionally have the
26
+ // optical axis along +Z with image-down along +Y. A 180° rotation around X
27
+ // flips both axes so a Three.js render matches "what a sensor at this frame
28
+ // would see." If empirical testing shows the view is rolled, swap to
29
+ // makeRotationY for an X-flip instead.
30
+ const VIAM_TO_THREE_CAMERA = new Matrix4().makeRotationX(Math.PI)
31
+
32
+ const PERSPECTIVE_FOV_DEG = 60
33
+ // Ortho frustum vertical extent at zoom=1, sized to match what the
34
+ // perspective camera sees at 1 m. zoom > 1 narrows the frustum (zoom in);
35
+ // zoom < 1 widens it (zoom out).
36
+ const BASE_ORTHO_HEIGHT = 2 * Math.tan((PERSPECTIVE_FOV_DEG * Math.PI) / 360)
37
+
38
+ const namedEntities = useQuery(traits.Name)
39
+ const entity = $derived(namedEntities.current.find((e) => e.get(traits.Name) === frameName))
40
+
41
+ const perspectiveCamera = new PerspectiveCamera(PERSPECTIVE_FOV_DEG, 1, 0.01, 1000)
42
+ perspectiveCamera.up.set(0, 0, 1)
43
+
44
+ const orthographicCamera = new OrthographicCamera(-1, 1, 1, -1, 0.01, 1000)
45
+ orthographicCamera.up.set(0, 0, 1)
46
+
47
+ let isOpen = $state(true)
48
+ let cameraMode = $state<'perspective' | 'orthographic'>('perspective')
49
+ let orthoZoom = $state(1)
50
+ let canvasEl = $state.raw<HTMLCanvasElement>()
51
+ let povRenderer = $state.raw<WebGLRenderer | undefined>()
52
+
53
+ const orthoHeight = $derived(BASE_ORTHO_HEIGHT / orthoZoom)
54
+
55
+ const composed = new Matrix4()
56
+ const originMat = new Matrix4()
57
+
58
+ $effect(() => {
59
+ if (!canvasEl) return
60
+ const r = new WebGLRenderer({ canvas: canvasEl, antialias: true, alpha: true })
61
+ // Match the main renderer so colors/tone/transparency are consistent
62
+ // with the main view.
63
+ r.outputColorSpace = mainRenderer.outputColorSpace
64
+ r.toneMapping = mainRenderer.toneMapping
65
+ r.toneMappingExposure = mainRenderer.toneMappingExposure
66
+ r.setPixelRatio(mainRenderer.getPixelRatio())
67
+ r.setSize(canvasEl.clientWidth, canvasEl.clientHeight, false)
68
+ povRenderer = r
69
+ invalidate()
70
+ return () => {
71
+ r.dispose()
72
+ povRenderer = undefined
73
+ }
74
+ })
75
+
76
+ $effect(() => {
77
+ if (!canvasEl) return
78
+ const ro = new ResizeObserver(() => {
79
+ const r = povRenderer
80
+ if (!r || !canvasEl) return
81
+ r.setSize(canvasEl.clientWidth, canvasEl.clientHeight, false)
82
+ invalidate()
83
+ })
84
+ ro.observe(canvasEl)
85
+ return () => ro.disconnect()
86
+ })
87
+
88
+ $effect(() => {
89
+ void cameraMode
90
+ void orthoZoom
91
+ invalidate()
92
+ })
93
+
94
+ $effect(() => {
95
+ if (entity === undefined) {
96
+ isOpen = false
97
+ }
98
+ })
99
+
100
+ $effect(() => {
101
+ if (isOpen) return
102
+ const list = settings.current.openFramePovWidgets[partID.current] ?? []
103
+ const next = list.filter((n) => n !== frameName)
104
+ if (next.length === list.length) return
105
+ settings.current.openFramePovWidgets = {
106
+ ...settings.current.openFramePovWidgets,
107
+ [partID.current]: next,
108
+ }
109
+ })
110
+
111
+ useTask(
112
+ () => {
113
+ const r = povRenderer
114
+ if (!r || !canvasEl || !entity) return
115
+ const worldMat = entity.get(traits.WorldMatrix)
116
+ if (!worldMat) return
117
+
118
+ const width = canvasEl.clientWidth
119
+ const height = canvasEl.clientHeight
120
+ if (width <= 0 || height <= 0) return
121
+
122
+ const povCamera = cameraMode === 'perspective' ? perspectiveCamera : orthographicCamera
123
+
124
+ // Compose origin × worldMatrix × VIAM_TO_THREE_CAMERA. The frame
125
+ // entities' WorldMatrix lives in ECS world space; the rendered scene
126
+ // is wrapped in a T.Group that applies `origin` on top, so the POV
127
+ // camera needs the same origin transform to share coordinate space
128
+ // with the meshes it's rendering.
129
+ originMat
130
+ .makeRotationZ(origin.rotation)
131
+ .setPosition(origin.position[0], origin.position[1], origin.position[2])
132
+ composed.copy(originMat).multiply(worldMat).multiply(VIAM_TO_THREE_CAMERA)
133
+ composed.decompose(povCamera.position, povCamera.quaternion, povCamera.scale)
134
+
135
+ const aspect = width / height
136
+ if (povCamera === perspectiveCamera) {
137
+ perspectiveCamera.aspect = aspect
138
+ } else {
139
+ const halfH = orthoHeight / 2
140
+ const halfW = halfH * aspect
141
+ orthographicCamera.left = -halfW
142
+ orthographicCamera.right = halfW
143
+ orthographicCamera.top = halfH
144
+ orthographicCamera.bottom = -halfH
145
+ }
146
+ povCamera.updateProjectionMatrix()
147
+ povCamera.updateMatrixWorld(true)
148
+
149
+ r.render(scene, povCamera)
150
+ },
151
+ { stage: renderStage, autoInvalidate: false }
152
+ )
153
+
154
+ const handleZoomChange = (event: SliderChangeEvent) => {
155
+ if (event.detail.origin !== 'internal') return
156
+ orthoZoom = event.detail.value as number
157
+ }
158
+ </script>
159
+
160
+ <FloatingPanel
161
+ title={`POV: ${frameName}`}
162
+ bind:isOpen
163
+ defaultSize={{ width: 320, height: 240 }}
164
+ resizable
165
+ onPositionChange={invalidate}
166
+ onSizeChange={invalidate}
167
+ >
168
+ <canvas
169
+ bind:this={canvasEl}
170
+ class="absolute inset-0 block h-full w-full"
171
+ ></canvas>
172
+
173
+ <fieldset class="absolute top-1 right-1 z-1 flex">
174
+ <Button
175
+ icon="grid-orthographic"
176
+ active={cameraMode === 'orthographic'}
177
+ description="Orthographic view"
178
+ onclick={() => (cameraMode = 'orthographic')}
179
+ />
180
+ <Button
181
+ icon="grid-perspective"
182
+ active={cameraMode === 'perspective'}
183
+ description="Perspective view"
184
+ class="-ml-px"
185
+ onclick={() => (cameraMode = 'perspective')}
186
+ />
187
+ </fieldset>
188
+
189
+ {#if cameraMode === 'orthographic'}
190
+ <div class="absolute right-1 bottom-1 left-1 z-1 rounded bg-white/85 p-1">
191
+ <Slider
192
+ label="zoom"
193
+ value={orthoZoom}
194
+ min={0.25}
195
+ max={5}
196
+ step={0.05}
197
+ format={(v) => `${v.toFixed(2)}×`}
198
+ on:change={handleZoomChange}
199
+ />
200
+ </div>
201
+ {/if}
202
+ </FloatingPanel>
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ frameName: string;
3
+ }
4
+ declare const FramePov: import("svelte").Component<Props, {}, "">;
5
+ type FramePov = ReturnType<typeof FramePov>;
6
+ export default FramePov;
@@ -38,5 +38,21 @@ export declare const destroyEntityTree: (world: World, entity: Entity) => void;
38
38
  * the world. Called by `provideHierarchy` when the orphan/named query sets
39
39
  * change or when a `Name` is renamed; also exposed for tests so they can
40
40
  * drive resolution without mounting a component.
41
+ *
42
+ * The first loop builds a `name → entity` map. The second loop reads each
43
+ * orphan's wanted parent name from that map and attaches `ChildOf` to the
44
+ * entity it found.
45
+ *
46
+ * Two checks prevent an entity from being parented to itself (a `ChildOf`
47
+ * cycle would loop `recomputeWorldMatrix` forever):
48
+ *
49
+ * 1. When two entities have the same `Name`, the map keeps whichever
50
+ * one does NOT have `Orphan`. An entity that still has `Orphan` is
51
+ * one we're still trying to resolve — it could be the same entity
52
+ * the second loop looks up. Letting it fill the slot would make the
53
+ * lookup return the orphan itself.
54
+ * 2. In the second loop, if the lookup returns the orphan itself, skip
55
+ * it. This catches the case where the orphan is the only entity in
56
+ * the world with that `Name`.
41
57
  */
42
58
  export declare const resolveOrphans: (named: QueryResult<[Trait<() => string>]>, orphans: QueryResult<[Trait<() => string>]>) => void;
@@ -1,6 +1,6 @@
1
1
  import {} from 'koota';
2
2
  import { ChildOf } from './relations';
3
- import { Name, Orphan } from './traits';
3
+ import * as traits from './traits';
4
4
  /**
5
5
  * Trait list for `world.spawn(...)`. Always emits `Orphan(name)` for non-root
6
6
  * parents; the hierarchy resolver (`provideHierarchy`) swaps it to
@@ -10,7 +10,7 @@ import { Name, Orphan } from './traits';
10
10
  export const parentTraits = (name) => {
11
11
  if (!name || name === 'world')
12
12
  return [];
13
- return [Orphan(name)];
13
+ return [traits.Orphan(name)];
14
14
  };
15
15
  /**
16
16
  * Set or clear an entity's parent. Strips any existing `ChildOf` or `Orphan`,
@@ -26,15 +26,15 @@ export const parentTraits = (name) => {
26
26
  export const setParent = (entity, name) => {
27
27
  const desired = !name || name === 'world' ? undefined : name;
28
28
  const target = entity.targetFor(ChildOf);
29
- const current = (target?.isAlive() ? target.get(Name) : undefined) ?? entity.get(Orphan);
29
+ const current = (target?.isAlive() ? target.get(traits.Name) : undefined) ?? entity.get(traits.Orphan);
30
30
  if (current === desired)
31
31
  return;
32
32
  if (target)
33
33
  entity.remove(ChildOf(target));
34
- entity.remove(Orphan);
34
+ entity.remove(traits.Orphan);
35
35
  if (desired === undefined)
36
36
  return;
37
- entity.add(Orphan(desired));
37
+ entity.add(traits.Orphan(desired));
38
38
  };
39
39
  /** The parent entity, or `undefined` at the world root or while orphaned. */
40
40
  export const getParentEntity = (entity) => entity.targetFor(ChildOf);
@@ -45,9 +45,10 @@ export const getParentEntity = (entity) => entity.targetFor(ChildOf);
45
45
  */
46
46
  export const getParentName = (entity) => {
47
47
  const parent = entity.targetFor(ChildOf);
48
- if (parent && parent.isAlive())
49
- return parent.get(Name);
50
- const orphanFor = entity.get(Orphan);
48
+ if (parent && parent.isAlive()) {
49
+ return parent.get(traits.Name);
50
+ }
51
+ const orphanFor = entity.get(traits.Orphan);
51
52
  return orphanFor || undefined;
52
53
  };
53
54
  /**
@@ -69,22 +70,43 @@ export const destroyEntityTree = (world, entity) => {
69
70
  * the world. Called by `provideHierarchy` when the orphan/named query sets
70
71
  * change or when a `Name` is renamed; also exposed for tests so they can
71
72
  * drive resolution without mounting a component.
73
+ *
74
+ * The first loop builds a `name → entity` map. The second loop reads each
75
+ * orphan's wanted parent name from that map and attaches `ChildOf` to the
76
+ * entity it found.
77
+ *
78
+ * Two checks prevent an entity from being parented to itself (a `ChildOf`
79
+ * cycle would loop `recomputeWorldMatrix` forever):
80
+ *
81
+ * 1. When two entities have the same `Name`, the map keeps whichever
82
+ * one does NOT have `Orphan`. An entity that still has `Orphan` is
83
+ * one we're still trying to resolve — it could be the same entity
84
+ * the second loop looks up. Letting it fill the slot would make the
85
+ * lookup return the orphan itself.
86
+ * 2. In the second loop, if the lookup returns the orphan itself, skip
87
+ * it. This catches the case where the orphan is the only entity in
88
+ * the world with that `Name`.
72
89
  */
73
90
  export const resolveOrphans = (named, orphans) => {
74
91
  const index = new Map();
75
92
  for (const entity of named) {
76
- const name = entity.get(Name);
77
- if (name)
78
- index.set(name, entity);
93
+ const name = entity.get(traits.Name);
94
+ if (!name)
95
+ continue;
96
+ const existing = index.get(name);
97
+ if (existing && !existing.has(traits.Orphan)) {
98
+ continue;
99
+ }
100
+ index.set(name, entity);
79
101
  }
80
102
  for (const orphan of orphans) {
81
- const wantedName = orphan.get(Orphan);
103
+ const wantedName = orphan.get(traits.Orphan);
82
104
  if (!wantedName)
83
105
  continue;
84
106
  const parent = index.get(wantedName);
85
- if (!parent)
107
+ if (!parent || parent === orphan)
86
108
  continue;
87
- orphan.remove(Orphan);
109
+ orphan.remove(traits.Orphan);
88
110
  orphan.add(ChildOf(parent));
89
111
  }
90
112
  };
@@ -230,7 +230,9 @@ export const updateGeometryTrait = (entity, geometry) => {
230
230
  }
231
231
  else if (geometry.geometryType.case === 'mesh') {
232
232
  if (entity.has(BufferGeometry)) {
233
+ const old = entity.get(BufferGeometry);
233
234
  entity.set(BufferGeometry, parsePlyInput(geometry.geometryType.value.mesh));
235
+ old?.dispose();
234
236
  }
235
237
  else {
236
238
  entity.remove(Box, Sphere, Capsule);
@@ -2,7 +2,7 @@ import {} from 'koota';
2
2
  import { Matrix4 } from 'three';
3
3
  import { composeLocalMatrix } from '../transform';
4
4
  import { ChildOf } from './relations';
5
- import { EditedMatrix, LiveMatrix, Matrix, WorldMatrix } from './traits';
5
+ import { EditedMatrix, LiveMatrix, Matrix, Name, WorldMatrix } from './traits';
6
6
  /**
7
7
  * Compute the entity's local-to-parent transform into `out`. Mirrors the
8
8
  * blend used by `Frame.svelte` so `WorldMatrix` agrees with the displayed
@@ -36,14 +36,25 @@ const toLocalMatrix = (entity, out) => {
36
36
  * Synchronously compute and write `WorldMatrix` for every entity in `dirty`
37
37
  * and every descendant via `ChildOf`. Memoizes per-entity world matrices in
38
38
  * `cache` so siblings reuse a parent's result. Caller passes a fresh `cache`
39
- * map per flush.
39
+ * map and `inProgress` set per flush.
40
+ *
41
+ * `inProgress` is the cycle guard: if the parent walk revisits an entity
42
+ * whose computation hasn't finished, we treat that branch as if it had no
43
+ * parent rather than recursing forever. `resolveOrphans` already prevents
44
+ * the only known way to introduce a `ChildOf` cycle; this is here so a
45
+ * future bug downgrades to a soft visual glitch instead of a hard crash.
40
46
  */
41
- const recomputeWorldMatrix = (world, entity, cache) => {
47
+ const recomputeWorldMatrix = (world, entity, cache, inProgress) => {
42
48
  if (!entity.isAlive())
43
49
  return undefined;
44
50
  const cached = cache.get(entity);
45
51
  if (cached)
46
52
  return cached;
53
+ if (inProgress.has(entity)) {
54
+ console.warn('[worldMatrix] ChildOf cycle detected at entity', entity.get(Name) ?? entity);
55
+ return undefined;
56
+ }
57
+ inProgress.add(entity);
47
58
  // Reuse the entity's existing `WorldMatrix` storage when present so a
48
59
  // flush doesn't allocate a throwaway matrix per entity. First-time
49
60
  // entities get a fresh `Matrix4` that's added as the trait below.
@@ -53,10 +64,11 @@ const recomputeWorldMatrix = (world, entity, cache) => {
53
64
  out.identity();
54
65
  const parent = entity.targetFor(ChildOf);
55
66
  if (parent && parent.isAlive()) {
56
- const parentWorld = recomputeWorldMatrix(world, parent, cache);
67
+ const parentWorld = recomputeWorldMatrix(world, parent, cache, inProgress);
57
68
  if (parentWorld)
58
69
  out.premultiply(parentWorld);
59
70
  }
71
+ inProgress.delete(entity);
60
72
  cache.set(entity, out);
61
73
  return out;
62
74
  };
@@ -64,6 +76,7 @@ const flushDirty = (world, dirty) => {
64
76
  if (dirty.size === 0)
65
77
  return;
66
78
  const cache = new Map();
79
+ const inProgress = new Set();
67
80
  const expanded = new Set();
68
81
  const collect = (entity) => {
69
82
  if (expanded.has(entity))
@@ -79,7 +92,7 @@ const flushDirty = (world, dirty) => {
79
92
  for (const entity of expanded) {
80
93
  if (!entity.isAlive())
81
94
  continue;
82
- const worldMat = recomputeWorldMatrix(world, entity, cache);
95
+ const worldMat = recomputeWorldMatrix(world, entity, cache, inProgress);
83
96
  if (!worldMat)
84
97
  continue;
85
98
  if (entity.has(WorldMatrix)) {
@@ -1,12 +1,13 @@
1
1
  import type { CameraControlsRef } from '@threlte/extras';
2
2
  import type { Vector3Tuple } from 'three';
3
+ import type { TrackballControls } from 'three/examples/jsm/Addons.js';
3
4
  export interface CameraPose {
4
5
  position: Vector3Tuple;
5
6
  lookAt: Vector3Tuple;
6
7
  }
7
8
  interface CameraControlsContext {
8
- current: CameraControlsRef | undefined;
9
- set(current: CameraControlsRef): void;
9
+ current: CameraControlsRef | TrackballControls | undefined;
10
+ set(current: CameraControlsRef | TrackballControls): void;
10
11
  setPose(pose: CameraPose, animate?: boolean): void;
11
12
  setInitialPose(): void;
12
13
  setZoom(zoom: number): void;
@@ -6,15 +6,23 @@ export const provideCameraControls = (initialCameraPose) => {
6
6
  const setPose = (pose, animate = false) => {
7
7
  const [x, y, z] = pose.position;
8
8
  const [lookAtX, lookAtY, lookAtZ] = pose.lookAt;
9
- controls?.setPosition(x, y, z, animate);
10
- controls?.setLookAt(x, y, z, lookAtX, lookAtY, lookAtZ, animate);
9
+ if (controls && 'setPosition' in controls) {
10
+ controls.setPosition(x, y, z, animate);
11
+ controls.setLookAt(x, y, z, lookAtX, lookAtY, lookAtZ, animate);
12
+ }
11
13
  };
12
14
  const setZoom = (zoom) => {
13
- controls?.zoomTo(zoom);
15
+ if (controls && 'zoomTo' in controls)
16
+ controls?.zoomTo(zoom);
14
17
  };
15
18
  const setInitialPose = () => {
16
- const pose = initialCameraPose();
17
- setPose(pose ?? { position: [3, 3, 3], lookAt: [0, 0, 0] }, true);
19
+ if (controls && 'setPosition' in controls) {
20
+ const pose = initialCameraPose();
21
+ setPose(pose ?? { position: [3, 3, 3], lookAt: [0, 0, 0] }, true);
22
+ }
23
+ else if (controls) {
24
+ controls.reset();
25
+ }
18
26
  };
19
27
  $effect(() => {
20
28
  const pose = initialCameraPose();
@@ -2,18 +2,19 @@ import { ArmClient, BaseClient, CameraClient, GantryClient, GenericComponentClie
2
2
  import { createResourceClient, createResourceQuery, useResourceNames, } from '@viamrobotics/svelte-sdk';
3
3
  import {} from 'koota';
4
4
  import { getContext, setContext, untrack } from 'svelte';
5
- import { Color } from 'three';
5
+ import { Color, Matrix4 } from 'three';
6
6
  import { resourceColors } from '../color';
7
7
  import { RefetchRates } from '../components/overlay/RefreshRate.svelte';
8
8
  import { hierarchy, traits, useWorld } from '../ecs';
9
9
  import { updateGeometryTrait } from '../ecs/traits';
10
- import { createPose, isPoseEqual } from '../transform';
10
+ import { createPose, poseToMatrix } from '../transform';
11
11
  import { useEnvironment } from './useEnvironment.svelte';
12
12
  import { useLogs } from './useLogs.svelte';
13
13
  import { useResourceByName } from './useResourceByName.svelte';
14
14
  import { RefreshRates, useSettings } from './useSettings.svelte';
15
15
  const key = Symbol('geometries-context');
16
16
  const colorUtil = new Color();
17
+ const tempMatrix = new Matrix4();
17
18
  export const provideGeometries = (partID) => {
18
19
  const environment = useEnvironment();
19
20
  const resources = useResourceByName();
@@ -97,8 +98,11 @@ export const provideGeometries = (partID) => {
97
98
  const existing = entities.get(entityKey);
98
99
  if (existing) {
99
100
  hierarchy.setParent(existing, name);
100
- if (!isPoseEqual(existing.get(traits.Center), center)) {
101
- existing.set(traits.Center, center);
101
+ poseToMatrix(center, tempMatrix);
102
+ const matrix = existing.get(traits.Matrix);
103
+ if (matrix && !matrix.equals(tempMatrix)) {
104
+ matrix.copy(tempMatrix);
105
+ existing.changed(traits.Matrix);
102
106
  }
103
107
  updateGeometryTrait(existing, geometry);
104
108
  continue;
@@ -106,7 +110,7 @@ export const provideGeometries = (partID) => {
106
110
  const entityTraits = [
107
111
  ...hierarchy.parentTraits(name),
108
112
  traits.Name(label),
109
- traits.Center(center),
113
+ traits.Matrix(poseToMatrix(center, new Matrix4())),
110
114
  traits.GeometriesAPI,
111
115
  traits.Geometry(geometry),
112
116
  ];
@@ -26,6 +26,7 @@ export interface Settings {
26
26
  enableQueryDevtools: boolean;
27
27
  enableArmPositionsWidget: boolean;
28
28
  openCameraWidgets: Record<string, string[]>;
29
+ openFramePovWidgets: Record<string, string[]>;
29
30
  renderStats: boolean;
30
31
  renderArmModels: 'colliders' | 'colliders+model' | 'model';
31
32
  renderSubEntityHoverDetail: boolean;
@@ -33,6 +33,7 @@ const defaults = () => ({
33
33
  enableQueryDevtools: false,
34
34
  enableArmPositionsWidget: false,
35
35
  openCameraWidgets: {},
36
+ openFramePovWidgets: {},
36
37
  renderStats: false,
37
38
  renderArmModels: 'colliders+model',
38
39
  renderSubEntityHoverDetail: false,
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import { useThrelte } from '@threlte/core'
3
+ import { EquirectangularReflectionMapping, type Texture, TextureLoader } from 'three'
4
+
5
+ interface Props {
6
+ url: string
7
+ /**
8
+ * Euler rotation `[x, y, z]` in radians applied to `scene.backgroundRotation`.
9
+ * Default `[Math.PI / 2, 0, 0]` aligns a Y-up equirectangular image to this scene's
10
+ * Z-up convention; the Z component then acts as yaw around world +Z.
11
+ */
12
+ rotation?: [x: number, y: number, z: number]
13
+ }
14
+
15
+ const { url, rotation = [Math.PI / 2, 0, 0] }: Props = $props()
16
+ const { scene, invalidate } = useThrelte()
17
+
18
+ $effect.pre(() => {
19
+ const previous = scene.background
20
+ let texture: Texture | undefined
21
+ let cancelled = false
22
+
23
+ new TextureLoader().load(url, (loaded) => {
24
+ if (cancelled) {
25
+ loaded.dispose()
26
+ return
27
+ }
28
+ loaded.mapping = EquirectangularReflectionMapping
29
+ texture = loaded
30
+ scene.background = loaded
31
+ invalidate()
32
+ })
33
+
34
+ return () => {
35
+ cancelled = true
36
+ if (texture && scene.background === texture) {
37
+ scene.background = previous
38
+ invalidate()
39
+ }
40
+ texture?.dispose()
41
+ }
42
+ })
43
+
44
+ $effect.pre(() => {
45
+ const previous = scene.backgroundRotation.clone()
46
+ scene.backgroundRotation.set(...rotation)
47
+ invalidate()
48
+
49
+ return () => {
50
+ scene.backgroundRotation.copy(previous)
51
+ invalidate()
52
+ }
53
+ })
54
+ </script>
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ url: string;
3
+ /**
4
+ * Euler rotation `[x, y, z]` in radians applied to `scene.backgroundRotation`.
5
+ * Default `[Math.PI / 2, 0, 0]` aligns a Y-up equirectangular image to this scene's
6
+ * Z-up convention; the Z component then acts as yaw around world +Z.
7
+ */
8
+ rotation?: [x: number, y: number, z: number];
9
+ }
10
+ declare const Skybox: import("svelte").Component<Props, {}, "">;
11
+ type Skybox = ReturnType<typeof Skybox>;
12
+ export default Skybox;
@@ -0,0 +1 @@
1
+ export { default as Skybox } from './Skybox/Skybox.svelte';
@@ -0,0 +1,2 @@
1
+ // Skybox
2
+ export { default as Skybox } from './Skybox/Skybox.svelte';
@@ -39,9 +39,12 @@ const expandBoxByTransformedBox = (box, childBox, matrix) => {
39
39
  };
40
40
  export class OBBHelper extends LineSegments2 {
41
41
  constructor(color = 0x000000, linewidth = 2) {
42
- const edges = new EdgesGeometry(new BoxGeometry());
42
+ const boxGeometry = new BoxGeometry();
43
+ const edges = new EdgesGeometry(boxGeometry);
43
44
  const geometry = new LineSegmentsGeometry();
44
45
  geometry.setPositions(edges.getAttribute('position').array);
46
+ edges.dispose();
47
+ boxGeometry.dispose();
45
48
  const material = new LineMaterial({
46
49
  color,
47
50
  linewidth,
@@ -20,6 +20,8 @@ export const createArrowGeometry = () => {
20
20
  // Place its center at y = shaftLength + headLength/2 so tip lands at y = shaftLength + headLength
21
21
  headGeo.translate(0, tailLength + headLength * 0.5, 0);
22
22
  const merged = mergeGeometries([tailGeometry, headGeo], true);
23
+ tailGeometry.dispose();
24
+ headGeo.dispose();
23
25
  merged.computeVertexNormals();
24
26
  merged.computeBoundingBox();
25
27
  merged.computeBoundingSphere();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  "prettier-plugin-tailwindcss": "0.6.14",
65
65
  "publint": "0.3.12",
66
66
  "runed": "0.31.1",
67
- "svelte": "5.55.0",
67
+ "svelte": "5.55.7",
68
68
  "svelte-check": "4.4.5",
69
69
  "svelte-tweakpane-ui": "^1.5.16",
70
70
  "svelte-virtuallists": "1.4.2",
@@ -122,6 +122,10 @@
122
122
  "./lib": {
123
123
  "types": "./dist/lib.d.ts",
124
124
  "svelte": "./dist/lib.js"
125
+ },
126
+ "./plugins": {
127
+ "types": "./dist/plugins/index.d.ts",
128
+ "svelte": "./dist/plugins/index.js"
125
129
  }
126
130
  },
127
131
  "repository": {
File without changes
File without changes