@viamrobotics/motion-tools 1.9.1 → 1.11.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/HoverUpdater.svelte.d.ts +16 -0
- package/dist/HoverUpdater.svelte.js +78 -0
- package/dist/WorldObject.svelte.d.ts +27 -0
- package/dist/WorldObject.svelte.js +8 -55
- package/dist/{draw → buf/draw}/v1/drawing_pb.d.ts +6 -0
- package/dist/{draw → buf/draw}/v1/drawing_pb.js +7 -0
- package/dist/buf/draw/v1/service_connect.d.ts +122 -0
- package/dist/buf/draw/v1/service_connect.js +126 -0
- package/dist/buf/draw/v1/service_pb.d.ts +382 -0
- package/dist/buf/draw/v1/service_pb.js +612 -0
- package/dist/components/App.svelte +28 -30
- package/dist/components/Arrows/Arrows.svelte +16 -3
- package/dist/components/FileDrop/file-dropper.d.ts +1 -1
- package/dist/components/FileDrop/snapshot-dropper.js +1 -1
- package/dist/components/FileDrop/useFileDrop.svelte.d.ts +2 -1
- package/dist/components/Frame.svelte +1 -1
- package/dist/components/Geometry.svelte +113 -71
- package/dist/components/Geometry.svelte.d.ts +6 -7
- package/dist/components/MeasureTool/MeasurePoint.svelte +3 -3
- package/dist/components/MeasureTool/MeasureTool.svelte +6 -6
- package/dist/components/SceneProviders.svelte +4 -0
- package/dist/components/Snapshot.svelte +1 -1
- package/dist/components/Snapshot.svelte.d.ts +1 -1
- package/dist/components/hover/HoveredEntities.svelte +23 -0
- package/dist/components/hover/HoveredEntity.svelte +15 -0
- package/dist/components/hover/HoveredEntity.svelte.d.ts +3 -0
- package/dist/components/hover/HoveredEntityTooltip.svelte +70 -0
- package/dist/components/{HoveredEntityTooltip.svelte.d.ts → hover/HoveredEntityTooltip.svelte.d.ts} +2 -2
- package/dist/components/hover/LinkedHoveredEntity.svelte +55 -0
- package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +9 -0
- package/dist/components/overlay/AddRelationship.svelte +131 -0
- package/dist/components/overlay/AddRelationship.svelte.d.ts +7 -0
- package/dist/components/overlay/Details.svelte +55 -2
- package/dist/components/overlay/FloatingPanel.svelte +78 -0
- package/dist/components/overlay/FloatingPanel.svelte.d.ts +13 -0
- package/dist/components/overlay/{left-pane/RefreshRate.svelte → RefreshRate.svelte} +1 -1
- package/dist/components/overlay/ToggleGroup.svelte +22 -26
- package/dist/components/overlay/ToggleGroup.svelte.d.ts +6 -7
- package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -4
- package/dist/components/overlay/settings/Settings.svelte +330 -0
- package/dist/components/overlay/settings/Tabs.svelte +54 -0
- package/dist/components/overlay/settings/Tabs.svelte.d.ts +12 -0
- package/dist/components/overlay/widgets/Camera.svelte +20 -12
- package/dist/components/xr/ArmTeleop.svelte +469 -0
- package/dist/components/xr/ArmTeleop.svelte.d.ts +10 -0
- package/dist/components/xr/CameraFeed.svelte +191 -47
- package/dist/components/xr/CameraFeed.svelte.d.ts +7 -0
- package/dist/components/xr/Controllers.svelte +45 -38
- package/dist/components/xr/Controllers.svelte.d.ts +2 -17
- package/dist/components/xr/Hands.svelte +2 -4
- package/dist/components/xr/JointLimitsWidget.svelte +209 -0
- package/dist/components/xr/JointLimitsWidget.svelte.d.ts +13 -0
- package/dist/components/xr/OriginMarker.svelte +1 -15
- package/dist/components/xr/XR.svelte +78 -5
- package/dist/components/xr/XRConfigPanel.svelte +449 -0
- package/dist/components/xr/XRConfigPanel.svelte.d.ts +11 -0
- package/dist/components/xr/XRControllerSettings.svelte +240 -0
- package/dist/components/xr/XRControllerSettings.svelte.d.ts +3 -0
- package/dist/components/xr/XRToast.svelte +215 -0
- package/dist/components/xr/XRToast.svelte.d.ts +3 -0
- package/dist/components/xr/math.d.ts +14 -0
- package/dist/components/xr/math.js +26 -0
- package/dist/components/xr/toasts.svelte.d.ts +20 -0
- package/dist/components/xr/toasts.svelte.js +32 -0
- package/dist/components/xr/useOrigin.svelte.d.ts +2 -2
- package/dist/components/xr/useOrigin.svelte.js +4 -4
- package/dist/ecs/index.d.ts +1 -0
- package/dist/ecs/index.js +1 -0
- package/dist/ecs/relations.d.ts +7 -0
- package/dist/ecs/relations.js +7 -0
- package/dist/ecs/traits.d.ts +15 -1
- package/dist/ecs/traits.js +19 -5
- package/dist/ecs/useTrait.svelte.d.ts +3 -3
- package/dist/frame.d.ts +0 -3
- package/dist/hooks/useArmKinematics.svelte.d.ts +12 -0
- package/dist/hooks/useArmKinematics.svelte.js +31 -0
- package/dist/hooks/useGeometries.svelte.js +47 -36
- package/dist/hooks/useLinked.svelte.d.ts +7 -0
- package/dist/hooks/useLinked.svelte.js +35 -0
- package/dist/hooks/useObjectEvents.svelte.js +52 -16
- package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
- package/dist/hooks/usePartConfig.svelte.js +2 -2
- package/dist/hooks/usePointcloudObjects.svelte.js +45 -64
- package/dist/hooks/usePointclouds.svelte.js +13 -9
- package/dist/hooks/usePose.svelte.js +5 -2
- package/dist/hooks/useResourceByName.svelte.d.ts +7 -0
- package/dist/hooks/useResourceByName.svelte.js +2 -2
- package/dist/hooks/useSettings.svelte.d.ts +14 -0
- package/dist/hooks/useSettings.svelte.js +10 -0
- package/dist/hooks/useWorldState.svelte.d.ts +0 -8
- package/dist/lib.d.ts +1 -3
- package/dist/lib.js +1 -3
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +2 -2
- package/dist/three/InstancedArrows/raycast.d.ts +2 -4
- package/dist/three/InstancedArrows/raycast.js +5 -5
- package/dist/transform.js +1 -0
- package/package.json +7 -5
- package/dist/assert.d.ts +0 -14
- package/dist/assert.js +0 -21
- package/dist/components/BatchedGeometry.svelte +0 -0
- package/dist/components/BatchedGeometry.svelte.d.ts +0 -26
- package/dist/components/Detections.svelte +0 -41
- package/dist/components/Detections.svelte.d.ts +0 -3
- package/dist/components/DetectionsPlane.svelte +0 -23
- package/dist/components/DetectionsPlane.svelte.d.ts +0 -21
- package/dist/components/Geometry2.svelte +0 -211
- package/dist/components/Geometry2.svelte.d.ts +0 -19
- package/dist/components/HoveredEntities.svelte +0 -19
- package/dist/components/HoveredEntityTooltip.svelte +0 -242
- package/dist/components/overlay/left-pane/Settings.svelte +0 -221
- package/dist/components/overlay/left-pane/Widgets.svelte +0 -65
- package/dist/components/overlay/left-pane/Widgets.svelte.d.ts +0 -3
- package/dist/entries.d.ts +0 -1
- package/dist/entries.js +0 -3
- package/dist/hooks/index.d.ts +0 -0
- package/dist/hooks/index.js +0 -1
- package/dist/test.d.ts +0 -1
- package/dist/test.js +0 -1
- package/dist/three/BoxHelper.d.ts +0 -50
- package/dist/three/BoxHelper.js +0 -134
- /package/dist/{common → buf/common}/v1/common_pb.d.ts +0 -0
- /package/dist/{common → buf/common}/v1/common_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/metadata_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/metadata_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/scene_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/scene_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/snapshot_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/snapshot_pb.js +0 -0
- /package/dist/{draw → buf/draw}/v1/transforms_pb.d.ts +0 -0
- /package/dist/{draw → buf/draw}/v1/transforms_pb.js +0 -0
- /package/dist/components/{HoveredEntities.svelte.d.ts → hover/HoveredEntities.svelte.d.ts} +0 -0
- /package/dist/components/overlay/{left-pane/RefreshRate.svelte.d.ts → RefreshRate.svelte.d.ts} +0 -0
- /package/dist/components/overlay/{left-pane → settings}/Settings.svelte.d.ts +0 -0
- /package/dist/components/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
- /package/dist/components/{BentPlaneGeometry.svelte.d.ts → xr/BentPlaneGeometry.svelte.d.ts} +0 -0
|
@@ -1,24 +1,97 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { T } from '@threlte/core'
|
|
3
3
|
import { useXR, XR, XRButton } from '@threlte/xr'
|
|
4
|
+
import { World } from '@threlte/rapier'
|
|
4
5
|
import OriginMarker from './OriginMarker.svelte'
|
|
5
6
|
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
6
7
|
import Controllers from './Controllers.svelte'
|
|
8
|
+
import CameraFeed from './CameraFeed.svelte'
|
|
9
|
+
import JointLimitsWidget from './JointLimitsWidget.svelte'
|
|
10
|
+
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
11
|
+
import XRToast from './XRToast.svelte'
|
|
12
|
+
import { useOrigin } from './useOrigin.svelte'
|
|
7
13
|
|
|
8
14
|
const { ...rest } = $props()
|
|
9
15
|
|
|
10
16
|
const { isPresenting } = useXR()
|
|
11
17
|
const settings = useSettings()
|
|
18
|
+
const origin = useOrigin()
|
|
12
19
|
const enableXR = $derived(settings.current.enableXR)
|
|
20
|
+
|
|
21
|
+
const partID = usePartID()
|
|
22
|
+
|
|
23
|
+
// Get all enabled camera widgets for the current part
|
|
24
|
+
const enabledCameras = $derived.by(() => {
|
|
25
|
+
const openWidgets = settings.current.openCameraWidgets
|
|
26
|
+
const currentPartID = partID.current
|
|
27
|
+
return openWidgets[currentPartID] || []
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Get arms assigned to controllers
|
|
31
|
+
const controllerConfig = $derived(settings.current.xrController)
|
|
32
|
+
const leftArmName = $derived(controllerConfig.left.armName)
|
|
33
|
+
const rightArmName = $derived(controllerConfig.right.armName)
|
|
13
34
|
</script>
|
|
14
35
|
|
|
15
36
|
{#if enableXR}
|
|
16
|
-
<XR
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
37
|
+
<XR
|
|
38
|
+
onsessionstart={() => {
|
|
39
|
+
origin.set([0, 0, -2])
|
|
40
|
+
}}
|
|
41
|
+
onsessionend={() => {
|
|
42
|
+
origin.set([0, 0, 0])
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<!-- Render all enabled camera feeds with horizontal spacing behind origin -->
|
|
46
|
+
{#each enabledCameras as cameraName, index (cameraName)}
|
|
47
|
+
{@const spacing = 1.2}
|
|
48
|
+
{@const centerOffset = ((enabledCameras.length - 1) * spacing) / 2}
|
|
49
|
+
<CameraFeed
|
|
50
|
+
resourceName={cameraName}
|
|
51
|
+
offset={{ x: index * spacing - centerOffset, y: 1.5, z: -2.5 }}
|
|
52
|
+
scale={0.8}
|
|
53
|
+
enableProfiling={false}
|
|
54
|
+
/>
|
|
55
|
+
{/each}
|
|
56
|
+
|
|
57
|
+
<!-- Render joint limits widgets only for arms assigned to controllers, on the matching side -->
|
|
58
|
+
{#if leftArmName}
|
|
59
|
+
{@const spacing = 1.2}
|
|
60
|
+
{@const centerOffset = ((enabledCameras.length - 1) * spacing) / 2}
|
|
61
|
+
{@const widgetX = -(centerOffset + spacing + 0.3)}
|
|
62
|
+
<JointLimitsWidget
|
|
63
|
+
armName={leftArmName}
|
|
64
|
+
offset={{ x: widgetX, y: 1.5, z: -2.5 }}
|
|
65
|
+
scale={0.6}
|
|
66
|
+
rotationY={15 * (Math.PI / 180)}
|
|
67
|
+
/>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if rightArmName}
|
|
70
|
+
{@const spacing = 1.2}
|
|
71
|
+
{@const centerOffset = ((enabledCameras.length - 1) * spacing) / 2}
|
|
72
|
+
{@const widgetX = centerOffset + spacing + 0.3}
|
|
73
|
+
<JointLimitsWidget
|
|
74
|
+
armName={rightArmName}
|
|
75
|
+
offset={{ x: widgetX, y: 1.5, z: -2.5 }}
|
|
76
|
+
scale={0.6}
|
|
77
|
+
/>
|
|
78
|
+
{/if}
|
|
79
|
+
|
|
80
|
+
<!-- XR Controller Configuration Panel -->
|
|
81
|
+
<!-- Temporarily disabled due to connection issues -->
|
|
82
|
+
<!-- <XRConfigPanel offset={{ x: 0, y: 2.5, z: -2.5 }} scale={0.7} /> -->
|
|
83
|
+
|
|
84
|
+
<XRToast />
|
|
85
|
+
|
|
86
|
+
<World>
|
|
87
|
+
<Controllers />
|
|
20
88
|
|
|
21
|
-
|
|
89
|
+
<T.Group position.z={-2}>
|
|
90
|
+
<T.Group rotation.x={$isPresenting ? -Math.PI / 2 : 0}>
|
|
91
|
+
<OriginMarker />
|
|
92
|
+
</T.Group>
|
|
93
|
+
</T.Group>
|
|
94
|
+
</World>
|
|
22
95
|
</XR>
|
|
23
96
|
|
|
24
97
|
<XRButton
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { T, useTask } from '@threlte/core'
|
|
3
|
+
import { CanvasTexture, PlaneGeometry, Mesh, Raycaster } from 'three'
|
|
4
|
+
import { useArmClient } from '../../hooks/useArmClient.svelte'
|
|
5
|
+
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
6
|
+
import { useResourceNames } from '@viamrobotics/svelte-sdk'
|
|
7
|
+
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
8
|
+
import { useController, type XRController } from '@threlte/xr'
|
|
9
|
+
|
|
10
|
+
interface XRConfigPanelProps {
|
|
11
|
+
offset?: { x?: number; y?: number; z?: number }
|
|
12
|
+
scale?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { offset = {}, scale = 0.8 }: XRConfigPanelProps = $props()
|
|
16
|
+
|
|
17
|
+
const settings = useSettings()
|
|
18
|
+
const armClient = useArmClient()
|
|
19
|
+
const partID = usePartID()
|
|
20
|
+
|
|
21
|
+
let resources: ReturnType<typeof useResourceNames> | undefined
|
|
22
|
+
try {
|
|
23
|
+
resources = useResourceNames(() => partID.current)
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.warn('Failed to get resources, robot may not be connected yet:', e)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Get available arms and grippers
|
|
29
|
+
const armNames = $derived(armClient.names || [])
|
|
30
|
+
const gripperNames = $derived(
|
|
31
|
+
resources?.current
|
|
32
|
+
?.filter((r) => r.subtype === 'gripper' && r.type === 'component')
|
|
33
|
+
.map((r) => r.name) || []
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Local state for UI
|
|
37
|
+
type Hand = 'left' | 'right'
|
|
38
|
+
let selectedHand = $state<Hand>('right')
|
|
39
|
+
|
|
40
|
+
// Get current config for selected hand
|
|
41
|
+
const currentConfig = $derived(settings.current.xrController[selectedHand])
|
|
42
|
+
|
|
43
|
+
// Local form state (editable) — synced from currentConfig via effect
|
|
44
|
+
let formArmName = $state<string | undefined>(undefined)
|
|
45
|
+
let formGripperName = $state<string | undefined>(undefined)
|
|
46
|
+
let formScaleFactor = $state<number>(1.0)
|
|
47
|
+
let formRotationEnabled = $state<boolean>(true)
|
|
48
|
+
|
|
49
|
+
// Sync form state when selected hand or config changes
|
|
50
|
+
$effect(() => {
|
|
51
|
+
const cfg = currentConfig
|
|
52
|
+
formArmName = cfg.armName
|
|
53
|
+
formGripperName = cfg.gripperName
|
|
54
|
+
formScaleFactor = cfg.scaleFactor
|
|
55
|
+
formRotationEnabled = cfg.rotationEnabled
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Canvas setup
|
|
59
|
+
const CANVAS_WIDTH = 600
|
|
60
|
+
const CANVAS_HEIGHT = 500
|
|
61
|
+
|
|
62
|
+
let canvas: HTMLCanvasElement | undefined = $state()
|
|
63
|
+
let texture: CanvasTexture | undefined = $state()
|
|
64
|
+
let geometry: PlaneGeometry | undefined = $state()
|
|
65
|
+
|
|
66
|
+
// Initialize canvas
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (!canvas) {
|
|
69
|
+
canvas = document.createElement('canvas')
|
|
70
|
+
canvas.width = CANVAS_WIDTH
|
|
71
|
+
canvas.height = CANVAS_HEIGHT
|
|
72
|
+
texture = new CanvasTexture(canvas)
|
|
73
|
+
|
|
74
|
+
// Calculate aspect ratio for plane geometry
|
|
75
|
+
const aspect = CANVAS_WIDTH / CANVAS_HEIGHT
|
|
76
|
+
geometry = new PlaneGeometry(1.5, 1.5 / aspect)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// UI element bounds for interaction
|
|
81
|
+
interface UIElement {
|
|
82
|
+
x: number
|
|
83
|
+
y: number
|
|
84
|
+
width: number
|
|
85
|
+
height: number
|
|
86
|
+
type: 'button' | 'dropdown' | 'slider' | 'checkbox' | 'tab'
|
|
87
|
+
id: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let uiElements: UIElement[] = []
|
|
91
|
+
|
|
92
|
+
// Mesh ref for raycasting
|
|
93
|
+
let meshRef = $state<Mesh | undefined>()
|
|
94
|
+
|
|
95
|
+
// Controller interaction
|
|
96
|
+
const rightController = useController('right')
|
|
97
|
+
const leftController = useController('left')
|
|
98
|
+
|
|
99
|
+
// Interaction state
|
|
100
|
+
let hoveredElement = $state<UIElement | undefined>()
|
|
101
|
+
let lastButtonPressed = $state(false)
|
|
102
|
+
|
|
103
|
+
// Handle click on UI element
|
|
104
|
+
function handleClick(element: UIElement) {
|
|
105
|
+
if (element.type === 'tab') {
|
|
106
|
+
selectedHand = element.id as Hand
|
|
107
|
+
} else if (element.type === 'button' && element.id === 'apply') {
|
|
108
|
+
applySettings()
|
|
109
|
+
} else if (element.id === 'arm-dropdown') {
|
|
110
|
+
// Cycle through arms
|
|
111
|
+
const currentIndex = armNames.indexOf(formArmName || '')
|
|
112
|
+
const nextIndex = (currentIndex + 1) % (armNames.length + 1)
|
|
113
|
+
formArmName = nextIndex === armNames.length ? undefined : armNames[nextIndex]
|
|
114
|
+
} else if (element.id === 'gripper-dropdown') {
|
|
115
|
+
// Cycle through grippers
|
|
116
|
+
const currentIndex = gripperNames.indexOf(formGripperName || '')
|
|
117
|
+
const nextIndex = (currentIndex + 1) % (gripperNames.length + 1)
|
|
118
|
+
formGripperName = nextIndex === gripperNames.length ? undefined : gripperNames[nextIndex]
|
|
119
|
+
} else if (element.id === 'rotation-checkbox') {
|
|
120
|
+
formRotationEnabled = !formRotationEnabled
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Reusable raycaster to avoid per-frame allocation
|
|
125
|
+
const raycaster = new Raycaster()
|
|
126
|
+
|
|
127
|
+
// Check for ray intersection with panel
|
|
128
|
+
function checkIntersection(controllerRef: typeof rightController) {
|
|
129
|
+
if (!meshRef || !controllerRef.current) return
|
|
130
|
+
|
|
131
|
+
const controller = controllerRef.current
|
|
132
|
+
|
|
133
|
+
// Get controller's world position and direction
|
|
134
|
+
const tempMatrix = controller.targetRay.matrixWorld
|
|
135
|
+
if (!tempMatrix || !tempMatrix.elements) return
|
|
136
|
+
|
|
137
|
+
raycaster.ray.origin.setFromMatrixPosition(tempMatrix)
|
|
138
|
+
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix).normalize()
|
|
139
|
+
|
|
140
|
+
// Check intersection with mesh
|
|
141
|
+
const intersects = raycaster.intersectObject(meshRef, false)
|
|
142
|
+
|
|
143
|
+
if (intersects.length > 0) {
|
|
144
|
+
const intersect = intersects[0]
|
|
145
|
+
const uv = intersect.uv
|
|
146
|
+
|
|
147
|
+
if (uv) {
|
|
148
|
+
// Map UV to canvas coordinates
|
|
149
|
+
const canvasX = uv.x * CANVAS_WIDTH
|
|
150
|
+
const canvasY = (1 - uv.y) * CANVAS_HEIGHT
|
|
151
|
+
|
|
152
|
+
// Check which UI element was hit
|
|
153
|
+
const hitElement = uiElements.find(
|
|
154
|
+
(el) =>
|
|
155
|
+
canvasX >= el.x &&
|
|
156
|
+
canvasX <= el.x + el.width &&
|
|
157
|
+
canvasY >= el.y &&
|
|
158
|
+
canvasY <= el.y + el.height
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
hoveredElement = hitElement
|
|
162
|
+
return hitElement
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hoveredElement = undefined
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Monitor controller A/X button every frame
|
|
171
|
+
useTask(() => {
|
|
172
|
+
const controller = rightController.current || leftController.current
|
|
173
|
+
if (!controller) return
|
|
174
|
+
|
|
175
|
+
// Check for intersection
|
|
176
|
+
const hitElement = checkIntersection(rightController.current ? rightController : leftController)
|
|
177
|
+
|
|
178
|
+
// Check for A/X button press (rising edge) - gamepad button 4
|
|
179
|
+
const gamepad = (controller as XRController & { gamepad?: Gamepad }).gamepad
|
|
180
|
+
const buttonPressed = gamepad?.buttons?.[4]?.pressed || false
|
|
181
|
+
|
|
182
|
+
if (buttonPressed && !lastButtonPressed && hitElement) {
|
|
183
|
+
handleClick(hitElement)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lastButtonPressed = buttonPressed
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Apply settings
|
|
190
|
+
function applySettings() {
|
|
191
|
+
settings.current.xrController[selectedHand] = {
|
|
192
|
+
armName: formArmName,
|
|
193
|
+
gripperName: formGripperName,
|
|
194
|
+
scaleFactor: formScaleFactor,
|
|
195
|
+
rotationEnabled: formRotationEnabled,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Render functions
|
|
200
|
+
function renderHeader(ctx: CanvasRenderingContext2D, width: number) {
|
|
201
|
+
// Header background
|
|
202
|
+
ctx.fillStyle = '#0a0a0a'
|
|
203
|
+
ctx.fillRect(0, 0, width, 50)
|
|
204
|
+
|
|
205
|
+
// Title
|
|
206
|
+
ctx.fillStyle = '#ffffff'
|
|
207
|
+
ctx.font = 'bold 20px sans-serif'
|
|
208
|
+
ctx.textBaseline = 'middle'
|
|
209
|
+
ctx.fillText('XR Controller Configuration', 20, 20)
|
|
210
|
+
|
|
211
|
+
// Instruction text
|
|
212
|
+
ctx.font = '12px sans-serif'
|
|
213
|
+
ctx.fillStyle = '#999999'
|
|
214
|
+
ctx.fillText('Use A/X button to click', 20, 38)
|
|
215
|
+
|
|
216
|
+
// Separator line
|
|
217
|
+
ctx.strokeStyle = '#444444'
|
|
218
|
+
ctx.lineWidth = 2
|
|
219
|
+
ctx.beginPath()
|
|
220
|
+
ctx.moveTo(0, 50)
|
|
221
|
+
ctx.lineTo(width, 50)
|
|
222
|
+
ctx.stroke()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renderTabs(ctx: CanvasRenderingContext2D, width: number) {
|
|
226
|
+
const tabY = 50
|
|
227
|
+
const tabHeight = 40
|
|
228
|
+
const tabWidth = width / 2
|
|
229
|
+
|
|
230
|
+
// Clear UI elements for tabs
|
|
231
|
+
uiElements = uiElements.filter((el) => el.type !== 'tab')
|
|
232
|
+
|
|
233
|
+
// Left tab
|
|
234
|
+
ctx.fillStyle = selectedHand === 'left' ? '#333333' : '#1a1a1a'
|
|
235
|
+
ctx.fillRect(0, tabY, tabWidth, tabHeight)
|
|
236
|
+
ctx.fillStyle = '#ffffff'
|
|
237
|
+
ctx.font = 'bold 18px sans-serif'
|
|
238
|
+
ctx.textAlign = 'center'
|
|
239
|
+
ctx.textBaseline = 'middle'
|
|
240
|
+
ctx.fillText('LEFT', tabWidth / 2, tabY + tabHeight / 2)
|
|
241
|
+
|
|
242
|
+
uiElements.push({
|
|
243
|
+
x: 0,
|
|
244
|
+
y: tabY,
|
|
245
|
+
width: tabWidth,
|
|
246
|
+
height: tabHeight,
|
|
247
|
+
type: 'tab',
|
|
248
|
+
id: 'left',
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Right tab
|
|
252
|
+
ctx.fillStyle = selectedHand === 'right' ? '#333333' : '#1a1a1a'
|
|
253
|
+
ctx.fillRect(tabWidth, tabY, tabWidth, tabHeight)
|
|
254
|
+
ctx.fillStyle = '#ffffff'
|
|
255
|
+
ctx.fillText('RIGHT', tabWidth + tabWidth / 2, tabY + tabHeight / 2)
|
|
256
|
+
|
|
257
|
+
uiElements.push({
|
|
258
|
+
x: tabWidth,
|
|
259
|
+
y: tabY,
|
|
260
|
+
width: tabWidth,
|
|
261
|
+
height: tabHeight,
|
|
262
|
+
type: 'tab',
|
|
263
|
+
id: 'right',
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Tab separator line
|
|
267
|
+
ctx.strokeStyle = '#444444'
|
|
268
|
+
ctx.lineWidth = 2
|
|
269
|
+
ctx.beginPath()
|
|
270
|
+
ctx.moveTo(0, tabY + tabHeight)
|
|
271
|
+
ctx.lineTo(width, tabY + tabHeight)
|
|
272
|
+
ctx.stroke()
|
|
273
|
+
|
|
274
|
+
ctx.textAlign = 'left'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderFormControls(ctx: CanvasRenderingContext2D, width: number) {
|
|
278
|
+
const formY = 90
|
|
279
|
+
const rowHeight = 70
|
|
280
|
+
const labelX = 30
|
|
281
|
+
const controlX = 200
|
|
282
|
+
const controlWidth = 350
|
|
283
|
+
|
|
284
|
+
// Clear form control UI elements
|
|
285
|
+
uiElements = uiElements.filter((el) => el.type === 'tab')
|
|
286
|
+
|
|
287
|
+
// Arm dropdown
|
|
288
|
+
ctx.fillStyle = '#ffffff'
|
|
289
|
+
ctx.font = '16px sans-serif'
|
|
290
|
+
ctx.textBaseline = 'middle'
|
|
291
|
+
ctx.fillText('Arm:', labelX, formY + rowHeight * 0 + 25)
|
|
292
|
+
|
|
293
|
+
const armDropdownY = formY + rowHeight * 0 + 10
|
|
294
|
+
ctx.fillStyle = hoveredElement?.id === 'arm-dropdown' ? '#444444' : '#333333'
|
|
295
|
+
ctx.fillRect(controlX, armDropdownY, controlWidth, 40)
|
|
296
|
+
ctx.fillStyle = '#ffffff'
|
|
297
|
+
ctx.fillText(formArmName || 'None (click to cycle)', controlX + 10, formY + rowHeight * 0 + 30)
|
|
298
|
+
|
|
299
|
+
uiElements.push({
|
|
300
|
+
x: controlX,
|
|
301
|
+
y: armDropdownY,
|
|
302
|
+
width: controlWidth,
|
|
303
|
+
height: 40,
|
|
304
|
+
type: 'dropdown',
|
|
305
|
+
id: 'arm-dropdown',
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Gripper dropdown
|
|
309
|
+
ctx.fillText('Gripper:', labelX, formY + rowHeight * 1 + 25)
|
|
310
|
+
|
|
311
|
+
const gripperDropdownY = formY + rowHeight * 1 + 10
|
|
312
|
+
ctx.fillStyle = hoveredElement?.id === 'gripper-dropdown' ? '#444444' : '#333333'
|
|
313
|
+
ctx.fillRect(controlX, gripperDropdownY, controlWidth, 40)
|
|
314
|
+
ctx.fillStyle = '#ffffff'
|
|
315
|
+
ctx.fillText(
|
|
316
|
+
formGripperName || 'None (click to cycle)',
|
|
317
|
+
controlX + 10,
|
|
318
|
+
formY + rowHeight * 1 + 30
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
uiElements.push({
|
|
322
|
+
x: controlX,
|
|
323
|
+
y: gripperDropdownY,
|
|
324
|
+
width: controlWidth,
|
|
325
|
+
height: 40,
|
|
326
|
+
type: 'dropdown',
|
|
327
|
+
id: 'gripper-dropdown',
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Scale factor slider
|
|
331
|
+
ctx.fillText('Scale Factor:', labelX, formY + rowHeight * 2 + 25)
|
|
332
|
+
|
|
333
|
+
// Slider track
|
|
334
|
+
const sliderX = controlX
|
|
335
|
+
const sliderY = formY + rowHeight * 2 + 20
|
|
336
|
+
const sliderWidth = 250
|
|
337
|
+
const sliderHeight = 10
|
|
338
|
+
|
|
339
|
+
ctx.fillStyle = '#333333'
|
|
340
|
+
ctx.fillRect(sliderX, sliderY, sliderWidth, sliderHeight)
|
|
341
|
+
|
|
342
|
+
// Slider thumb
|
|
343
|
+
const thumbPos = ((formScaleFactor - 0.1) / (3.0 - 0.1)) * sliderWidth
|
|
344
|
+
ctx.fillStyle = '#4CAF50'
|
|
345
|
+
ctx.beginPath()
|
|
346
|
+
ctx.arc(sliderX + thumbPos, sliderY + sliderHeight / 2, 12, 0, Math.PI * 2)
|
|
347
|
+
ctx.fill()
|
|
348
|
+
|
|
349
|
+
// Scale value text
|
|
350
|
+
ctx.fillStyle = '#ffffff'
|
|
351
|
+
ctx.font = '14px sans-serif'
|
|
352
|
+
ctx.fillText(formScaleFactor.toFixed(1), sliderX + sliderWidth + 15, sliderY + sliderHeight / 2)
|
|
353
|
+
|
|
354
|
+
// Rotation checkbox
|
|
355
|
+
ctx.font = '16px sans-serif'
|
|
356
|
+
ctx.fillText('Enable Rotation', labelX, formY + rowHeight * 3 + 25)
|
|
357
|
+
|
|
358
|
+
// Checkbox
|
|
359
|
+
const checkboxX = controlX
|
|
360
|
+
const checkboxY = formY + rowHeight * 3 + 10
|
|
361
|
+
const checkboxSize = 30
|
|
362
|
+
|
|
363
|
+
ctx.strokeStyle = hoveredElement?.id === 'rotation-checkbox' ? '#888888' : '#666666'
|
|
364
|
+
ctx.lineWidth = 2
|
|
365
|
+
ctx.strokeRect(checkboxX, checkboxY, checkboxSize, checkboxSize)
|
|
366
|
+
|
|
367
|
+
if (formRotationEnabled) {
|
|
368
|
+
ctx.fillStyle = '#4CAF50'
|
|
369
|
+
ctx.fillRect(checkboxX + 5, checkboxY + 5, checkboxSize - 10, checkboxSize - 10)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
uiElements.push({
|
|
373
|
+
x: checkboxX,
|
|
374
|
+
y: checkboxY,
|
|
375
|
+
width: checkboxSize,
|
|
376
|
+
height: checkboxSize,
|
|
377
|
+
type: 'checkbox',
|
|
378
|
+
id: 'rotation-checkbox',
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Apply button
|
|
382
|
+
const buttonY = formY + rowHeight * 4 + 10
|
|
383
|
+
const buttonX = width / 2 - 100
|
|
384
|
+
const buttonWidth = 200
|
|
385
|
+
const buttonHeight = 50
|
|
386
|
+
|
|
387
|
+
ctx.fillStyle = hoveredElement?.id === 'apply' ? '#5CBF60' : '#4CAF50'
|
|
388
|
+
ctx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight)
|
|
389
|
+
ctx.fillStyle = '#ffffff'
|
|
390
|
+
ctx.font = 'bold 18px sans-serif'
|
|
391
|
+
ctx.textAlign = 'center'
|
|
392
|
+
ctx.fillText('Apply Settings', buttonX + buttonWidth / 2, buttonY + buttonHeight / 2)
|
|
393
|
+
|
|
394
|
+
uiElements.push({
|
|
395
|
+
x: buttonX,
|
|
396
|
+
y: buttonY,
|
|
397
|
+
width: buttonWidth,
|
|
398
|
+
height: buttonHeight,
|
|
399
|
+
type: 'button',
|
|
400
|
+
id: 'apply',
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
ctx.textAlign = 'left'
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Render canvas
|
|
407
|
+
$effect(() => {
|
|
408
|
+
if (canvas) {
|
|
409
|
+
const ctx = canvas.getContext('2d')
|
|
410
|
+
if (!ctx) return
|
|
411
|
+
|
|
412
|
+
// Clear canvas
|
|
413
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
414
|
+
|
|
415
|
+
// Background
|
|
416
|
+
ctx.fillStyle = '#1a1a1a'
|
|
417
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
418
|
+
|
|
419
|
+
// Render components
|
|
420
|
+
renderHeader(ctx, canvas.width)
|
|
421
|
+
renderTabs(ctx, canvas.width)
|
|
422
|
+
renderFormControls(ctx, canvas.width)
|
|
423
|
+
|
|
424
|
+
// Mark texture for update
|
|
425
|
+
if (texture) {
|
|
426
|
+
texture.needsUpdate = true
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
// Clean up on unmount
|
|
432
|
+
$effect(() => {
|
|
433
|
+
return () => {
|
|
434
|
+
texture?.dispose()
|
|
435
|
+
geometry?.dispose()
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
</script>
|
|
439
|
+
|
|
440
|
+
{#if texture && geometry}
|
|
441
|
+
<T.Mesh
|
|
442
|
+
bind:ref={meshRef}
|
|
443
|
+
position={[offset.x ?? 0, offset.y ?? 2.5, offset.z ?? -2.5]}
|
|
444
|
+
{scale}
|
|
445
|
+
>
|
|
446
|
+
<T is={geometry} />
|
|
447
|
+
<T.MeshBasicMaterial map={texture} />
|
|
448
|
+
</T.Mesh>
|
|
449
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface XRConfigPanelProps {
|
|
2
|
+
offset?: {
|
|
3
|
+
x?: number;
|
|
4
|
+
y?: number;
|
|
5
|
+
z?: number;
|
|
6
|
+
};
|
|
7
|
+
scale?: number;
|
|
8
|
+
}
|
|
9
|
+
declare const XRConfigPanel: import("svelte").Component<XRConfigPanelProps, {}, "">;
|
|
10
|
+
type XRConfigPanel = ReturnType<typeof XRConfigPanel>;
|
|
11
|
+
export default XRConfigPanel;
|