@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.
- package/dist/components/App.svelte +22 -8
- package/dist/components/App.svelte.d.ts +1 -1
- package/dist/components/Camera.svelte +1 -1
- package/dist/components/CameraControls.svelte +1 -15
- package/dist/components/Focus.svelte +5 -19
- package/dist/components/MeasureTool/MeasureTool.svelte +2 -1
- package/dist/components/Scene.svelte +1 -1
- package/dist/components/SceneProviders.svelte +2 -8
- package/dist/components/SceneProviders.svelte.d.ts +0 -2
- package/dist/components/Selection/Ellipse.svelte +10 -8
- package/dist/components/Selection/Lasso.svelte +10 -8
- package/dist/components/overlay/Details.svelte +37 -12
- package/dist/components/overlay/FloatingPanel.svelte +8 -3
- package/dist/components/overlay/FloatingPanel.svelte.d.ts +5 -0
- package/dist/components/overlay/controls/Controls.svelte +40 -0
- package/dist/components/overlay/controls/Controls.svelte.d.ts +3 -0
- package/dist/components/overlay/dashboard/Button.svelte +3 -3
- package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
- package/dist/components/overlay/dashboard/Dashboard.svelte +15 -38
- package/dist/components/overlay/widgets/FramePov.svelte +202 -0
- package/dist/components/overlay/widgets/FramePov.svelte.d.ts +6 -0
- package/dist/ecs/hierarchy.d.ts +16 -0
- package/dist/ecs/hierarchy.js +36 -14
- package/dist/ecs/traits.js +2 -0
- package/dist/ecs/worldMatrix.js +18 -5
- package/dist/hooks/useControls.svelte.d.ts +3 -2
- package/dist/hooks/useControls.svelte.js +13 -5
- package/dist/hooks/useGeometries.svelte.js +9 -5
- package/dist/hooks/useSettings.svelte.d.ts +1 -0
- package/dist/hooks/useSettings.svelte.js +1 -0
- package/dist/plugins/Skybox/Skybox.svelte +54 -0
- package/dist/plugins/Skybox/Skybox.svelte.d.ts +12 -0
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +2 -0
- package/dist/three/OBBHelper.js +4 -1
- package/dist/three/arrow.js +2 -0
- package/package.json +6 -2
- /package/dist/{plugins → hooks/plugins}/bvh.svelte.d.ts +0 -0
- /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>
|
package/dist/ecs/hierarchy.d.ts
CHANGED
|
@@ -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;
|
package/dist/ecs/hierarchy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {} from 'koota';
|
|
2
2
|
import { ChildOf } from './relations';
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/ecs/traits.js
CHANGED
|
@@ -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);
|
package/dist/ecs/worldMatrix.js
CHANGED
|
@@ -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
|
|
10
|
-
|
|
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
|
|
15
|
+
if (controls && 'zoomTo' in controls)
|
|
16
|
+
controls?.zoomTo(zoom);
|
|
14
17
|
};
|
|
15
18
|
const setInitialPose = () => {
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
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
|
-
|
|
101
|
-
|
|
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.
|
|
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;
|
|
@@ -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';
|
package/dist/three/OBBHelper.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/three/arrow.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|