@viamrobotics/motion-tools 1.10.0 → 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 +0 -3
- package/dist/HoverUpdater.svelte.js +8 -50
- 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 +0 -1
- 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/SceneProviders.svelte +2 -0
- package/dist/components/Snapshot.svelte +1 -1
- package/dist/components/Snapshot.svelte.d.ts +1 -1
- package/dist/components/overlay/Details.svelte +20 -0
- package/dist/components/overlay/left-pane/TreeContainer.svelte +0 -2
- package/dist/components/overlay/settings/Settings.svelte +51 -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/traits.d.ts +9 -0
- package/dist/ecs/traits.js +9 -0
- 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 +46 -35
- package/dist/hooks/useObjectEvents.svelte.js +24 -7
- package/dist/hooks/usePartConfig.svelte.d.ts +0 -35
- package/dist/hooks/usePartConfig.svelte.js +2 -2
- package/dist/hooks/usePointcloudObjects.svelte.js +44 -63
- package/dist/hooks/usePointclouds.svelte.js +10 -6
- package/dist/hooks/usePose.svelte.js +4 -1
- 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 +4 -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/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/{BentPlaneGeometry.svelte → xr/BentPlaneGeometry.svelte} +0 -0
- /package/dist/components/{BentPlaneGeometry.svelte.d.ts → xr/BentPlaneGeometry.svelte.d.ts} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Select, Switch } from '@viamrobotics/prime-core'
|
|
3
|
+
import { useArmClient } from '../../hooks/useArmClient.svelte'
|
|
4
|
+
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
5
|
+
import { useResourceNames } from '@viamrobotics/svelte-sdk'
|
|
6
|
+
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
7
|
+
|
|
8
|
+
const settings = useSettings()
|
|
9
|
+
const armClient = useArmClient()
|
|
10
|
+
const partID = usePartID()
|
|
11
|
+
const resources = useResourceNames(() => partID.current, 'gripper')
|
|
12
|
+
|
|
13
|
+
const armNames = $derived(armClient.names || [])
|
|
14
|
+
const gripperNames = $derived(
|
|
15
|
+
resources.current
|
|
16
|
+
.filter((r) => r.subtype === 'gripper' && r.type === 'component')
|
|
17
|
+
.map((r) => r.name)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const config = $derived(settings.current.xrController)
|
|
21
|
+
|
|
22
|
+
// Filter available arms/grippers - exclude what the other controller has selected
|
|
23
|
+
const leftAvailableArms = $derived(
|
|
24
|
+
armNames.filter((name) => name === config.left.armName || name !== config.right.armName)
|
|
25
|
+
)
|
|
26
|
+
const rightAvailableArms = $derived(
|
|
27
|
+
armNames.filter((name) => name === config.right.armName || name !== config.left.armName)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const leftAvailableGrippers = $derived(
|
|
31
|
+
gripperNames.filter(
|
|
32
|
+
(name) => name === config.left.gripperName || name !== config.right.gripperName
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
const rightAvailableGrippers = $derived(
|
|
36
|
+
gripperNames.filter(
|
|
37
|
+
(name) => name === config.right.gripperName || name !== config.left.gripperName
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
function updateConfig(
|
|
42
|
+
hand: 'left' | 'right',
|
|
43
|
+
key: string,
|
|
44
|
+
value: string | number | boolean | undefined
|
|
45
|
+
) {
|
|
46
|
+
settings.current.xrController[hand] = {
|
|
47
|
+
...settings.current.xrController[hand],
|
|
48
|
+
[key]: value,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#snippet SectionTitle(title: string)}
|
|
54
|
+
<h3 class="border-gray-3 border-b py-1 text-sm"><strong>{title}</strong></h3>
|
|
55
|
+
{/snippet}
|
|
56
|
+
|
|
57
|
+
<div class="flex flex-col gap-2.5">
|
|
58
|
+
<label class="flex items-center justify-between gap-2">
|
|
59
|
+
Enable VR / AR mode <Switch bind:on={settings.current.enableXR} />
|
|
60
|
+
</label>
|
|
61
|
+
|
|
62
|
+
<!-- Left Controller -->
|
|
63
|
+
{@render SectionTitle('Left Controller')}
|
|
64
|
+
|
|
65
|
+
<label class="flex items-center justify-between gap-2">
|
|
66
|
+
Arm
|
|
67
|
+
<Select
|
|
68
|
+
value={config.left.armName || ''}
|
|
69
|
+
onchange={(event: Event) => {
|
|
70
|
+
if (event.target instanceof HTMLSelectElement) {
|
|
71
|
+
updateConfig('left', 'armName', event.target.value || undefined)
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<option value="">None</option>
|
|
76
|
+
{#each leftAvailableArms as armName (armName)}
|
|
77
|
+
<option value={armName}>{armName}</option>
|
|
78
|
+
{/each}
|
|
79
|
+
</Select>
|
|
80
|
+
</label>
|
|
81
|
+
|
|
82
|
+
<label class="flex items-center justify-between gap-2">
|
|
83
|
+
Gripper
|
|
84
|
+
<Select
|
|
85
|
+
value={config.left.gripperName || ''}
|
|
86
|
+
onchange={(event: Event) => {
|
|
87
|
+
if (event.target instanceof HTMLSelectElement) {
|
|
88
|
+
updateConfig('left', 'gripperName', event.target.value || undefined)
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<option value="">None</option>
|
|
93
|
+
{#each leftAvailableGrippers as gripperName (gripperName)}
|
|
94
|
+
<option value={gripperName}>{gripperName}</option>
|
|
95
|
+
{/each}
|
|
96
|
+
</Select>
|
|
97
|
+
</label>
|
|
98
|
+
|
|
99
|
+
<label class="flex items-center justify-between gap-2">
|
|
100
|
+
Scale: {config.left.scaleFactor.toFixed(1)}
|
|
101
|
+
<input
|
|
102
|
+
class="w-20"
|
|
103
|
+
type="range"
|
|
104
|
+
min="0.1"
|
|
105
|
+
max="3.0"
|
|
106
|
+
step="0.1"
|
|
107
|
+
value={config.left.scaleFactor}
|
|
108
|
+
style="--value: {((config.left.scaleFactor - 0.1) / (3.0 - 0.1)) * 100}%"
|
|
109
|
+
oninput={(e) => updateConfig('left', 'scaleFactor', parseFloat(e.currentTarget.value))}
|
|
110
|
+
/>
|
|
111
|
+
</label>
|
|
112
|
+
|
|
113
|
+
<label class="flex items-center justify-between gap-2">
|
|
114
|
+
Rotation
|
|
115
|
+
<Switch
|
|
116
|
+
on={config.left.rotationEnabled}
|
|
117
|
+
on:change={(event) => {
|
|
118
|
+
updateConfig('left', 'rotationEnabled', event.detail)
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</label>
|
|
122
|
+
|
|
123
|
+
<!-- Right Controller -->
|
|
124
|
+
{@render SectionTitle('Right Controller')}
|
|
125
|
+
|
|
126
|
+
<label class="flex items-center justify-between gap-2">
|
|
127
|
+
Arm
|
|
128
|
+
<Select
|
|
129
|
+
value={config.right.armName || ''}
|
|
130
|
+
onchange={(event: Event) => {
|
|
131
|
+
if (event.target instanceof HTMLSelectElement) {
|
|
132
|
+
updateConfig('right', 'armName', event.target.value || undefined)
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<option value="">None</option>
|
|
137
|
+
{#each rightAvailableArms as armName (armName)}
|
|
138
|
+
<option value={armName}>{armName}</option>
|
|
139
|
+
{/each}
|
|
140
|
+
</Select>
|
|
141
|
+
</label>
|
|
142
|
+
|
|
143
|
+
<label class="flex items-center justify-between gap-2">
|
|
144
|
+
Gripper
|
|
145
|
+
<Select
|
|
146
|
+
value={config.right.gripperName || ''}
|
|
147
|
+
onchange={(event: Event) => {
|
|
148
|
+
if (event.target instanceof HTMLSelectElement) {
|
|
149
|
+
updateConfig('right', 'gripperName', event.target.value || undefined)
|
|
150
|
+
}
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<option value="">None</option>
|
|
154
|
+
{#each rightAvailableGrippers as gripperName (gripperName)}
|
|
155
|
+
<option value={gripperName}>{gripperName}</option>
|
|
156
|
+
{/each}
|
|
157
|
+
</Select>
|
|
158
|
+
</label>
|
|
159
|
+
|
|
160
|
+
<label class="flex items-center justify-between gap-2">
|
|
161
|
+
Scale: {config.right.scaleFactor.toFixed(1)}
|
|
162
|
+
<input
|
|
163
|
+
class="w-20"
|
|
164
|
+
type="range"
|
|
165
|
+
min="0.1"
|
|
166
|
+
max="3.0"
|
|
167
|
+
step="0.1"
|
|
168
|
+
value={config.right.scaleFactor}
|
|
169
|
+
style="--value: {((config.right.scaleFactor - 0.1) / (3.0 - 0.1)) * 100}%"
|
|
170
|
+
oninput={(e) => updateConfig('right', 'scaleFactor', parseFloat(e.currentTarget.value))}
|
|
171
|
+
/>
|
|
172
|
+
</label>
|
|
173
|
+
|
|
174
|
+
<label class="flex items-center justify-between gap-2">
|
|
175
|
+
Rotation
|
|
176
|
+
<Switch
|
|
177
|
+
on={config.right.rotationEnabled}
|
|
178
|
+
on:change={(event) => {
|
|
179
|
+
updateConfig('right', 'rotationEnabled', event.detail)
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
</label>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<style>
|
|
186
|
+
input[type='range'] {
|
|
187
|
+
-webkit-appearance: none;
|
|
188
|
+
appearance: none;
|
|
189
|
+
width: 100%;
|
|
190
|
+
background: linear-gradient(
|
|
191
|
+
to right,
|
|
192
|
+
#3d7d3f 0%,
|
|
193
|
+
#3d7d3f var(--value),
|
|
194
|
+
#d1d5db var(--value),
|
|
195
|
+
#d1d5db 100%
|
|
196
|
+
);
|
|
197
|
+
border-radius: 0.25rem;
|
|
198
|
+
height: 0.5rem;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
outline: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Webkit browsers (Chrome, Safari, Edge) */
|
|
204
|
+
input[type='range']::-webkit-slider-track {
|
|
205
|
+
background: transparent;
|
|
206
|
+
height: 0.5rem;
|
|
207
|
+
border-radius: 0.25rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
input[type='range']::-webkit-slider-thumb {
|
|
211
|
+
-webkit-appearance: none;
|
|
212
|
+
appearance: none;
|
|
213
|
+
background: #3d7d3f;
|
|
214
|
+
height: 1.25rem;
|
|
215
|
+
width: 1.25rem;
|
|
216
|
+
border-radius: 50%;
|
|
217
|
+
border: 2px solid white;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Firefox */
|
|
221
|
+
input[type='range']::-moz-range-track {
|
|
222
|
+
background: transparent;
|
|
223
|
+
height: 0.5rem;
|
|
224
|
+
border-radius: 0.25rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
input[type='range']::-moz-range-thumb {
|
|
228
|
+
background: #3d7d3f;
|
|
229
|
+
height: 1.25rem;
|
|
230
|
+
width: 1.25rem;
|
|
231
|
+
border-radius: 50%;
|
|
232
|
+
border: 2px solid white;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
input[type='range']::-moz-range-progress {
|
|
236
|
+
background: #3d7d3f;
|
|
237
|
+
height: 0.5rem;
|
|
238
|
+
border-radius: 0.25rem;
|
|
239
|
+
}
|
|
240
|
+
</style>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte'
|
|
3
|
+
import { T } from '@threlte/core'
|
|
4
|
+
import { CanvasTexture, PlaneGeometry } from 'three'
|
|
5
|
+
import { xrToast, type XRToastItem, type ToastVariant } from './toasts.svelte'
|
|
6
|
+
import { Headset } from '@threlte/xr'
|
|
7
|
+
|
|
8
|
+
const CANVAS_WIDTH = 700
|
|
9
|
+
const TOAST_HEIGHT = 80
|
|
10
|
+
const TOAST_GAP = 10
|
|
11
|
+
const MAX_VISIBLE = 5
|
|
12
|
+
const PLANE_WIDTH = 0.7
|
|
13
|
+
|
|
14
|
+
// Offscreen canvas (created once)
|
|
15
|
+
const canvas = document.createElement('canvas')
|
|
16
|
+
canvas.width = CANVAS_WIDTH
|
|
17
|
+
canvas.height = 1
|
|
18
|
+
const texture = new CanvasTexture(canvas)
|
|
19
|
+
|
|
20
|
+
let geometry: PlaneGeometry | undefined = $state()
|
|
21
|
+
|
|
22
|
+
const visibleToasts = $derived(xrToast.toasts.slice(-MAX_VISIBLE))
|
|
23
|
+
const hasToasts = $derived(visibleToasts.length > 0)
|
|
24
|
+
|
|
25
|
+
// Variant styling matching Prime design tokens
|
|
26
|
+
const VARIANT_STYLES: Record<ToastVariant, { accent: string; bg: string }> = {
|
|
27
|
+
success: { accent: '#22C55E', bg: 'rgba(13, 40, 24, 0.94)' },
|
|
28
|
+
danger: { accent: '#EF4444', bg: 'rgba(45, 15, 15, 0.94)' },
|
|
29
|
+
warning: { accent: '#F59E0B', bg: 'rgba(45, 34, 16, 0.94)' },
|
|
30
|
+
info: { accent: '#3B82F6', bg: 'rgba(15, 26, 45, 0.94)' },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function drawRoundRect(
|
|
34
|
+
ctx: CanvasRenderingContext2D,
|
|
35
|
+
x: number,
|
|
36
|
+
y: number,
|
|
37
|
+
w: number,
|
|
38
|
+
h: number,
|
|
39
|
+
r: number
|
|
40
|
+
) {
|
|
41
|
+
ctx.beginPath()
|
|
42
|
+
ctx.moveTo(x + r, y)
|
|
43
|
+
ctx.lineTo(x + w - r, y)
|
|
44
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r)
|
|
45
|
+
ctx.lineTo(x + w, y + h - r)
|
|
46
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h)
|
|
47
|
+
ctx.lineTo(x + r, y + h)
|
|
48
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r)
|
|
49
|
+
ctx.lineTo(x, y + r)
|
|
50
|
+
ctx.quadraticCurveTo(x, y, x + r, y)
|
|
51
|
+
ctx.closePath()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function drawIcon(
|
|
55
|
+
ctx: CanvasRenderingContext2D,
|
|
56
|
+
variant: ToastVariant,
|
|
57
|
+
cx: number,
|
|
58
|
+
cy: number,
|
|
59
|
+
size: number
|
|
60
|
+
) {
|
|
61
|
+
const color = VARIANT_STYLES[variant].accent
|
|
62
|
+
ctx.strokeStyle = color
|
|
63
|
+
ctx.fillStyle = color
|
|
64
|
+
ctx.lineWidth = 3.5
|
|
65
|
+
ctx.lineCap = 'round'
|
|
66
|
+
ctx.lineJoin = 'round'
|
|
67
|
+
|
|
68
|
+
if (variant === 'success') {
|
|
69
|
+
// Checkmark
|
|
70
|
+
ctx.beginPath()
|
|
71
|
+
ctx.moveTo(cx - size * 0.35, cy + size * 0.05)
|
|
72
|
+
ctx.lineTo(cx - size * 0.08, cy + size * 0.3)
|
|
73
|
+
ctx.lineTo(cx + size * 0.4, cy - size * 0.25)
|
|
74
|
+
ctx.stroke()
|
|
75
|
+
} else if (variant === 'danger') {
|
|
76
|
+
// X mark
|
|
77
|
+
const s = size * 0.28
|
|
78
|
+
ctx.beginPath()
|
|
79
|
+
ctx.moveTo(cx - s, cy - s)
|
|
80
|
+
ctx.lineTo(cx + s, cy + s)
|
|
81
|
+
ctx.moveTo(cx + s, cy - s)
|
|
82
|
+
ctx.lineTo(cx - s, cy + s)
|
|
83
|
+
ctx.stroke()
|
|
84
|
+
} else if (variant === 'warning') {
|
|
85
|
+
// Triangle outline with !
|
|
86
|
+
ctx.lineWidth = 3
|
|
87
|
+
ctx.beginPath()
|
|
88
|
+
ctx.moveTo(cx, cy - size * 0.32)
|
|
89
|
+
ctx.lineTo(cx + size * 0.34, cy + size * 0.26)
|
|
90
|
+
ctx.lineTo(cx - size * 0.34, cy + size * 0.26)
|
|
91
|
+
ctx.closePath()
|
|
92
|
+
ctx.stroke()
|
|
93
|
+
// Exclamation mark
|
|
94
|
+
ctx.lineWidth = 3
|
|
95
|
+
ctx.beginPath()
|
|
96
|
+
ctx.moveTo(cx, cy - size * 0.12)
|
|
97
|
+
ctx.lineTo(cx, cy + size * 0.06)
|
|
98
|
+
ctx.stroke()
|
|
99
|
+
ctx.beginPath()
|
|
100
|
+
ctx.arc(cx, cy + size * 0.16, 2, 0, Math.PI * 2)
|
|
101
|
+
ctx.fill()
|
|
102
|
+
} else {
|
|
103
|
+
// Info: circle with i
|
|
104
|
+
ctx.lineWidth = 3
|
|
105
|
+
ctx.beginPath()
|
|
106
|
+
ctx.arc(cx, cy, size * 0.32, 0, Math.PI * 2)
|
|
107
|
+
ctx.stroke()
|
|
108
|
+
// i dot
|
|
109
|
+
ctx.beginPath()
|
|
110
|
+
ctx.arc(cx, cy - size * 0.15, 2.5, 0, Math.PI * 2)
|
|
111
|
+
ctx.fill()
|
|
112
|
+
// i stem
|
|
113
|
+
ctx.lineWidth = 3
|
|
114
|
+
ctx.beginPath()
|
|
115
|
+
ctx.moveTo(cx, cy - size * 0.04)
|
|
116
|
+
ctx.lineTo(cx, cy + size * 0.2)
|
|
117
|
+
ctx.stroke()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderToasts(toasts: XRToastItem[]) {
|
|
122
|
+
const ctx = canvas.getContext('2d')!
|
|
123
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
124
|
+
|
|
125
|
+
if (toasts.length === 0) {
|
|
126
|
+
texture.needsUpdate = true
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const radius = 12
|
|
131
|
+
const accentBarWidth = 8
|
|
132
|
+
const iconCenterX = accentBarWidth + 30
|
|
133
|
+
const textStartX = accentBarWidth + 56
|
|
134
|
+
|
|
135
|
+
toasts.forEach((toast, index) => {
|
|
136
|
+
const y = index * (TOAST_HEIGHT + TOAST_GAP)
|
|
137
|
+
const style = VARIANT_STYLES[toast.variant]
|
|
138
|
+
|
|
139
|
+
// Draw background with clip (so accent bar respects rounded corners)
|
|
140
|
+
ctx.save()
|
|
141
|
+
drawRoundRect(ctx, 0, y, CANVAS_WIDTH, TOAST_HEIGHT, radius)
|
|
142
|
+
ctx.clip()
|
|
143
|
+
|
|
144
|
+
// Fill background
|
|
145
|
+
ctx.fillStyle = style.bg
|
|
146
|
+
ctx.fillRect(0, y, CANVAS_WIDTH, TOAST_HEIGHT)
|
|
147
|
+
|
|
148
|
+
// Accent bar on left
|
|
149
|
+
ctx.fillStyle = style.accent
|
|
150
|
+
ctx.fillRect(0, y, accentBarWidth, TOAST_HEIGHT)
|
|
151
|
+
|
|
152
|
+
ctx.restore()
|
|
153
|
+
|
|
154
|
+
// Subtle border
|
|
155
|
+
ctx.strokeStyle = style.accent + '50' // 31% opacity
|
|
156
|
+
ctx.lineWidth = 2
|
|
157
|
+
drawRoundRect(ctx, 1, y + 1, CANVAS_WIDTH - 2, TOAST_HEIGHT - 2, radius)
|
|
158
|
+
ctx.stroke()
|
|
159
|
+
|
|
160
|
+
// Icon
|
|
161
|
+
drawIcon(ctx, toast.variant, iconCenterX, y + TOAST_HEIGHT / 2, 28)
|
|
162
|
+
|
|
163
|
+
// Message text
|
|
164
|
+
ctx.fillStyle = '#ffffff'
|
|
165
|
+
ctx.font = '28px sans-serif'
|
|
166
|
+
ctx.textBaseline = 'middle'
|
|
167
|
+
ctx.fillText(toast.message, textStartX, y + TOAST_HEIGHT / 2)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
texture.needsUpdate = true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Combined effect: resize canvas, update geometry, render
|
|
174
|
+
$effect(() => {
|
|
175
|
+
const toasts = visibleToasts
|
|
176
|
+
|
|
177
|
+
if (toasts.length === 0) {
|
|
178
|
+
renderToasts([])
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const h = toasts.length * (TOAST_HEIGHT + TOAST_GAP) - TOAST_GAP
|
|
183
|
+
canvas.height = h
|
|
184
|
+
|
|
185
|
+
untrack(() => geometry?.dispose())
|
|
186
|
+
const aspect = CANVAS_WIDTH / h
|
|
187
|
+
geometry = new PlaneGeometry(PLANE_WIDTH, PLANE_WIDTH / aspect)
|
|
188
|
+
|
|
189
|
+
renderToasts(toasts)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Cleanup
|
|
193
|
+
$effect(() => {
|
|
194
|
+
return () => {
|
|
195
|
+
texture.dispose()
|
|
196
|
+
untrack(() => geometry?.dispose())
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<Headset>
|
|
202
|
+
{#if hasToasts && geometry}
|
|
203
|
+
<T.Mesh
|
|
204
|
+
position={[0, -0.3, -1.5]}
|
|
205
|
+
renderOrder={999}
|
|
206
|
+
>
|
|
207
|
+
<T is={geometry} />
|
|
208
|
+
<T.MeshBasicMaterial
|
|
209
|
+
map={texture}
|
|
210
|
+
transparent
|
|
211
|
+
depthTest={false}
|
|
212
|
+
/>
|
|
213
|
+
</T.Mesh>
|
|
214
|
+
{/if}
|
|
215
|
+
</Headset>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Quaternion, Vector3 } from 'three';
|
|
2
|
+
export declare function getFrameTransformationQuaternion(): Quaternion;
|
|
3
|
+
/**
|
|
4
|
+
* Calculates the delta position in Robot Frame
|
|
5
|
+
*/
|
|
6
|
+
export declare function calculatePositionTarget(currentControllerPos: Vector3, referenceControllerPos: Vector3, robotReferencePos: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
z: number;
|
|
10
|
+
}, qTransform: Quaternion, scaleFactor: number): {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
z: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Quaternion, Vector3 } from 'three';
|
|
2
|
+
export function getFrameTransformationQuaternion() {
|
|
3
|
+
// MATCHING DART IMPLEMENTATION EXACTLY:
|
|
4
|
+
// 1: Rotate -90° around Z-axis
|
|
5
|
+
const rotZ = new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), -Math.PI / 2);
|
|
6
|
+
// 2: Rotate 90° around X-axis
|
|
7
|
+
const rotX = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2);
|
|
8
|
+
// Combine: Apply rotX first, then rotZ
|
|
9
|
+
return rotZ.multiply(rotX);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Calculates the delta position in Robot Frame
|
|
13
|
+
*/
|
|
14
|
+
export function calculatePositionTarget(currentControllerPos, referenceControllerPos, robotReferencePos, qTransform, scaleFactor) {
|
|
15
|
+
// 1. Get delta in XR space (Meters)
|
|
16
|
+
const deltaXR = currentControllerPos.clone().sub(referenceControllerPos);
|
|
17
|
+
// 2. Convert to Robot Frame
|
|
18
|
+
const deltaRobot = deltaXR.clone().applyQuaternion(qTransform);
|
|
19
|
+
// 3. Scale (Meters -> Millimeters) and Apply
|
|
20
|
+
const scaleMM = scaleFactor * 1000;
|
|
21
|
+
return {
|
|
22
|
+
x: robotReferencePos.x + deltaRobot.x * scaleMM,
|
|
23
|
+
y: robotReferencePos.y + deltaRobot.y * scaleMM,
|
|
24
|
+
z: robotReferencePos.z + deltaRobot.z * scaleMM,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ToastVariant = 'success' | 'danger' | 'warning' | 'info';
|
|
2
|
+
export interface XRToastItem {
|
|
3
|
+
id: number;
|
|
4
|
+
message: string;
|
|
5
|
+
variant: ToastVariant;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
duration: number;
|
|
8
|
+
}
|
|
9
|
+
declare class VRToastStore {
|
|
10
|
+
toasts: XRToastItem[];
|
|
11
|
+
private nextId;
|
|
12
|
+
add(message: string, variant?: ToastVariant, duration?: number): number;
|
|
13
|
+
remove(id: number): void;
|
|
14
|
+
success(message: string, duration?: number): number;
|
|
15
|
+
danger(message: string, duration?: number): number;
|
|
16
|
+
warning(message: string, duration?: number): number;
|
|
17
|
+
info(message: string, duration?: number): number;
|
|
18
|
+
}
|
|
19
|
+
export declare const xrToast: VRToastStore;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class VRToastStore {
|
|
2
|
+
toasts = $state([]);
|
|
3
|
+
nextId = 0;
|
|
4
|
+
add(message, variant = 'info', duration = 3000) {
|
|
5
|
+
const id = this.nextId++;
|
|
6
|
+
this.toasts.push({
|
|
7
|
+
id,
|
|
8
|
+
message,
|
|
9
|
+
variant,
|
|
10
|
+
createdAt: Date.now(),
|
|
11
|
+
duration,
|
|
12
|
+
});
|
|
13
|
+
setTimeout(() => this.remove(id), duration);
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
remove(id) {
|
|
17
|
+
this.toasts = this.toasts.filter((t) => t.id !== id);
|
|
18
|
+
}
|
|
19
|
+
success(message, duration) {
|
|
20
|
+
return this.add(message, 'success', duration);
|
|
21
|
+
}
|
|
22
|
+
danger(message, duration) {
|
|
23
|
+
return this.add(message, 'danger', duration);
|
|
24
|
+
}
|
|
25
|
+
warning(message, duration) {
|
|
26
|
+
return this.add(message, 'warning', duration);
|
|
27
|
+
}
|
|
28
|
+
info(message, duration) {
|
|
29
|
+
return this.add(message, 'info', duration);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export const xrToast = new VRToastStore();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Vector3Tuple } from 'three';
|
|
2
2
|
interface Context {
|
|
3
3
|
position: Vector3Tuple;
|
|
4
4
|
rotation: number;
|
|
5
|
-
set: (pos?:
|
|
5
|
+
set: (pos?: Vector3Tuple, rot?: number) => void;
|
|
6
6
|
}
|
|
7
7
|
export declare const provideOrigin: () => void;
|
|
8
8
|
export declare const useOrigin: () => Context;
|
|
@@ -12,11 +12,11 @@ export const provideOrigin = () => {
|
|
|
12
12
|
},
|
|
13
13
|
set(pos, rot) {
|
|
14
14
|
if (pos) {
|
|
15
|
-
position[0] = pos
|
|
16
|
-
position[1] = pos
|
|
17
|
-
position[2] = pos
|
|
15
|
+
position[0] = pos[0];
|
|
16
|
+
position[1] = pos[1];
|
|
17
|
+
position[2] = pos[2];
|
|
18
18
|
}
|
|
19
|
-
if (rot) {
|
|
19
|
+
if (rot !== undefined) {
|
|
20
20
|
rotation = rot;
|
|
21
21
|
}
|
|
22
22
|
},
|
package/dist/ecs/traits.d.ts
CHANGED
|
@@ -40,6 +40,15 @@ export declare const InstancedPose: import("koota").Trait<{
|
|
|
40
40
|
theta: number;
|
|
41
41
|
index: number;
|
|
42
42
|
}>;
|
|
43
|
+
export declare const WorldPose: import("koota").Trait<{
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
z: number;
|
|
47
|
+
oX: number;
|
|
48
|
+
oY: number;
|
|
49
|
+
oZ: number;
|
|
50
|
+
theta: number;
|
|
51
|
+
}>;
|
|
43
52
|
export declare const Hovered: import("koota").Trait<() => boolean>;
|
|
44
53
|
/**
|
|
45
54
|
* Represents that an entity is composed of many instances, so that the treeview and
|
package/dist/ecs/traits.js
CHANGED
|
@@ -18,6 +18,15 @@ export const InstancedPose = trait({
|
|
|
18
18
|
theta: 0,
|
|
19
19
|
index: -1,
|
|
20
20
|
});
|
|
21
|
+
export const WorldPose = trait({
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0,
|
|
24
|
+
z: 0,
|
|
25
|
+
oX: 0,
|
|
26
|
+
oY: 0,
|
|
27
|
+
oZ: 1,
|
|
28
|
+
theta: 0,
|
|
29
|
+
});
|
|
21
30
|
export const Hovered = trait(() => true);
|
|
22
31
|
/**
|
|
23
32
|
* Represents that an entity is composed of many instances, so that the treeview and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Entity, type Trait, type World } from 'koota';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
type AoSFactory = () => unknown;
|
|
3
|
+
type Schema = {
|
|
4
4
|
[key: string]: number | bigint | string | boolean | null | undefined | (() => unknown);
|
|
5
5
|
} | AoSFactory | Record<string, never>;
|
|
6
6
|
type TraitRecordFromSchema<T extends Schema> = T extends AoSFactory ? ReturnType<T> : {
|
|
@@ -11,7 +11,7 @@ type TraitRecordFromSchema<T extends Schema> = T extends AoSFactory ? ReturnType
|
|
|
11
11
|
* For SoA it is a snapshot of the state for a single entity.
|
|
12
12
|
* For AoS it is the state instance for a single entity.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
type TraitRecord<T extends Trait | Schema> = T extends Trait ? TraitRecordFromSchema<T['schema']> : TraitRecordFromSchema<T>;
|
|
15
15
|
export declare function isWorld(target: Entity | World | null | undefined): target is World;
|
|
16
16
|
export declare function useTrait<T extends Trait>(target: () => Entity | World | undefined | null, trait: T): {
|
|
17
17
|
current: TraitRecord<T> | undefined;
|
package/dist/frame.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Transform } from '@viamrobotics/sdk';
|
|
2
|
-
import type { ValueOf } from 'type-fest';
|
|
3
2
|
type FrameGeometryMap = {
|
|
4
3
|
none: {
|
|
5
4
|
type: 'none';
|
|
@@ -21,7 +20,6 @@ type FrameGeometryMap = {
|
|
|
21
20
|
};
|
|
22
21
|
};
|
|
23
22
|
export type FrameGeometry = keyof FrameGeometryMap;
|
|
24
|
-
export type FrameGeometries = ValueOf<FrameGeometryMap>;
|
|
25
23
|
type FrameOrientationMap = {
|
|
26
24
|
quaternion: {
|
|
27
25
|
type: 'quaternion';
|
|
@@ -60,7 +58,6 @@ type FrameOrientationMap = {
|
|
|
60
58
|
};
|
|
61
59
|
};
|
|
62
60
|
export type FrameOrientation = keyof FrameOrientationMap;
|
|
63
|
-
export type FrameOrientations = ValueOf<FrameOrientationMap>;
|
|
64
61
|
export interface Frame<T extends FrameGeometry = FrameGeometry, K extends FrameOrientation = FrameOrientation> {
|
|
65
62
|
id?: string;
|
|
66
63
|
name?: string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface JointLimit {
|
|
2
|
+
id: string;
|
|
3
|
+
min: number;
|
|
4
|
+
max: number;
|
|
5
|
+
}
|
|
6
|
+
interface Context {
|
|
7
|
+
names: string[];
|
|
8
|
+
kinematics: Record<string, JointLimit[] | undefined>;
|
|
9
|
+
}
|
|
10
|
+
export declare const provideArmKinematics: (partID: () => string) => void;
|
|
11
|
+
export declare const useArmKinematics: () => Context;
|
|
12
|
+
export {};
|