@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.
Files changed (136) hide show
  1. package/dist/HoverUpdater.svelte.d.ts +16 -0
  2. package/dist/HoverUpdater.svelte.js +78 -0
  3. package/dist/WorldObject.svelte.d.ts +27 -0
  4. package/dist/WorldObject.svelte.js +8 -55
  5. package/dist/{draw → buf/draw}/v1/drawing_pb.d.ts +6 -0
  6. package/dist/{draw → buf/draw}/v1/drawing_pb.js +7 -0
  7. package/dist/buf/draw/v1/service_connect.d.ts +122 -0
  8. package/dist/buf/draw/v1/service_connect.js +126 -0
  9. package/dist/buf/draw/v1/service_pb.d.ts +382 -0
  10. package/dist/buf/draw/v1/service_pb.js +612 -0
  11. package/dist/components/App.svelte +28 -30
  12. package/dist/components/Arrows/Arrows.svelte +16 -3
  13. package/dist/components/FileDrop/file-dropper.d.ts +1 -1
  14. package/dist/components/FileDrop/snapshot-dropper.js +1 -1
  15. package/dist/components/FileDrop/useFileDrop.svelte.d.ts +2 -1
  16. package/dist/components/Frame.svelte +1 -1
  17. package/dist/components/Geometry.svelte +113 -71
  18. package/dist/components/Geometry.svelte.d.ts +6 -7
  19. package/dist/components/MeasureTool/MeasurePoint.svelte +3 -3
  20. package/dist/components/MeasureTool/MeasureTool.svelte +6 -6
  21. package/dist/components/SceneProviders.svelte +4 -0
  22. package/dist/components/Snapshot.svelte +1 -1
  23. package/dist/components/Snapshot.svelte.d.ts +1 -1
  24. package/dist/components/hover/HoveredEntities.svelte +23 -0
  25. package/dist/components/hover/HoveredEntity.svelte +15 -0
  26. package/dist/components/hover/HoveredEntity.svelte.d.ts +3 -0
  27. package/dist/components/hover/HoveredEntityTooltip.svelte +70 -0
  28. package/dist/components/{HoveredEntityTooltip.svelte.d.ts → hover/HoveredEntityTooltip.svelte.d.ts} +2 -2
  29. package/dist/components/hover/LinkedHoveredEntity.svelte +55 -0
  30. package/dist/components/hover/LinkedHoveredEntity.svelte.d.ts +9 -0
  31. package/dist/components/overlay/AddRelationship.svelte +131 -0
  32. package/dist/components/overlay/AddRelationship.svelte.d.ts +7 -0
  33. package/dist/components/overlay/Details.svelte +55 -2
  34. package/dist/components/overlay/FloatingPanel.svelte +78 -0
  35. package/dist/components/overlay/FloatingPanel.svelte.d.ts +13 -0
  36. package/dist/components/overlay/{left-pane/RefreshRate.svelte → RefreshRate.svelte} +1 -1
  37. package/dist/components/overlay/ToggleGroup.svelte +22 -26
  38. package/dist/components/overlay/ToggleGroup.svelte.d.ts +6 -7
  39. package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -4
  40. package/dist/components/overlay/settings/Settings.svelte +330 -0
  41. package/dist/components/overlay/settings/Tabs.svelte +54 -0
  42. package/dist/components/overlay/settings/Tabs.svelte.d.ts +12 -0
  43. package/dist/components/overlay/widgets/Camera.svelte +20 -12
  44. package/dist/components/xr/ArmTeleop.svelte +469 -0
  45. package/dist/components/xr/ArmTeleop.svelte.d.ts +10 -0
  46. package/dist/components/xr/CameraFeed.svelte +191 -47
  47. package/dist/components/xr/CameraFeed.svelte.d.ts +7 -0
  48. package/dist/components/xr/Controllers.svelte +45 -38
  49. package/dist/components/xr/Controllers.svelte.d.ts +2 -17
  50. package/dist/components/xr/Hands.svelte +2 -4
  51. package/dist/components/xr/JointLimitsWidget.svelte +209 -0
  52. package/dist/components/xr/JointLimitsWidget.svelte.d.ts +13 -0
  53. package/dist/components/xr/OriginMarker.svelte +1 -15
  54. package/dist/components/xr/XR.svelte +78 -5
  55. package/dist/components/xr/XRConfigPanel.svelte +449 -0
  56. package/dist/components/xr/XRConfigPanel.svelte.d.ts +11 -0
  57. package/dist/components/xr/XRControllerSettings.svelte +240 -0
  58. package/dist/components/xr/XRControllerSettings.svelte.d.ts +3 -0
  59. package/dist/components/xr/XRToast.svelte +215 -0
  60. package/dist/components/xr/XRToast.svelte.d.ts +3 -0
  61. package/dist/components/xr/math.d.ts +14 -0
  62. package/dist/components/xr/math.js +26 -0
  63. package/dist/components/xr/toasts.svelte.d.ts +20 -0
  64. package/dist/components/xr/toasts.svelte.js +32 -0
  65. package/dist/components/xr/useOrigin.svelte.d.ts +2 -2
  66. package/dist/components/xr/useOrigin.svelte.js +4 -4
  67. package/dist/ecs/index.d.ts +1 -0
  68. package/dist/ecs/index.js +1 -0
  69. package/dist/ecs/relations.d.ts +7 -0
  70. package/dist/ecs/relations.js +7 -0
  71. package/dist/ecs/traits.d.ts +15 -1
  72. package/dist/ecs/traits.js +19 -5
  73. package/dist/ecs/useTrait.svelte.d.ts +3 -3
  74. package/dist/frame.d.ts +0 -3
  75. package/dist/hooks/useArmKinematics.svelte.d.ts +12 -0
  76. package/dist/hooks/useArmKinematics.svelte.js +31 -0
  77. package/dist/hooks/useGeometries.svelte.js +47 -36
  78. package/dist/hooks/useLinked.svelte.d.ts +7 -0
  79. package/dist/hooks/useLinked.svelte.js +35 -0
  80. package/dist/hooks/useObjectEvents.svelte.js +52 -16
  81. package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
  82. package/dist/hooks/usePartConfig.svelte.js +2 -2
  83. package/dist/hooks/usePointcloudObjects.svelte.js +45 -64
  84. package/dist/hooks/usePointclouds.svelte.js +13 -9
  85. package/dist/hooks/usePose.svelte.js +5 -2
  86. package/dist/hooks/useResourceByName.svelte.d.ts +7 -0
  87. package/dist/hooks/useResourceByName.svelte.js +2 -2
  88. package/dist/hooks/useSettings.svelte.d.ts +14 -0
  89. package/dist/hooks/useSettings.svelte.js +10 -0
  90. package/dist/hooks/useWorldState.svelte.d.ts +0 -8
  91. package/dist/lib.d.ts +1 -3
  92. package/dist/lib.js +1 -3
  93. package/dist/snapshot.d.ts +2 -2
  94. package/dist/snapshot.js +2 -2
  95. package/dist/three/InstancedArrows/raycast.d.ts +2 -4
  96. package/dist/three/InstancedArrows/raycast.js +5 -5
  97. package/dist/transform.js +1 -0
  98. package/package.json +7 -5
  99. package/dist/assert.d.ts +0 -14
  100. package/dist/assert.js +0 -21
  101. package/dist/components/BatchedGeometry.svelte +0 -0
  102. package/dist/components/BatchedGeometry.svelte.d.ts +0 -26
  103. package/dist/components/Detections.svelte +0 -41
  104. package/dist/components/Detections.svelte.d.ts +0 -3
  105. package/dist/components/DetectionsPlane.svelte +0 -23
  106. package/dist/components/DetectionsPlane.svelte.d.ts +0 -21
  107. package/dist/components/Geometry2.svelte +0 -211
  108. package/dist/components/Geometry2.svelte.d.ts +0 -19
  109. package/dist/components/HoveredEntities.svelte +0 -19
  110. package/dist/components/HoveredEntityTooltip.svelte +0 -242
  111. package/dist/components/overlay/left-pane/Settings.svelte +0 -221
  112. package/dist/components/overlay/left-pane/Widgets.svelte +0 -65
  113. package/dist/components/overlay/left-pane/Widgets.svelte.d.ts +0 -3
  114. package/dist/entries.d.ts +0 -1
  115. package/dist/entries.js +0 -3
  116. package/dist/hooks/index.d.ts +0 -0
  117. package/dist/hooks/index.js +0 -1
  118. package/dist/test.d.ts +0 -1
  119. package/dist/test.js +0 -1
  120. package/dist/three/BoxHelper.d.ts +0 -50
  121. package/dist/three/BoxHelper.js +0 -134
  122. /package/dist/{common → buf/common}/v1/common_pb.d.ts +0 -0
  123. /package/dist/{common → buf/common}/v1/common_pb.js +0 -0
  124. /package/dist/{draw → buf/draw}/v1/metadata_pb.d.ts +0 -0
  125. /package/dist/{draw → buf/draw}/v1/metadata_pb.js +0 -0
  126. /package/dist/{draw → buf/draw}/v1/scene_pb.d.ts +0 -0
  127. /package/dist/{draw → buf/draw}/v1/scene_pb.js +0 -0
  128. /package/dist/{draw → buf/draw}/v1/snapshot_pb.d.ts +0 -0
  129. /package/dist/{draw → buf/draw}/v1/snapshot_pb.js +0 -0
  130. /package/dist/{draw → buf/draw}/v1/transforms_pb.d.ts +0 -0
  131. /package/dist/{draw → buf/draw}/v1/transforms_pb.js +0 -0
  132. /package/dist/components/{HoveredEntities.svelte.d.ts → hover/HoveredEntities.svelte.d.ts} +0 -0
  133. /package/dist/components/overlay/{left-pane/RefreshRate.svelte.d.ts → RefreshRate.svelte.d.ts} +0 -0
  134. /package/dist/components/overlay/{left-pane → settings}/Settings.svelte.d.ts +0 -0
  135. /package/dist/components/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
  136. /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
- <T.Group rotation.x={$isPresenting ? -Math.PI / 2 : 0}>
18
- <OriginMarker />
19
- </T.Group>
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
- <Controllers />
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;