@viamrobotics/motion-tools 1.27.1 → 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 +11 -3
- 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 -5
- 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/worldMatrix.js +18 -5
- package/dist/hooks/useControls.svelte.d.ts +3 -2
- package/dist/hooks/useControls.svelte.js +13 -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/package.json +5 -1
- /package/dist/{plugins → hooks/plugins}/bvh.svelte.d.ts +0 -0
- /package/dist/{plugins → hooks/plugins}/bvh.svelte.js +0 -0
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
|
|
11
11
|
import { ThemeUtils } from 'svelte-tweakpane-ui'
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
|
|
13
|
+
import Controls from './overlay/controls/Controls.svelte'
|
|
15
14
|
import Dashboard from './overlay/dashboard/Dashboard.svelte'
|
|
16
15
|
import Details from './overlay/Details.svelte'
|
|
17
16
|
import TreeContainer from './overlay/left-pane/TreeContainer.svelte'
|
|
18
17
|
import Settings from './overlay/settings/Settings.svelte'
|
|
19
18
|
import XR from './xr/XR.svelte'
|
|
20
19
|
import { provideWorld } from '../ecs'
|
|
20
|
+
import { type CameraPose, provideCameraControls } from '../hooks/useControls.svelte'
|
|
21
21
|
import {
|
|
22
22
|
type DrawConnectionConfig,
|
|
23
23
|
provideDrawConnectionConfig,
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import Logs from './overlay/Logs.svelte'
|
|
37
37
|
import ArmPositions from './overlay/widgets/ArmPositions.svelte'
|
|
38
38
|
import Camera from './overlay/widgets/Camera.svelte'
|
|
39
|
+
import FramePov from './overlay/widgets/FramePov.svelte'
|
|
39
40
|
import Scene from './Scene.svelte'
|
|
40
41
|
import SceneProviders from './SceneProviders.svelte'
|
|
41
42
|
|
|
@@ -89,8 +90,10 @@
|
|
|
89
90
|
const settings = provideSettings()
|
|
90
91
|
const environment = provideEnvironment()
|
|
91
92
|
const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
|
|
93
|
+
const currentFramePovWidgets = $derived(settings.current.openFramePovWidgets[partID] || [])
|
|
92
94
|
const { isPresenting } = useXR()
|
|
93
95
|
|
|
96
|
+
provideCameraControls(() => cameraPose)
|
|
94
97
|
createPartIDContext(() => partID)
|
|
95
98
|
provideDrawConnectionConfig(() => drawConnectionConfig)
|
|
96
99
|
provideWeblabs()
|
|
@@ -129,7 +132,7 @@
|
|
|
129
132
|
renderMode="on-demand"
|
|
130
133
|
dpr={[1, 2]}
|
|
131
134
|
>
|
|
132
|
-
<SceneProviders
|
|
135
|
+
<SceneProviders>
|
|
133
136
|
{#snippet children({ focus })}
|
|
134
137
|
<Scene>
|
|
135
138
|
{@render appChildren?.()}
|
|
@@ -145,6 +148,7 @@
|
|
|
145
148
|
<div {@attach domPortal(root)}>
|
|
146
149
|
<FileDrop />
|
|
147
150
|
<Dashboard {dashboard} />
|
|
151
|
+
<Controls />
|
|
148
152
|
<Details {details} />
|
|
149
153
|
|
|
150
154
|
{#if environment.current.isStandalone}
|
|
@@ -163,6 +167,10 @@
|
|
|
163
167
|
{#each currentRobotCameraWidgets as cameraName (cameraName)}
|
|
164
168
|
<Camera name={cameraName} />
|
|
165
169
|
{/each}
|
|
170
|
+
|
|
171
|
+
{#each currentFramePovWidgets as povFrameName (povFrameName)}
|
|
172
|
+
<FramePov frameName={povFrameName} />
|
|
173
|
+
{/each}
|
|
166
174
|
{/if}
|
|
167
175
|
|
|
168
176
|
<PortalTarget id="dom" />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Struct } from '@viamrobotics/sdk';
|
|
2
2
|
import type { Entity } from 'koota';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
|
-
import type
|
|
4
|
+
import { type CameraPose } from '../hooks/useControls.svelte';
|
|
5
5
|
import { type DrawConnectionConfig } from '../hooks/useDrawConnectionConfig.svelte';
|
|
6
6
|
interface LocalConfigProps {
|
|
7
7
|
current: Struct;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { CameraControls, type CameraControlsRef, Gizmo
|
|
2
|
+
import { CameraControls, type CameraControlsRef, Gizmo } from '@threlte/extras'
|
|
3
3
|
import { MathUtils } from 'three'
|
|
4
4
|
|
|
5
|
-
import Button from './overlay/dashboard/Button.svelte'
|
|
6
5
|
import { useCameraControls, useTransformControls } from '../hooks/useControls.svelte'
|
|
7
6
|
import { useEnvironment } from '../hooks/useEnvironment.svelte'
|
|
8
7
|
|
|
@@ -15,19 +14,6 @@
|
|
|
15
14
|
const inputBindingsEnabled = $derived(environment.current.inputBindingsEnabled)
|
|
16
15
|
</script>
|
|
17
16
|
|
|
18
|
-
<Portal id="dashboard">
|
|
19
|
-
<fieldset>
|
|
20
|
-
<Button
|
|
21
|
-
active
|
|
22
|
-
icon="camera-outline"
|
|
23
|
-
description="Reset camera"
|
|
24
|
-
onclick={() => {
|
|
25
|
-
cameraControls.setInitialPose()
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
</fieldset>
|
|
29
|
-
</Portal>
|
|
30
|
-
|
|
31
17
|
<CameraControls
|
|
32
18
|
enabled={!transformControls.active}
|
|
33
19
|
oncreate={(ref) => {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { T } from '@threlte/core'
|
|
3
|
-
import { Gizmo,
|
|
3
|
+
import { Gizmo, TrackballControls } from '@threlte/extras'
|
|
4
4
|
import { Box3, type Object3D, Vector3 } from 'three'
|
|
5
|
-
import { TrackballControls as ThreeTrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
|
|
6
5
|
|
|
7
|
-
import
|
|
6
|
+
import { useCameraControls } from '../hooks/useControls.svelte'
|
|
8
7
|
|
|
9
8
|
import Camera from './Camera.svelte'
|
|
10
9
|
|
|
@@ -14,14 +13,14 @@
|
|
|
14
13
|
|
|
15
14
|
let { object3d }: Props = $props()
|
|
16
15
|
|
|
16
|
+
const cameraControls = useCameraControls()
|
|
17
|
+
|
|
17
18
|
const box = new Box3()
|
|
18
19
|
const vec = new Vector3()
|
|
19
20
|
|
|
20
21
|
let center = $state.raw<[number, number, number]>([0, 0, 0])
|
|
21
22
|
let size = $state.raw<[number, number, number]>([0, 0, 0])
|
|
22
23
|
|
|
23
|
-
let controls = $state.raw<ThreeTrackballControls>()
|
|
24
|
-
|
|
25
24
|
$effect.pre(() => {
|
|
26
25
|
box.setFromObject(object3d)
|
|
27
26
|
size = box.getSize(vec).toArray()
|
|
@@ -29,23 +28,10 @@
|
|
|
29
28
|
})
|
|
30
29
|
</script>
|
|
31
30
|
|
|
32
|
-
<Portal id="dashboard">
|
|
33
|
-
<fieldset>
|
|
34
|
-
<Button
|
|
35
|
-
active
|
|
36
|
-
icon="camera-outline"
|
|
37
|
-
description="Reset camera"
|
|
38
|
-
onclick={() => {
|
|
39
|
-
controls?.reset()
|
|
40
|
-
}}
|
|
41
|
-
/>
|
|
42
|
-
</fieldset>
|
|
43
|
-
</Portal>
|
|
44
|
-
|
|
45
31
|
<Camera position={[size[0] + 1, size[0] + 1, size[0] + 1]}>
|
|
46
32
|
<TrackballControls
|
|
47
|
-
bind:ref={controls}
|
|
48
33
|
target={center}
|
|
34
|
+
oncreate={(ref) => cameraControls.set(ref)}
|
|
49
35
|
>
|
|
50
36
|
<Gizmo placement="bottom-right" />
|
|
51
37
|
</TrackballControls>
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<div class="flex">
|
|
85
85
|
<Button
|
|
86
86
|
active={enabled}
|
|
87
|
+
class="rounded-r-none"
|
|
87
88
|
icon="ruler"
|
|
88
89
|
description="{enabled ? 'Disable' : 'Enable'} measurement"
|
|
89
90
|
onclick={() => {
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
<Button
|
|
96
97
|
{...triggerProps}
|
|
97
98
|
active={enabled}
|
|
98
|
-
class="border-l-0"
|
|
99
|
+
class="rounded-l-none border-l-0"
|
|
99
100
|
icon="filter-sliders"
|
|
100
101
|
description="Measurement settings"
|
|
101
102
|
/>
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
import Selected from './Selected.svelte'
|
|
13
13
|
import SelectedTransformControls from './SelectedTransformControls.svelte'
|
|
14
14
|
import StaticGeometries from './StaticGeometries.svelte'
|
|
15
|
+
import { bvh } from '../hooks/plugins/bvh.svelte'
|
|
15
16
|
import { useFocusedObject3d } from '../hooks/useSelection.svelte'
|
|
16
17
|
import { useSettings } from '../hooks/useSettings.svelte'
|
|
17
|
-
import { bvh } from '../plugins/bvh.svelte'
|
|
18
18
|
|
|
19
19
|
import hdrImage from '../assets/ferndale_studio_11_1k.hdr'
|
|
20
20
|
import BatchedArrows from './BatchedArrows.svelte'
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
import { provideArmClient } from '../hooks/useArmClient.svelte'
|
|
7
7
|
import { provideArmKinematics } from '../hooks/useArmKinematics.svelte'
|
|
8
8
|
import { provideConfigFrames } from '../hooks/useConfigFrames.svelte'
|
|
9
|
-
import {
|
|
10
|
-
type CameraPose,
|
|
11
|
-
provideCameraControls,
|
|
12
|
-
provideTransformControls,
|
|
13
|
-
} from '../hooks/useControls.svelte'
|
|
9
|
+
import { provideTransformControls } from '../hooks/useControls.svelte'
|
|
14
10
|
import { provideDrawAPI } from '../hooks/useDrawAPI.svelte'
|
|
15
11
|
import { provideDrawService } from '../hooks/useDrawService.svelte'
|
|
16
12
|
import { provideFrameEditSession } from '../hooks/useFrameEditSession.svelte'
|
|
@@ -30,15 +26,13 @@
|
|
|
30
26
|
import { provideOrigin } from './xr/useOrigin.svelte'
|
|
31
27
|
|
|
32
28
|
interface Props {
|
|
33
|
-
cameraPose?: CameraPose
|
|
34
29
|
children: Snippet<[{ focus: boolean }]>
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
let {
|
|
32
|
+
let { children }: Props = $props()
|
|
38
33
|
|
|
39
34
|
const partID = usePartID()
|
|
40
35
|
|
|
41
|
-
provideCameraControls(() => cameraPose)
|
|
42
36
|
provideTransformControls()
|
|
43
37
|
provideLogs()
|
|
44
38
|
|
|
@@ -263,16 +263,18 @@
|
|
|
263
263
|
|
|
264
264
|
const currentControls = controls.current
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
if ('minPolarAngle' in currentControls) {
|
|
267
|
+
const { minPolarAngle, maxPolarAngle } = currentControls
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
// Locks the camera to top down while this component is mounted
|
|
270
|
+
currentControls.polarAngle = 0
|
|
271
|
+
currentControls.minPolarAngle = 0
|
|
272
|
+
currentControls.maxPolarAngle = 0
|
|
272
273
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
return () => {
|
|
275
|
+
currentControls.minPolarAngle = minPolarAngle
|
|
276
|
+
currentControls.maxPolarAngle = maxPolarAngle
|
|
277
|
+
}
|
|
276
278
|
}
|
|
277
279
|
})
|
|
278
280
|
|
|
@@ -244,16 +244,18 @@
|
|
|
244
244
|
|
|
245
245
|
const currentControls = controls.current
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
if ('minPolarAngle' in currentControls) {
|
|
248
|
+
const { minPolarAngle, maxPolarAngle } = currentControls
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
// Locks the camera to top down while this component is mounted
|
|
251
|
+
currentControls.polarAngle = 0
|
|
252
|
+
currentControls.minPolarAngle = 0
|
|
253
|
+
currentControls.maxPolarAngle = 0
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
return () => {
|
|
256
|
+
currentControls.minPolarAngle = minPolarAngle
|
|
257
|
+
currentControls.maxPolarAngle = maxPolarAngle
|
|
258
|
+
}
|
|
257
259
|
}
|
|
258
260
|
})
|
|
259
261
|
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import { useEnvironment } from '../../hooks/useEnvironment.svelte'
|
|
46
46
|
import { useLinkedEntities } from '../../hooks/useLinked.svelte'
|
|
47
47
|
import { usePartConfig } from '../../hooks/usePartConfig.svelte'
|
|
48
|
+
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
48
49
|
import { useResourceByName } from '../../hooks/useResourceByName.svelte'
|
|
49
50
|
import {
|
|
50
51
|
useFocusedEntity,
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
useSelectedEntity,
|
|
53
54
|
useSelectedObject3d,
|
|
54
55
|
} from '../../hooks/useSelection.svelte'
|
|
56
|
+
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
55
57
|
import { createPose, matrixToPose } from '../../transform'
|
|
56
58
|
|
|
57
59
|
interface Props {
|
|
@@ -67,6 +69,8 @@
|
|
|
67
69
|
const resourceByName = useResourceByName()
|
|
68
70
|
const configFrames = useConfigFrames()
|
|
69
71
|
const partConfig = usePartConfig()
|
|
72
|
+
const partID = usePartID()
|
|
73
|
+
const settings = useSettings()
|
|
70
74
|
const selectedEntity = useSelectedEntity()
|
|
71
75
|
const selectedObject3d = useSelectedObject3d()
|
|
72
76
|
const environment = useEnvironment()
|
|
@@ -331,11 +335,13 @@
|
|
|
331
335
|
onclick={() => {
|
|
332
336
|
const padding = 0.4
|
|
333
337
|
|
|
334
|
-
|
|
338
|
+
const currentControls = controls.current
|
|
335
339
|
|
|
336
|
-
|
|
340
|
+
if (!currentControls || !('fitToBox' in currentControls)) return
|
|
337
341
|
|
|
338
|
-
|
|
342
|
+
const { azimuthAngle, polarAngle } = currentControls
|
|
343
|
+
|
|
344
|
+
currentControls.fitToBox(object3d, true, {
|
|
339
345
|
paddingTop: padding,
|
|
340
346
|
paddingBottom: padding,
|
|
341
347
|
paddingLeft: padding,
|
|
@@ -343,8 +349,8 @@
|
|
|
343
349
|
})
|
|
344
350
|
|
|
345
351
|
// Preserve previous rotation
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
currentControls.rotateAzimuthTo(azimuthAngle, true)
|
|
353
|
+
currentControls.rotatePolarTo(polarAngle, true)
|
|
348
354
|
}}
|
|
349
355
|
>
|
|
350
356
|
<Icon name="image-filter-center-focus" />
|
|
@@ -353,6 +359,32 @@
|
|
|
353
359
|
</Tooltip>
|
|
354
360
|
{/if}
|
|
355
361
|
|
|
362
|
+
{#if name.current}
|
|
363
|
+
<Tooltip
|
|
364
|
+
let:tooltipID
|
|
365
|
+
location="bottom"
|
|
366
|
+
>
|
|
367
|
+
<button
|
|
368
|
+
class="text-subtle-2"
|
|
369
|
+
aria-describedby={tooltipID}
|
|
370
|
+
aria-label="Open view from this frame"
|
|
371
|
+
onclick={() => {
|
|
372
|
+
const frameName = name.current
|
|
373
|
+
if (!frameName) return
|
|
374
|
+
const list = settings.current.openFramePovWidgets[partID.current] ?? []
|
|
375
|
+
if (list.includes(frameName)) return
|
|
376
|
+
settings.current.openFramePovWidgets = {
|
|
377
|
+
...settings.current.openFramePovWidgets,
|
|
378
|
+
[partID.current]: [...list, frameName],
|
|
379
|
+
}
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<Icon name="camera-outline" />
|
|
383
|
+
</button>
|
|
384
|
+
<p slot="description">View from this frame</p>
|
|
385
|
+
</Tooltip>
|
|
386
|
+
{/if}
|
|
387
|
+
|
|
356
388
|
{#if removable.current}
|
|
357
389
|
<Tooltip
|
|
358
390
|
let:tooltipID
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { ClassValue } from 'svelte/elements'
|
|
3
4
|
|
|
4
5
|
import { useThrelte } from '@threlte/core'
|
|
5
6
|
import { Icon } from '@viamrobotics/prime-core'
|
|
@@ -14,6 +15,9 @@
|
|
|
14
15
|
resizable?: boolean
|
|
15
16
|
persistRect?: boolean
|
|
16
17
|
isOpen?: boolean
|
|
18
|
+
bodyClass?: ClassValue
|
|
19
|
+
onPositionChange?: (details: floatingPanel.PositionChangeDetails) => void
|
|
20
|
+
onSizeChange?: (details: floatingPanel.SizeChangeDetails) => void
|
|
17
21
|
children: Snippet
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -25,6 +29,7 @@
|
|
|
25
29
|
resizable = false,
|
|
26
30
|
persistRect = true,
|
|
27
31
|
isOpen = $bindable(false),
|
|
32
|
+
bodyClass = 'bg-white',
|
|
28
33
|
children,
|
|
29
34
|
...props
|
|
30
35
|
}: Props = $props()
|
|
@@ -56,7 +61,7 @@
|
|
|
56
61
|
>
|
|
57
62
|
<div
|
|
58
63
|
{...api.getContentProps()}
|
|
59
|
-
class="border-medium border-1
|
|
64
|
+
class="border-medium border-1 dark:text-black"
|
|
60
65
|
>
|
|
61
66
|
<div
|
|
62
67
|
{...api.getDragTriggerProps()}
|
|
@@ -64,7 +69,7 @@
|
|
|
64
69
|
>
|
|
65
70
|
<div
|
|
66
71
|
{...api.getHeaderProps()}
|
|
67
|
-
class="border-medium flex items-center justify-between border-b p-2"
|
|
72
|
+
class="border-medium flex items-center justify-between border-b bg-white p-2"
|
|
68
73
|
>
|
|
69
74
|
<h3
|
|
70
75
|
{...api.getTitleProps()}
|
|
@@ -97,7 +102,7 @@
|
|
|
97
102
|
-->
|
|
98
103
|
<div
|
|
99
104
|
{...api.getBodyProps()}
|
|
100
|
-
class=
|
|
105
|
+
class={['relative h-[calc(100%-33px)]', bodyClass]}
|
|
101
106
|
>
|
|
102
107
|
{#if isOpen}
|
|
103
108
|
{@render children()}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ClassValue } from 'svelte/elements';
|
|
3
|
+
import * as floatingPanel from '@zag-js/floating-panel';
|
|
2
4
|
interface Props {
|
|
3
5
|
title?: string;
|
|
4
6
|
defaultSize?: {
|
|
@@ -13,6 +15,9 @@ interface Props {
|
|
|
13
15
|
resizable?: boolean;
|
|
14
16
|
persistRect?: boolean;
|
|
15
17
|
isOpen?: boolean;
|
|
18
|
+
bodyClass?: ClassValue;
|
|
19
|
+
onPositionChange?: (details: floatingPanel.PositionChangeDetails) => void;
|
|
20
|
+
onSizeChange?: (details: floatingPanel.SizeChangeDetails) => void;
|
|
16
21
|
children: Snippet;
|
|
17
22
|
}
|
|
18
23
|
declare const FloatingPanel: import("svelte").Component<Props, {}, "isOpen">;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { PortalTarget } from '@threlte/extras'
|
|
3
|
+
|
|
4
|
+
import Button from '../dashboard/Button.svelte'
|
|
5
|
+
import { useCameraControls } from '../../../hooks/useControls.svelte'
|
|
6
|
+
import { useSettings } from '../../../hooks/useSettings.svelte'
|
|
7
|
+
|
|
8
|
+
const settings = useSettings()
|
|
9
|
+
const cameraControls = useCameraControls()
|
|
10
|
+
|
|
11
|
+
const isOrthographic = $derived(settings.current.cameraMode === 'orthographic')
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="absolute right-2 bottom-26 z-4 flex flex-col items-end gap-2">
|
|
15
|
+
<PortalTarget id="controls" />
|
|
16
|
+
|
|
17
|
+
<fieldset class="flex flex-col">
|
|
18
|
+
<Button
|
|
19
|
+
active
|
|
20
|
+
class="rounded-b-none"
|
|
21
|
+
icon="camera-outline"
|
|
22
|
+
description="Reset camera"
|
|
23
|
+
tooltipLocation="left"
|
|
24
|
+
onclick={() => {
|
|
25
|
+
cameraControls.setInitialPose()
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
<Button
|
|
29
|
+
active
|
|
30
|
+
class="-my-0.5 rounded-t-none"
|
|
31
|
+
icon={isOrthographic ? 'grid-orthographic' : 'grid-perspective'}
|
|
32
|
+
description={isOrthographic ? 'Switch to perspective view' : 'Switch to orthographic view'}
|
|
33
|
+
hotkey="C"
|
|
34
|
+
tooltipLocation="left"
|
|
35
|
+
onclick={() => {
|
|
36
|
+
settings.current.cameraMode = isOrthographic ? 'perspective' : 'orthographic'
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
</fieldset>
|
|
40
|
+
</div>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
description: string
|
|
11
11
|
hotkey?: string
|
|
12
12
|
class?: ClassValue | null | undefined
|
|
13
|
-
tooltipLocation?: 'bottom' | 'right'
|
|
13
|
+
tooltipLocation?: 'bottom' | 'right' | 'left' | 'top'
|
|
14
14
|
onclick?: MouseEventHandler<HTMLButtonElement> | null | undefined
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
<label
|
|
34
34
|
class={[
|
|
35
35
|
className,
|
|
36
|
-
'relative block border',
|
|
36
|
+
'relative block rounded-md border',
|
|
37
37
|
active ? 'border-gray-5 text-gray-8 z-4 bg-white' : 'bg-light border-medium text-disabled',
|
|
38
38
|
]}
|
|
39
39
|
aria-describedby={tooltipID}
|
|
40
40
|
>
|
|
41
41
|
<button
|
|
42
|
-
class="p-1.5"
|
|
42
|
+
class=" p-1.5"
|
|
43
43
|
role="radio"
|
|
44
44
|
aria-label={description}
|
|
45
45
|
aria-checked={active}
|
|
@@ -6,7 +6,7 @@ interface Props extends HTMLButtonAttributes {
|
|
|
6
6
|
description: string;
|
|
7
7
|
hotkey?: string;
|
|
8
8
|
class?: ClassValue | null | undefined;
|
|
9
|
-
tooltipLocation?: 'bottom' | 'right';
|
|
9
|
+
tooltipLocation?: 'bottom' | 'right' | 'left' | 'top';
|
|
10
10
|
onclick?: MouseEventHandler<HTMLButtonElement> | null | undefined;
|
|
11
11
|
}
|
|
12
12
|
declare const Button: import("svelte").Component<Props, {}, "">;
|
|
@@ -14,33 +14,11 @@
|
|
|
14
14
|
class="absolute top-2 z-4 flex w-full items-center justify-center gap-2"
|
|
15
15
|
{...rest}
|
|
16
16
|
>
|
|
17
|
-
<!-- camera view -->
|
|
18
|
-
<fieldset class="flex">
|
|
19
|
-
<Button
|
|
20
|
-
icon="grid-orthographic"
|
|
21
|
-
active={settings.current.cameraMode === 'orthographic'}
|
|
22
|
-
description="Orthographic view"
|
|
23
|
-
hotkey="C"
|
|
24
|
-
onclick={() => {
|
|
25
|
-
settings.current.cameraMode = 'orthographic'
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
<Button
|
|
29
|
-
icon="grid-perspective"
|
|
30
|
-
active={settings.current.cameraMode === 'perspective'}
|
|
31
|
-
description="Perspective view"
|
|
32
|
-
hotkey="C"
|
|
33
|
-
class="-ml-px"
|
|
34
|
-
onclick={() => {
|
|
35
|
-
settings.current.cameraMode = 'perspective'
|
|
36
|
-
}}
|
|
37
|
-
/>
|
|
38
|
-
</fieldset>
|
|
39
|
-
|
|
40
17
|
<!-- transform -->
|
|
41
18
|
<fieldset class="flex">
|
|
42
19
|
<Button
|
|
43
20
|
icon="mouse-pointer"
|
|
21
|
+
class="rounded-r-none"
|
|
44
22
|
active={settings.current.transformMode === 'none'}
|
|
45
23
|
description="No transform controls"
|
|
46
24
|
hotkey="0"
|
|
@@ -50,30 +28,30 @@
|
|
|
50
28
|
/>
|
|
51
29
|
<Button
|
|
52
30
|
icon="cursor-move"
|
|
31
|
+
class="-ml-px rounded-none"
|
|
53
32
|
active={settings.current.transformMode === 'translate'}
|
|
54
33
|
description="Translate"
|
|
55
34
|
hotkey="1"
|
|
56
|
-
class="-ml-px"
|
|
57
35
|
onclick={() => {
|
|
58
36
|
settings.current.transformMode = 'translate'
|
|
59
37
|
}}
|
|
60
38
|
/>
|
|
61
39
|
<Button
|
|
62
40
|
icon="sync"
|
|
41
|
+
class="-ml-px rounded-none"
|
|
63
42
|
active={settings.current.transformMode === 'rotate'}
|
|
64
43
|
description="Rotate"
|
|
65
44
|
hotkey="2"
|
|
66
|
-
class="-ml-px"
|
|
67
45
|
onclick={() => {
|
|
68
46
|
settings.current.transformMode = 'rotate'
|
|
69
47
|
}}
|
|
70
48
|
/>
|
|
71
49
|
<Button
|
|
72
50
|
icon="resize"
|
|
51
|
+
class="-ml-px rounded-l-none"
|
|
73
52
|
active={settings.current.transformMode === 'scale'}
|
|
74
53
|
description="Scale"
|
|
75
54
|
hotkey="3"
|
|
76
|
-
class="-ml-px"
|
|
77
55
|
onclick={() => {
|
|
78
56
|
settings.current.transformMode = 'scale'
|
|
79
57
|
}}
|
|
@@ -81,18 +59,17 @@
|
|
|
81
59
|
</fieldset>
|
|
82
60
|
|
|
83
61
|
<!-- snapping -->
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{/if}
|
|
62
|
+
|
|
63
|
+
<fieldset class="flex">
|
|
64
|
+
<Button
|
|
65
|
+
icon={settings.current.snapping ? 'magnet' : 'magnet-off'}
|
|
66
|
+
active={settings.current.snapping}
|
|
67
|
+
description="Snapping"
|
|
68
|
+
onclick={() => {
|
|
69
|
+
settings.current.snapping = !settings.current.snapping
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</fieldset>
|
|
96
73
|
|
|
97
74
|
<PortalTarget id="dashboard" />
|
|
98
75
|
|
|
@@ -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/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();
|
|
@@ -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/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",
|
|
@@ -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
|