@viamrobotics/motion-tools 1.31.0 → 1.33.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 +64 -53
- package/dist/components/App.svelte.d.ts +14 -7
- package/dist/components/Entities/Arrows/Arrows.svelte +4 -7
- package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +0 -1
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +30 -16
- package/dist/components/InputBindings.svelte +0 -43
- package/dist/components/KeyboardBindings.svelte +38 -0
- package/dist/components/KeyboardBindings.svelte.d.ts +18 -0
- package/dist/components/PointerMissBox.svelte +6 -3
- package/dist/components/Scene.svelte +43 -61
- package/dist/components/SceneProviders.svelte +2 -7
- package/dist/components/SceneProviders.svelte.d.ts +1 -3
- package/dist/components/Selected.svelte +20 -27
- package/dist/components/SelectedTransformControls.svelte +8 -7
- package/dist/components/StaticGeometries.svelte +3 -5
- package/dist/components/hover/HoveredEntities.svelte +15 -14
- package/dist/components/hover/HoveredEntities.svelte.d.ts +17 -2
- package/dist/components/hover/HoveredEntity.svelte +8 -5
- package/dist/components/hover/HoveredEntity.svelte.d.ts +5 -1
- package/dist/components/hover/LinkedHoveredEntity.svelte +7 -11
- package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +1 -0
- package/dist/components/overlay/Details.svelte +22 -37
- package/dist/components/overlay/Details.svelte.d.ts +3 -1
- package/dist/components/overlay/controls/Controls.svelte +0 -2
- package/dist/components/overlay/dashboard/Button.svelte +5 -3
- package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
- package/dist/components/overlay/left-pane/Tree.svelte +13 -10
- package/dist/components/overlay/left-pane/TreeContainer.svelte +9 -4
- package/dist/components/overlay/left-pane/TreeNode.svelte +6 -4
- package/dist/components/overlay/settings/ConnectionSettings.svelte +42 -0
- package/dist/components/overlay/settings/ConnectionSettings.svelte.d.ts +18 -0
- package/dist/components/overlay/settings/DebugSettings.svelte +13 -0
- package/dist/components/{xr/frame-configure/Controllers.svelte.d.ts → overlay/settings/DebugSettings.svelte.d.ts} +3 -3
- package/dist/components/overlay/settings/PointcloudSettings.svelte +61 -0
- package/dist/components/overlay/settings/PointcloudSettings.svelte.d.ts +3 -0
- package/dist/components/overlay/settings/SceneSettings.svelte +110 -0
- package/dist/components/overlay/settings/SceneSettings.svelte.d.ts +18 -0
- package/dist/components/overlay/settings/Settings.svelte +27 -312
- package/dist/components/overlay/settings/Settings.svelte.d.ts +8 -1
- package/dist/components/overlay/settings/Tabs.svelte +5 -3
- package/dist/components/overlay/settings/Tabs.svelte.d.ts +3 -3
- package/dist/components/overlay/settings/VisionSettings.svelte +31 -0
- package/dist/components/overlay/settings/VisionSettings.svelte.d.ts +3 -0
- package/dist/components/overlay/settings/WeblabSettings.svelte +27 -0
- package/dist/components/overlay/settings/WeblabSettings.svelte.d.ts +18 -0
- package/dist/components/overlay/settings/WidgetSettings.svelte +49 -0
- package/dist/components/overlay/settings/WidgetSettings.svelte.d.ts +3 -0
- package/dist/components/overlay/widgets/FramePov.svelte +1 -12
- package/dist/draw.d.ts +1 -0
- package/dist/draw.js +1 -1
- package/dist/ecs/index.d.ts +1 -0
- package/dist/ecs/index.js +1 -0
- package/dist/ecs/traits.d.ts +22 -5
- package/dist/ecs/traits.js +33 -4
- package/dist/ecs/useTag.svelte.d.ts +5 -0
- package/dist/ecs/useTag.svelte.js +43 -0
- package/dist/hooks/useEnvironment.svelte.d.ts +1 -1
- package/dist/hooks/useLinked.svelte.js +7 -8
- package/dist/hooks/useMouseRaycaster.svelte.d.ts +4 -3
- package/dist/hooks/useMouseRaycaster.svelte.js +1 -0
- package/dist/hooks/useSettings.svelte.d.ts +1 -1
- package/dist/plugins/Focus/Focus.svelte +45 -0
- package/dist/plugins/Focus/Focus.svelte.d.ts +3 -0
- package/dist/plugins/Focus/FocusBox.svelte +75 -0
- package/dist/plugins/Focus/FocusBox.svelte.d.ts +3 -0
- package/dist/plugins/Focus/provideFocus.svelte.d.ts +1 -0
- package/dist/plugins/Focus/provideFocus.svelte.js +61 -0
- package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte +6 -8
- package/dist/plugins/Selection/SelectionTool.svelte +10 -3
- package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte +3 -5
- package/dist/plugins/XR/DebugPanel.svelte +29 -0
- package/dist/plugins/XR/DebugPanel.svelte.d.ts +3 -0
- package/dist/plugins/XR/OriginMarker.svelte +341 -0
- package/dist/plugins/XR/PendingEditsPanel.svelte +60 -0
- package/dist/plugins/XR/PendingEditsPanel.svelte.d.ts +18 -0
- package/dist/plugins/XR/WristDisplay.svelte +60 -0
- package/dist/plugins/XR/WristDisplay.svelte.d.ts +19 -0
- package/dist/{components/xr → plugins/XR}/XR.svelte +69 -23
- package/dist/plugins/XR/XRPlugins.svelte +9 -0
- package/dist/plugins/XR/XRPlugins.svelte.d.ts +26 -0
- package/dist/plugins/XR/XRSettings.svelte +240 -0
- package/dist/plugins/XR/XRSettings.svelte.d.ts +3 -0
- package/dist/{components/xr → plugins/XR}/XRToast.svelte +6 -9
- package/dist/plugins/XR/debug.svelte.d.ts +7 -0
- package/dist/plugins/XR/debug.svelte.js +13 -0
- package/dist/plugins/XR/frame-configure/Controllers.svelte +413 -0
- package/dist/plugins/XR/teleop/Controllers.svelte.d.ts +3 -0
- package/dist/{components/xr → plugins/XR}/useAnchors.svelte.d.ts +4 -0
- package/dist/{components/xr → plugins/XR}/useAnchors.svelte.js +22 -0
- package/dist/plugins/XR/useOrigin.svelte.d.ts +24 -0
- package/dist/plugins/XR/useOrigin.svelte.js +50 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +4 -0
- package/dist/three/OBBHelper.js +1 -0
- package/dist/three/arrow.d.ts +2 -0
- package/dist/three/arrow.js +3 -1
- package/package.json +16 -4
- package/dist/components/Focus.svelte +0 -46
- package/dist/components/Focus.svelte.d.ts +0 -7
- package/dist/components/xr/OriginMarker.svelte +0 -151
- package/dist/components/xr/XRControllerSettings.svelte +0 -242
- package/dist/components/xr/XRControllerSettings.svelte.d.ts +0 -3
- package/dist/components/xr/frame-configure/Controllers.svelte +0 -6
- package/dist/components/xr/useOrigin.svelte.d.ts +0 -9
- package/dist/components/xr/useOrigin.svelte.js +0 -27
- package/dist/hooks/useSelection.svelte.d.ts +0 -33
- package/dist/hooks/useSelection.svelte.js +0 -94
- /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte +0 -0
- /package/dist/{components → plugins}/MeasureTool/MeasurePoint.svelte.d.ts +0 -0
- /package/dist/{components → plugins}/MeasureTool/MeasureTool.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte +0 -0
- /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte +0 -0
- /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte +0 -0
- /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/OriginMarker.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/PointDistance.svelte +0 -0
- /package/dist/{components/xr → plugins/XR}/PointDistance.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/XR.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte +0 -0
- /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/XRToast.svelte.d.ts +0 -0
- /package/dist/{components/xr/teleop → plugins/XR/frame-configure}/Controllers.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/math.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/math.js +0 -0
- /package/dist/{components/xr → plugins/XR}/teleop/Controllers.svelte +0 -0
- /package/dist/{components/xr → plugins/XR}/toasts.svelte.d.ts +0 -0
- /package/dist/{components/xr → plugins/XR}/toasts.svelte.js +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { trait } from 'koota';
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { relations, traits, useQuery, useWorld } from '../../ecs';
|
|
4
|
+
const HiddenByFocus = trait();
|
|
5
|
+
export const provideFocus = (focusing) => {
|
|
6
|
+
const world = useWorld();
|
|
7
|
+
const selected = useQuery(traits.Selected);
|
|
8
|
+
$effect(() => {
|
|
9
|
+
if (!focusing()) {
|
|
10
|
+
for (const entity of world.query(HiddenByFocus)) {
|
|
11
|
+
entity.remove(HiddenByFocus, traits.Invisible);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot the selection at the moment focus is entered. Reading it
|
|
17
|
+
* untracked makes `focusing()` this effect's only dependency, so the
|
|
18
|
+
* focused view stays frozen: selecting or deselecting entities while
|
|
19
|
+
* focused must not change what's hidden. Everything is restored when
|
|
20
|
+
* focus exits.
|
|
21
|
+
*/
|
|
22
|
+
const selectedEntities = untrack(() => selected.current);
|
|
23
|
+
/**
|
|
24
|
+
* Entities only render when their `InheritedInvisible` is unset, and that
|
|
25
|
+
* trait is computed by walking `ChildOf` ancestors (see
|
|
26
|
+
* `useInheritedInvisible`). So hiding a selected entity's parent — or its
|
|
27
|
+
* renderable sub-entities, which are `ChildOf` children that never carry
|
|
28
|
+
* `Selected` — makes the selection itself disappear. Keep the whole
|
|
29
|
+
* connected subtree of each selection visible: its ancestors (so the
|
|
30
|
+
* cascade can't reach it) and its descendants (so its geometry shows).
|
|
31
|
+
*/
|
|
32
|
+
const keep = new Set();
|
|
33
|
+
const keepSubtree = (entity) => {
|
|
34
|
+
if (keep.has(entity))
|
|
35
|
+
return;
|
|
36
|
+
keep.add(entity);
|
|
37
|
+
for (const child of world.query(relations.ChildOf(entity))) {
|
|
38
|
+
keepSubtree(child);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
for (const entity of selectedEntities) {
|
|
42
|
+
let ancestor = entity.targetFor(relations.ChildOf);
|
|
43
|
+
while (ancestor?.isAlive()) {
|
|
44
|
+
keep.add(ancestor);
|
|
45
|
+
ancestor = ancestor.targetFor(relations.ChildOf);
|
|
46
|
+
}
|
|
47
|
+
keepSubtree(entity);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Hide the rest. Skip already-invisible entities so we don't take
|
|
51
|
+
* ownership of — and later wrongly reveal — user-hidden entities.
|
|
52
|
+
*/
|
|
53
|
+
for (const entity of world.query(traits.Name)) {
|
|
54
|
+
if (keep.has(entity))
|
|
55
|
+
continue;
|
|
56
|
+
if (!entity.has(traits.Invisible)) {
|
|
57
|
+
entity.add(HiddenByFocus, traits.Invisible);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
@@ -4,16 +4,14 @@
|
|
|
4
4
|
import { untrack } from 'svelte'
|
|
5
5
|
import { type Intersection, Vector3 } from 'three'
|
|
6
6
|
|
|
7
|
-
import Button from '
|
|
7
|
+
import Button from '../../components/overlay/dashboard/Button.svelte'
|
|
8
|
+
import Popover from '../../components/overlay/Popover.svelte'
|
|
9
|
+
import ToggleGroup from '../../components/overlay/ToggleGroup.svelte'
|
|
8
10
|
import { useMouseRaycaster } from '../../hooks/useMouseRaycaster.svelte'
|
|
9
|
-
import { useFocusedEntity } from '../../hooks/useSelection.svelte'
|
|
10
11
|
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
11
12
|
|
|
12
|
-
import Popover from '../overlay/Popover.svelte'
|
|
13
|
-
import ToggleGroup from '../overlay/ToggleGroup.svelte'
|
|
14
13
|
import MeasurePoint from './MeasurePoint.svelte'
|
|
15
14
|
|
|
16
|
-
const focusedEntity = useFocusedEntity()
|
|
17
15
|
const settings = useSettings()
|
|
18
16
|
|
|
19
17
|
const htmlPosition = new Vector3()
|
|
@@ -73,9 +71,9 @@
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
$effect(() => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
if (!enabled) {
|
|
75
|
+
untrack(() => clear())
|
|
76
|
+
}
|
|
79
77
|
})
|
|
80
78
|
</script>
|
|
81
79
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import DashboardButton from '../../components/overlay/dashboard/Button.svelte'
|
|
10
10
|
import Popover from '../../components/overlay/Popover.svelte'
|
|
11
11
|
import ToggleGroup from '../../components/overlay/ToggleGroup.svelte'
|
|
12
|
-
import {
|
|
12
|
+
import { traits, useWorld } from '../../ecs'
|
|
13
13
|
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
14
14
|
|
|
15
15
|
import Ellipse from './Ellipse.svelte'
|
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
let { enabled = false, autoSelectNewEntities = false, children }: Props = $props()
|
|
30
30
|
|
|
31
31
|
const { dom } = useThrelte()
|
|
32
|
+
const world = useWorld()
|
|
32
33
|
const settings = useSettings()
|
|
33
34
|
const isSelectionMode = $derived(settings.current.interactionMode === 'select')
|
|
34
35
|
|
|
35
36
|
const selectionPlugin = provideSelectionPlugin()
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
let selectionType = $state<SelectionType>('lasso')
|
|
38
39
|
|
|
39
40
|
$effect(() => {
|
|
@@ -58,7 +59,13 @@
|
|
|
58
59
|
|
|
59
60
|
const newest = newEntities.at(-1)
|
|
60
61
|
if (newest === undefined) return
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
const selected = world.query(traits.Selected)
|
|
64
|
+
for (const entity of selected) {
|
|
65
|
+
entity.remove(traits.Selected)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
newest.add(traits.Selected)
|
|
62
69
|
})
|
|
63
70
|
|
|
64
71
|
const rect = new ElementRect(() => dom)
|
|
@@ -6,14 +6,12 @@
|
|
|
6
6
|
import { createResourceClient } from '@viamrobotics/svelte-sdk'
|
|
7
7
|
import { Quaternion, Vector3 } from 'three'
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
calculatePositionTarget,
|
|
11
|
-
getFrameTransformationQuaternion,
|
|
12
|
-
} from './math'
|
|
13
|
-
import { xrToast } from './toasts.svelte'
|
|
14
9
|
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
15
10
|
import { OrientationVector } from '../../three/OrientationVector'
|
|
16
11
|
|
|
12
|
+
import { calculatePositionTarget, getFrameTransformationQuaternion } from './math'
|
|
13
|
+
import { xrToast } from './toasts.svelte'
|
|
14
|
+
|
|
17
15
|
interface Props {
|
|
18
16
|
armName: string
|
|
19
17
|
gripperName?: string
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Text } from 'threlte-uikit'
|
|
3
|
+
import { Panel } from 'threlte-uikit/horizon'
|
|
4
|
+
|
|
5
|
+
import { xrDebug } from './debug.svelte'
|
|
6
|
+
import WristDisplay from './WristDisplay.svelte'
|
|
7
|
+
|
|
8
|
+
const messages = $derived(xrDebug.messages)
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<WristDisplay position={[0, 0.005, 0.2]}>
|
|
12
|
+
<Panel
|
|
13
|
+
flexDirection="column"
|
|
14
|
+
padding={12}
|
|
15
|
+
gap={4}
|
|
16
|
+
backgroundColor="#0a0a0a"
|
|
17
|
+
borderRadius={8}
|
|
18
|
+
minWidth={400}
|
|
19
|
+
minHeight={40}
|
|
20
|
+
>
|
|
21
|
+
{#each messages as message, i (i)}
|
|
22
|
+
<Text
|
|
23
|
+
text={message}
|
|
24
|
+
fontSize={14}
|
|
25
|
+
color="#ffffff"
|
|
26
|
+
/>
|
|
27
|
+
{/each}
|
|
28
|
+
</Panel>
|
|
29
|
+
</WristDisplay>
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useTask, useThrelte } from '@threlte/core'
|
|
3
|
+
import { useGamepad } from '@threlte/extras'
|
|
4
|
+
import { Hand, useController, useHand, useHeadset, useXR } from '@threlte/xr'
|
|
5
|
+
import { useDebounce } from 'runed'
|
|
6
|
+
import { Euler, Quaternion, Vector3 } from 'three'
|
|
7
|
+
|
|
8
|
+
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
9
|
+
|
|
10
|
+
import { useAnchors } from './useAnchors.svelte'
|
|
11
|
+
import { useOrigin } from './useOrigin.svelte'
|
|
12
|
+
|
|
13
|
+
const origin = useOrigin()
|
|
14
|
+
const anchors = useAnchors()
|
|
15
|
+
const partID = usePartID()
|
|
16
|
+
const headset = useHeadset()
|
|
17
|
+
|
|
18
|
+
const storageKey = $derived(`xr-origin-anchor:${partID.current}`)
|
|
19
|
+
|
|
20
|
+
const DEFAULT_ORIGIN: [number, number, number] = [-1, -1, 0]
|
|
21
|
+
const COMMIT_DEBOUNCE_MS = 500
|
|
22
|
+
|
|
23
|
+
const leftPad = useGamepad({ xr: true, hand: 'left' })
|
|
24
|
+
const rightPad = useGamepad({ xr: true, hand: 'right' })
|
|
25
|
+
const leftController = useController('left')
|
|
26
|
+
const rightController = useController('right')
|
|
27
|
+
|
|
28
|
+
const THUMBSTICK_SPEED = 0.05
|
|
29
|
+
|
|
30
|
+
// Head-relative translation basis. Recomputed per thumbstick tick so
|
|
31
|
+
// stick-forward always moves content away from the viewer, regardless of
|
|
32
|
+
// how the scene was rotated or which way the user is physically facing.
|
|
33
|
+
const headForward = new Vector3()
|
|
34
|
+
const headRight = new Vector3()
|
|
35
|
+
|
|
36
|
+
// The anchor that currently represents the persisted origin. Kept so we can
|
|
37
|
+
// delete it when a new calibration is committed.
|
|
38
|
+
let persistedAnchor: XRAnchor | undefined
|
|
39
|
+
|
|
40
|
+
// The restored anchor we're waiting to localize on session start. Once the
|
|
41
|
+
// device reports it as tracked we snap origin to its pose and clear this.
|
|
42
|
+
let pendingRestore = $state.raw<XRAnchor | undefined>(undefined)
|
|
43
|
+
|
|
44
|
+
const restoreQuat = new Quaternion()
|
|
45
|
+
const restoreEuler = new Euler(0, 0, 0, 'ZYX')
|
|
46
|
+
|
|
47
|
+
const commitVec = new Vector3()
|
|
48
|
+
const commitQuat = new Quaternion()
|
|
49
|
+
|
|
50
|
+
const commit = useDebounce(async () => {
|
|
51
|
+
// origin.position/rotation define the composed XR reference space's
|
|
52
|
+
// offset from zUp, so an anchor at identity in the current (composed)
|
|
53
|
+
// space IS the anchor at origin's pose in zUp. commitVec/commitQuat
|
|
54
|
+
// are left at their default-constructed zero/identity values.
|
|
55
|
+
const anchor = await anchors.createAnchor(commitVec, commitQuat)
|
|
56
|
+
if (!anchor) return
|
|
57
|
+
|
|
58
|
+
const uuid = await anchors.persist(anchor)
|
|
59
|
+
if (!uuid) {
|
|
60
|
+
anchor.delete()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const prev = localStorage.getItem(storageKey)
|
|
65
|
+
if (prev && prev !== uuid) {
|
|
66
|
+
anchors.remove(prev).catch(() => {})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (persistedAnchor && persistedAnchor !== anchor) {
|
|
70
|
+
persistedAnchor.delete()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
persistedAnchor = anchor
|
|
74
|
+
localStorage.setItem(storageKey, uuid)
|
|
75
|
+
}, COMMIT_DEBOUNCE_MS)
|
|
76
|
+
|
|
77
|
+
origin.registerCommit(() => commit())
|
|
78
|
+
|
|
79
|
+
leftPad.thumbstick.on('change', ({ value }) => {
|
|
80
|
+
// While the grip is held, the left controller drives fine rotation;
|
|
81
|
+
// ignore the thumbstick so the two inputs don't fight each other.
|
|
82
|
+
if (leftPad.squeeze.pressed || typeof value === 'number') return
|
|
83
|
+
|
|
84
|
+
const { x: vx, y: vy } = value
|
|
85
|
+
const [x, y, z] = origin.position
|
|
86
|
+
|
|
87
|
+
origin.set([x, y, z + vy * THUMBSTICK_SPEED], origin.rotation + vx * THUMBSTICK_SPEED)
|
|
88
|
+
commit()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
rightPad.thumbstick.on('change', ({ value }) => {
|
|
92
|
+
if (rightPad.squeeze.pressed || typeof value === 'number') return
|
|
93
|
+
|
|
94
|
+
const { x: vx, y: vy } = value
|
|
95
|
+
const [x, y, z] = origin.position
|
|
96
|
+
|
|
97
|
+
headset.getWorldDirection(headForward)
|
|
98
|
+
|
|
99
|
+
// Flatten onto the XY (ground) plane so pitch doesn't bleed into horizontal motion.
|
|
100
|
+
headForward.z = 0
|
|
101
|
+
if (headForward.lengthSq() < 1e-6) {
|
|
102
|
+
// Viewer gaze was purely vertical (or pose not yet reported) — fall back to scene +Y.
|
|
103
|
+
headForward.set(0, 1, 0)
|
|
104
|
+
} else {
|
|
105
|
+
headForward.normalize()
|
|
106
|
+
}
|
|
107
|
+
// headForward is in the composed XR reference space; rotate into zUp
|
|
108
|
+
// so the stick direction maps to the user's physical gaze regardless
|
|
109
|
+
// of the current origin rotation.
|
|
110
|
+
origin.toZUpDir(headForward)
|
|
111
|
+
headRight.set(headForward.y, -headForward.x, 0)
|
|
112
|
+
|
|
113
|
+
const deltaX = headRight.x * vx * THUMBSTICK_SPEED + headForward.x * vy * THUMBSTICK_SPEED
|
|
114
|
+
const deltaY = headRight.y * vx * THUMBSTICK_SPEED + headForward.y * vy * THUMBSTICK_SPEED
|
|
115
|
+
|
|
116
|
+
origin.set([x + deltaX, y + deltaY, z], origin.rotation)
|
|
117
|
+
commit()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Fine calibration: hold the grip, then move/rotate the controller to nudge
|
|
121
|
+
// the origin 1:1 with the controller delta. Extract world-Z yaw from the
|
|
122
|
+
// controller quaternion for rotation so pitch/roll don't leak in.
|
|
123
|
+
const quatYaw = (q: Quaternion) =>
|
|
124
|
+
Math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z))
|
|
125
|
+
|
|
126
|
+
const fineTranslateStart = new Vector3()
|
|
127
|
+
const fineTranslateOriginStart = new Vector3()
|
|
128
|
+
const fineTranslateCurrent = new Vector3()
|
|
129
|
+
let fineTranslating = $state(false)
|
|
130
|
+
|
|
131
|
+
rightPad.squeeze.on('change', () => {
|
|
132
|
+
const ray = rightController.current?.targetRay
|
|
133
|
+
if (rightPad.squeeze.pressed && ray) {
|
|
134
|
+
// Save start position in zUp so the delta stays stable as the
|
|
135
|
+
// composed reference space recomposes each tick on origin changes.
|
|
136
|
+
origin.toZUpPos(fineTranslateStart, ray.position)
|
|
137
|
+
const [ox, oy, oz] = origin.position
|
|
138
|
+
fineTranslateOriginStart.set(ox, oy, oz)
|
|
139
|
+
fineTranslating = true
|
|
140
|
+
} else {
|
|
141
|
+
fineTranslating = false
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
useTask(
|
|
146
|
+
() => {
|
|
147
|
+
const ray = rightController.current?.targetRay
|
|
148
|
+
if (!ray) return
|
|
149
|
+
origin.toZUpPos(fineTranslateCurrent, ray.position)
|
|
150
|
+
origin.set(
|
|
151
|
+
[
|
|
152
|
+
fineTranslateOriginStart.x + fineTranslateCurrent.x - fineTranslateStart.x,
|
|
153
|
+
fineTranslateOriginStart.y + fineTranslateCurrent.y - fineTranslateStart.y,
|
|
154
|
+
fineTranslateOriginStart.z + fineTranslateCurrent.z - fineTranslateStart.z,
|
|
155
|
+
],
|
|
156
|
+
origin.rotation
|
|
157
|
+
)
|
|
158
|
+
commit()
|
|
159
|
+
},
|
|
160
|
+
{ running: () => fineTranslating }
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
let fineRotateStartYaw = 0
|
|
164
|
+
let fineRotateOriginStart = 0
|
|
165
|
+
let fineRotating = $state(false)
|
|
166
|
+
|
|
167
|
+
leftPad.squeeze.on('change', () => {
|
|
168
|
+
const ray = leftController.current?.targetRay
|
|
169
|
+
if (leftPad.squeeze.pressed && ray) {
|
|
170
|
+
// Controller yaw in composed = yaw_zUp − origin.rotation; convert
|
|
171
|
+
// to zUp so the delta stays stable while origin.rotation updates.
|
|
172
|
+
fineRotateStartYaw = quatYaw(ray.quaternion) + origin.rotation
|
|
173
|
+
fineRotateOriginStart = origin.rotation
|
|
174
|
+
fineRotating = true
|
|
175
|
+
} else {
|
|
176
|
+
fineRotating = false
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
useTask(
|
|
181
|
+
() => {
|
|
182
|
+
const ray = leftController.current?.targetRay
|
|
183
|
+
if (!ray) return
|
|
184
|
+
const yawZup = quatYaw(ray.quaternion) + origin.rotation
|
|
185
|
+
origin.set(origin.position, fineRotateOriginStart + yawZup - fineRotateStartYaw)
|
|
186
|
+
commit()
|
|
187
|
+
},
|
|
188
|
+
{ running: () => fineRotating }
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
let startLeftPinchTranslation = new Vector3()
|
|
192
|
+
let leftPinchTranslation = new Vector3()
|
|
193
|
+
let startRightPinchTranslation = new Vector3()
|
|
194
|
+
let rightPinchCurrent = new Vector3()
|
|
195
|
+
let startRightPinchRotation = 0
|
|
196
|
+
|
|
197
|
+
const leftHand = useHand('left')
|
|
198
|
+
const rightHand = useHand('right')
|
|
199
|
+
|
|
200
|
+
let translating = $state(false)
|
|
201
|
+
let rotating = $state(false)
|
|
202
|
+
|
|
203
|
+
const { renderer } = useThrelte()
|
|
204
|
+
const { isPresenting } = useXR()
|
|
205
|
+
|
|
206
|
+
// Session start: try to restore a persisted anchor for this part. If none
|
|
207
|
+
// exists or restore fails, fall back to a default offset so the origin is
|
|
208
|
+
// visible in front of the user.
|
|
209
|
+
$effect(() => {
|
|
210
|
+
if (!$isPresenting) {
|
|
211
|
+
pendingRestore = undefined
|
|
212
|
+
persistedAnchor = undefined
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const uuid = localStorage.getItem(storageKey)
|
|
217
|
+
if (!uuid) {
|
|
218
|
+
origin.set(DEFAULT_ORIGIN, 0)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let cancelled = false
|
|
223
|
+
anchors
|
|
224
|
+
.restore(uuid)
|
|
225
|
+
.then((anchor) => {
|
|
226
|
+
if (cancelled) return
|
|
227
|
+
if (anchor) {
|
|
228
|
+
persistedAnchor = anchor
|
|
229
|
+
pendingRestore = anchor
|
|
230
|
+
} else {
|
|
231
|
+
localStorage.removeItem(storageKey)
|
|
232
|
+
origin.set(DEFAULT_ORIGIN, 0)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
.catch(() => {
|
|
236
|
+
if (cancelled) return
|
|
237
|
+
localStorage.removeItem(storageKey)
|
|
238
|
+
origin.set(DEFAULT_ORIGIN, 0)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return () => {
|
|
242
|
+
cancelled = true
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Once the restored anchor has localized, snap origin to its pose. This
|
|
247
|
+
// runs only while a restore is pending so it won't fight user input.
|
|
248
|
+
useTask(
|
|
249
|
+
() => {
|
|
250
|
+
const anchor = pendingRestore
|
|
251
|
+
if (!anchor) return
|
|
252
|
+
|
|
253
|
+
const pose = anchors.getAnchorPose(anchor)
|
|
254
|
+
if (!pose) return
|
|
255
|
+
|
|
256
|
+
const { position: p, orientation: o } = pose.transform
|
|
257
|
+
restoreQuat.set(o.x, o.y, o.z, o.w)
|
|
258
|
+
restoreEuler.setFromQuaternion(restoreQuat, 'ZYX')
|
|
259
|
+
origin.set([p.x, p.y, p.z], restoreEuler.z)
|
|
260
|
+
pendingRestore = undefined
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
running: () => pendingRestore !== undefined,
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
$effect(() => {
|
|
268
|
+
if (!$isPresenting) {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
renderer.xr.getHand(0).addEventListener('pinchstart', () => {
|
|
272
|
+
const p = leftHand.current?.targetRay.position
|
|
273
|
+
if (p) {
|
|
274
|
+
translating = true
|
|
275
|
+
// Pinch start position in zUp; delta is cumulative from here.
|
|
276
|
+
origin.toZUpPos(startLeftPinchTranslation, p)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
useTask(
|
|
282
|
+
() => {
|
|
283
|
+
const p = leftHand.current?.targetRay.position
|
|
284
|
+
if (p && translating) {
|
|
285
|
+
origin.toZUpPos(leftPinchTranslation, p)
|
|
286
|
+
origin.set(
|
|
287
|
+
[
|
|
288
|
+
leftPinchTranslation.x - startLeftPinchTranslation.x,
|
|
289
|
+
leftPinchTranslation.y - startLeftPinchTranslation.y,
|
|
290
|
+
leftPinchTranslation.z - startLeftPinchTranslation.z,
|
|
291
|
+
],
|
|
292
|
+
origin.rotation
|
|
293
|
+
)
|
|
294
|
+
commit()
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
running: () => translating,
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
useTask(
|
|
303
|
+
() => {
|
|
304
|
+
const p = rightHand.current?.targetRay.position
|
|
305
|
+
if (p && rotating) {
|
|
306
|
+
origin.toZUpPos(rightPinchCurrent, p)
|
|
307
|
+
const deltaX = rightPinchCurrent.x - startRightPinchTranslation.x
|
|
308
|
+
origin.set(origin.position, startRightPinchRotation + deltaX)
|
|
309
|
+
commit()
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
running: () => rotating,
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
</script>
|
|
317
|
+
|
|
318
|
+
<Hand
|
|
319
|
+
left
|
|
320
|
+
onpinchstart={() => {
|
|
321
|
+
const p = leftHand.current?.targetRay.position
|
|
322
|
+
if (p) {
|
|
323
|
+
translating = true
|
|
324
|
+
origin.toZUpPos(startLeftPinchTranslation, p)
|
|
325
|
+
}
|
|
326
|
+
}}
|
|
327
|
+
onpinchend={() => (translating = false)}
|
|
328
|
+
/>
|
|
329
|
+
|
|
330
|
+
<Hand
|
|
331
|
+
right
|
|
332
|
+
onpinchstart={() => {
|
|
333
|
+
const p = rightHand.current?.targetRay.position
|
|
334
|
+
if (p) {
|
|
335
|
+
rotating = true
|
|
336
|
+
origin.toZUpPos(startRightPinchTranslation, p)
|
|
337
|
+
startRightPinchRotation = origin.rotation
|
|
338
|
+
}
|
|
339
|
+
}}
|
|
340
|
+
onpinchend={() => (rotating = false)}
|
|
341
|
+
/>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Text } from 'threlte-uikit'
|
|
3
|
+
import { Button, ButtonLabel, Panel } from 'threlte-uikit/horizon'
|
|
4
|
+
|
|
5
|
+
import { usePartConfig } from '../../hooks/usePartConfig.svelte'
|
|
6
|
+
|
|
7
|
+
import WristDisplay from './WristDisplay.svelte'
|
|
8
|
+
|
|
9
|
+
const partConfig = usePartConfig()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if partConfig.isDirty}
|
|
13
|
+
<WristDisplay position={[0, 0.005, 0.1]}>
|
|
14
|
+
<Panel
|
|
15
|
+
flexDirection="column"
|
|
16
|
+
padding={16}
|
|
17
|
+
gap={12}
|
|
18
|
+
backgroundColor="#111"
|
|
19
|
+
borderRadius={16}
|
|
20
|
+
minWidth={420}
|
|
21
|
+
>
|
|
22
|
+
<Text
|
|
23
|
+
text="Pending frame edits"
|
|
24
|
+
fontSize={18}
|
|
25
|
+
color="#ffffff"
|
|
26
|
+
/>
|
|
27
|
+
<Panel
|
|
28
|
+
flexDirection="row"
|
|
29
|
+
gap={8}
|
|
30
|
+
>
|
|
31
|
+
<Button
|
|
32
|
+
variant="tertiary"
|
|
33
|
+
size="sm"
|
|
34
|
+
onclick={() => partConfig.discardChanges()}
|
|
35
|
+
>
|
|
36
|
+
<ButtonLabel>
|
|
37
|
+
<Text
|
|
38
|
+
text="Discard"
|
|
39
|
+
fontSize={14}
|
|
40
|
+
color="#ffffff"
|
|
41
|
+
/>
|
|
42
|
+
</ButtonLabel>
|
|
43
|
+
</Button>
|
|
44
|
+
<Button
|
|
45
|
+
variant="primary"
|
|
46
|
+
size="sm"
|
|
47
|
+
onclick={() => partConfig.save()}
|
|
48
|
+
>
|
|
49
|
+
<ButtonLabel>
|
|
50
|
+
<Text
|
|
51
|
+
text="Save"
|
|
52
|
+
fontSize={14}
|
|
53
|
+
color="#ffffff"
|
|
54
|
+
/>
|
|
55
|
+
</ButtonLabel>
|
|
56
|
+
</Button>
|
|
57
|
+
</Panel>
|
|
58
|
+
</Panel>
|
|
59
|
+
</WristDisplay>
|
|
60
|
+
{/if}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const PendingEditsPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type PendingEditsPanel = InstanceType<typeof PendingEditsPanel>;
|
|
18
|
+
export default PendingEditsPanel;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
import { T } from '@threlte/core'
|
|
5
|
+
import { useController, useHandJoint } from '@threlte/xr'
|
|
6
|
+
import { fromStore } from 'svelte/store'
|
|
7
|
+
import { Group, type Vector3Tuple } from 'three'
|
|
8
|
+
import { provideDefaultProperties } from 'threlte-uikit'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Offset from the wrist in the wrist-local frame, meters. */
|
|
12
|
+
position?: Vector3Tuple
|
|
13
|
+
/**
|
|
14
|
+
* Rotation in the wrist-local frame. The default orients uikit content
|
|
15
|
+
* (panels face their own +Z) onto the dorsal side of the wrist, so the
|
|
16
|
+
* user sees it when turning their palm down, like a smartwatch.
|
|
17
|
+
*/
|
|
18
|
+
rotation?: Vector3Tuple
|
|
19
|
+
/** Uniform scale for the wrist group. Smaller than the HUD default
|
|
20
|
+
* because panels live at arm's length instead of ~1 m away. */
|
|
21
|
+
scale?: number
|
|
22
|
+
children?: Snippet
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
position = [0, 0.005, 0.08],
|
|
27
|
+
rotation = [-Math.PI / 2, 0, 0],
|
|
28
|
+
scale = 0.03,
|
|
29
|
+
children,
|
|
30
|
+
}: Props = $props()
|
|
31
|
+
|
|
32
|
+
// Draw uikit content on top of the scene (real-world depth, selection OBB,
|
|
33
|
+
// etc.), matching the old HUD behavior.
|
|
34
|
+
provideDefaultProperties(() => ({
|
|
35
|
+
depthTest: false,
|
|
36
|
+
renderOrder: 999,
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
const leftController = fromStore(useController('left'))
|
|
40
|
+
const leftWrist = fromStore(useHandJoint('left', 'wrist'))
|
|
41
|
+
|
|
42
|
+
// Prefer the hand wrist joint when hand tracking is active; fall back to
|
|
43
|
+
// the controller grip. Both are three.js Groups updated per frame by
|
|
44
|
+
// WebXR, so attaching as a child follows the wrist automatically.
|
|
45
|
+
const parent = $derived(leftWrist.current ?? leftController.current?.grip)
|
|
46
|
+
|
|
47
|
+
const group = new Group()
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
{#if parent}
|
|
51
|
+
<T
|
|
52
|
+
is={group}
|
|
53
|
+
attach={parent}
|
|
54
|
+
{position}
|
|
55
|
+
{rotation}
|
|
56
|
+
{scale}
|
|
57
|
+
>
|
|
58
|
+
{@render children?.()}
|
|
59
|
+
</T>
|
|
60
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type Vector3Tuple } from 'three';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Offset from the wrist in the wrist-local frame, meters. */
|
|
5
|
+
position?: Vector3Tuple;
|
|
6
|
+
/**
|
|
7
|
+
* Rotation in the wrist-local frame. The default orients uikit content
|
|
8
|
+
* (panels face their own +Z) onto the dorsal side of the wrist, so the
|
|
9
|
+
* user sees it when turning their palm down, like a smartwatch.
|
|
10
|
+
*/
|
|
11
|
+
rotation?: Vector3Tuple;
|
|
12
|
+
/** Uniform scale for the wrist group. Smaller than the HUD default
|
|
13
|
+
* because panels live at arm's length instead of ~1 m away. */
|
|
14
|
+
scale?: number;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
declare const WristDisplay: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type WristDisplay = ReturnType<typeof WristDisplay>;
|
|
19
|
+
export default WristDisplay;
|