@viamrobotics/motion-tools 1.21.0 → 1.23.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 (37) hide show
  1. package/README.md +18 -100
  2. package/dist/FrameConfigUpdater.svelte.d.ts +0 -1
  3. package/dist/FrameConfigUpdater.svelte.js +6 -24
  4. package/dist/components/App.svelte +3 -3
  5. package/dist/components/App.svelte.d.ts +1 -1
  6. package/dist/components/CameraControls.svelte +6 -6
  7. package/dist/components/Entities/Pose.svelte +18 -13
  8. package/dist/components/FileDrop/useFileDrop.svelte.js +16 -2
  9. package/dist/components/{KeyboardControls.svelte → InputBindings.svelte} +50 -77
  10. package/dist/components/InputBindings.svelte.d.ts +7 -0
  11. package/dist/components/PointerMissBox.svelte +1 -1
  12. package/dist/components/Scene.svelte +2 -0
  13. package/dist/components/SceneProviders.svelte +2 -0
  14. package/dist/components/SelectedTransformControls.svelte +227 -0
  15. package/dist/components/SelectedTransformControls.svelte.d.ts +3 -0
  16. package/dist/components/StaticGeometries.svelte +3 -56
  17. package/dist/components/overlay/Details.svelte +82 -54
  18. package/dist/components/overlay/dashboard/Button.svelte +4 -2
  19. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  20. package/dist/components/overlay/dashboard/Dashboard.svelte +43 -33
  21. package/dist/ecs/traits.d.ts +15 -0
  22. package/dist/ecs/traits.js +7 -0
  23. package/dist/editing/FrameEditSession.d.ts +37 -0
  24. package/dist/editing/FrameEditSession.js +178 -0
  25. package/dist/hooks/useEnvironment.svelte.d.ts +1 -0
  26. package/dist/hooks/useEnvironment.svelte.js +1 -0
  27. package/dist/hooks/useFrameEditSession.svelte.d.ts +15 -0
  28. package/dist/hooks/useFrameEditSession.svelte.js +36 -0
  29. package/dist/hooks/useFrames.svelte.js +45 -5
  30. package/dist/hooks/usePartConfig.svelte.js +10 -0
  31. package/dist/hooks/useSettings.svelte.d.ts +1 -3
  32. package/dist/hooks/useSettings.svelte.js +1 -3
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +2 -0
  35. package/dist/transform.js +13 -0
  36. package/package.json +8 -6
  37. package/dist/components/KeyboardControls.svelte.d.ts +0 -7
@@ -0,0 +1,227 @@
1
+ <script lang="ts">
2
+ import { TransformControls } from '@threlte/extras'
3
+ import { Quaternion, Vector3 } from 'three'
4
+
5
+ import type { FrameEditSession } from '../editing/FrameEditSession'
6
+
7
+ import { traits, useTrait } from '../ecs'
8
+ import { useTransformControls } from '../hooks/useControls.svelte'
9
+ import { useEnvironment } from '../hooks/useEnvironment.svelte'
10
+ import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
11
+ import { useSelectedEntity, useSelectedObject3d } from '../hooks/useSelection.svelte'
12
+ import { useSettings } from '../hooks/useSettings.svelte'
13
+ import {
14
+ composeEditedPoseForRenderedPose,
15
+ createPose,
16
+ quaternionToPose,
17
+ vector3ToPose,
18
+ } from '../transform'
19
+
20
+ const settings = useSettings()
21
+ const environment = useEnvironment()
22
+ const transformControls = useTransformControls()
23
+ const selectedEntity = useSelectedEntity()
24
+ const selectedObject3d = useSelectedObject3d()
25
+ const sessions = useFrameEditSession()
26
+
27
+ const mode = $derived(settings.current.transformMode)
28
+ const entity = $derived(selectedEntity.current)
29
+ const transformable = useTrait(() => entity, traits.Transformable)
30
+ const networkPose = useTrait(() => entity, traits.Pose)
31
+ const livePose = useTrait(() => entity, traits.LivePose)
32
+ const box = useTrait(() => entity, traits.Box)
33
+ const sphere = useTrait(() => entity, traits.Sphere)
34
+ const capsule = useTrait(() => entity, traits.Capsule)
35
+ const hasScalableGeometry = $derived(
36
+ box.current !== undefined || sphere.current !== undefined || capsule.current !== undefined
37
+ )
38
+
39
+ // Mesh sets name={entity} on its inner mesh, so useSelectedObject3d resolves
40
+ // to that mesh — not the parent Frame Group we actually want to drive. Walk
41
+ // up to the Group so translate/rotate/scale apply to the whole frame, not
42
+ // the geometry inside it.
43
+ const ref = $derived(selectedObject3d.current?.parent ?? selectedObject3d.current)
44
+
45
+ const activeMode = $derived.by(() => {
46
+ if (mode === 'none' || !transformable.current) return undefined
47
+ // Scale only does anything for primitive geometries the gizmo can size.
48
+ if (mode === 'scale' && !hasScalableGeometry) return undefined
49
+ return mode
50
+ })
51
+ const isSphereScale = $derived(activeMode === 'scale' && sphere.current !== undefined)
52
+ const isCapsuleScale = $derived(activeMode === 'scale' && capsule.current !== undefined)
53
+
54
+ const quaternion = new Quaternion()
55
+ const vector3 = new Vector3()
56
+ const refPose = createPose()
57
+
58
+ let session: FrameEditSession | undefined
59
+ let scaleStart:
60
+ | { type: 'box'; x: number; y: number; z: number }
61
+ | { type: 'sphere'; r: number }
62
+ | { type: 'capsule'; r: number; l: number }
63
+ | undefined
64
+
65
+ const captureScaleStart = () => {
66
+ if (!entity || activeMode !== 'scale') {
67
+ scaleStart = undefined
68
+ return
69
+ }
70
+
71
+ const box = entity.get(traits.Box)
72
+ if (box) {
73
+ scaleStart = { type: 'box', ...box }
74
+ return
75
+ }
76
+
77
+ const sphere = entity.get(traits.Sphere)
78
+ if (sphere) {
79
+ scaleStart = { type: 'sphere', ...sphere }
80
+ return
81
+ }
82
+
83
+ const capsule = entity.get(traits.Capsule)
84
+ if (capsule) {
85
+ scaleStart = { type: 'capsule', ...capsule }
86
+ return
87
+ }
88
+
89
+ scaleStart = undefined
90
+ }
91
+
92
+ const onMouseDown = () => {
93
+ if (entity?.has(traits.FramesAPI)) {
94
+ session = sessions.begin([entity])
95
+ }
96
+ captureScaleStart()
97
+ environment.current.viewerMode = 'edit'
98
+ transformControls.setActive(true)
99
+ }
100
+
101
+ const onChange = () => {
102
+ if (!ref || !entity || !activeMode) {
103
+ return
104
+ }
105
+
106
+ const isFrameEntity = entity.has(traits.FramesAPI)
107
+
108
+ if (activeMode === 'translate' || activeMode === 'rotate') {
109
+ if (isFrameEntity) {
110
+ stageFrameTransform()
111
+ } else {
112
+ const pose = entity.get(traits.Pose)
113
+ if (pose) {
114
+ if (activeMode === 'translate') {
115
+ vector3ToPose(ref.getWorldPosition(vector3), pose)
116
+ } else {
117
+ quaternionToPose(ref.getWorldQuaternion(quaternion), pose)
118
+ ref.quaternion.copy(quaternion)
119
+ }
120
+ entity.set(traits.Pose, pose)
121
+ }
122
+ }
123
+ } else {
124
+ // scale → bake the gizmo's scale factor into the geometry trait,
125
+ // then reset the object's scale so subsequent drags start from 1.
126
+ if (!scaleStart) {
127
+ captureScaleStart()
128
+ }
129
+
130
+ if (scaleStart?.type === 'box') {
131
+ const next = {
132
+ x: scaleStart.x * ref.scale.x,
133
+ y: scaleStart.y * ref.scale.y,
134
+ z: scaleStart.z * ref.scale.z,
135
+ }
136
+ if (isFrameEntity) {
137
+ session?.stageGeometry(entity, { type: 'box', ...next })
138
+ } else {
139
+ entity.set(traits.Box, next)
140
+ }
141
+ } else if (scaleStart?.type === 'sphere') {
142
+ const next = { r: scaleStart.r * ref.scale.x }
143
+ if (isFrameEntity) {
144
+ session?.stageGeometry(entity, { type: 'sphere', ...next })
145
+ } else {
146
+ entity.set(traits.Sphere, next)
147
+ }
148
+ } else if (scaleStart?.type === 'capsule') {
149
+ const next = { r: scaleStart.r * ref.scale.x, l: scaleStart.l * ref.scale.y }
150
+ if (isFrameEntity) {
151
+ session?.stageGeometry(entity, { type: 'capsule', ...next })
152
+ } else {
153
+ entity.set(traits.Capsule, next)
154
+ }
155
+ }
156
+
157
+ ref.scale.setScalar(1)
158
+ }
159
+ }
160
+
161
+ const onMouseUp = () => {
162
+ session?.commit()
163
+ session = undefined
164
+ scaleStart = undefined
165
+ transformControls.setActive(false)
166
+ }
167
+
168
+ // Pose.svelte renders frame entities through the live blend
169
+ // render = M(live) × M(network)⁻¹ × M(edited)
170
+ // so for the user's drag to render where they pulled the gizmo to, EditedPose
171
+ // must satisfy
172
+ // M(edited) = M(network) × M(live)⁻¹ × M(ref)
173
+ // where M(ref) is the gizmo-driven group's parent-relative matrix in mm.
174
+ // When live ≈ network (no kinematic offset), this collapses to M(edited) =
175
+ // M(ref) — the same as the naive writeback. When they diverge (e.g. an arm
176
+ // whose joints have moved away from its config pose), this composition is
177
+ // what keeps the rendering anchored to the user's pointer instead of
178
+ // shearing through the live × baseline⁻¹ offset.
179
+ const stageFrameTransform = () => {
180
+ if (!ref || !entity) return
181
+
182
+ vector3ToPose(ref.position, refPose)
183
+ quaternionToPose(ref.quaternion, refPose)
184
+
185
+ const live = livePose.current
186
+ const network = networkPose.current
187
+
188
+ if (!live || !network) {
189
+ // No live pose available — Pose.svelte's blend short-circuits to
190
+ // editedPose, so naive writeback is correct.
191
+ if (activeMode === 'translate') {
192
+ session?.stagePose(entity, {
193
+ x: refPose.x,
194
+ y: refPose.y,
195
+ z: refPose.z,
196
+ })
197
+ } else if (activeMode === 'rotate') {
198
+ session?.stagePose(entity, {
199
+ oX: refPose.oX,
200
+ oY: refPose.oY,
201
+ oZ: refPose.oZ,
202
+ theta: refPose.theta,
203
+ })
204
+ }
205
+ return
206
+ }
207
+
208
+ session?.stagePose(entity, composeEditedPoseForRenderedPose(network, live, refPose))
209
+ }
210
+ </script>
211
+
212
+ {#if ref && entity && activeMode}
213
+ {#key entity}
214
+ <TransformControls
215
+ object={ref}
216
+ mode={activeMode}
217
+ translationSnap={settings.current.snapping ? 0.1 : undefined}
218
+ rotationSnap={settings.current.snapping ? Math.PI / 24 : undefined}
219
+ scaleSnap={settings.current.snapping ? 0.1 : undefined}
220
+ showY={!isSphereScale}
221
+ showZ={!isSphereScale && !isCapsuleScale}
222
+ onmouseDown={onMouseDown}
223
+ onobjectChange={onChange}
224
+ onmouseUp={onMouseUp}
225
+ />
226
+ {/key}
227
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const SelectedTransformControls: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SelectedTransformControls = ReturnType<typeof SelectedTransformControls>;
3
+ export default SelectedTransformControls;
@@ -8,22 +8,15 @@
8
8
  <script lang="ts">
9
9
  import type { Entity } from 'koota'
10
10
 
11
- import { TransformControls } from '@threlte/extras'
12
11
  import { PressedKeys } from 'runed'
13
12
  import { SvelteSet } from 'svelte/reactivity'
14
- import { Quaternion, Vector3 } from 'three'
15
13
 
16
14
  import { traits, useWorld } from '../ecs'
17
- import { useTransformControls } from '../hooks/useControls.svelte'
18
15
  import { useSelectedEntity } from '../hooks/useSelection.svelte'
19
- import { useSettings } from '../hooks/useSettings.svelte'
20
- import { quaternionToPose, vector3ToPose } from '../transform'
21
16
 
22
17
  import Frame from './Entities/Frame.svelte'
23
18
 
24
19
  const world = useWorld()
25
- const settings = useSettings()
26
- const transformControls = useTransformControls()
27
20
  const selectedEntity = useSelectedEntity()
28
21
 
29
22
  const entities = new SvelteSet<Entity>()
@@ -31,11 +24,6 @@
31
24
  [...entities].find((entity) => entity === selectedEntity.current)
32
25
  )
33
26
 
34
- const mode = $derived(settings.current.transformMode)
35
-
36
- const quaternion = new Quaternion()
37
- const vector3 = new Vector3()
38
-
39
27
  const keys = new PressedKeys()
40
28
 
41
29
  keys.onKeys('=', () => {
@@ -43,7 +31,8 @@
43
31
  traits.Name(`custom geometry ${++index}`),
44
32
  traits.Pose,
45
33
  traits.Box({ x: 100, y: 100, z: 100 }),
46
- traits.Removable
34
+ traits.Removable,
35
+ traits.Transformable
47
36
  )
48
37
 
49
38
  entities.add(entity)
@@ -57,50 +46,8 @@
57
46
  selectedEntity.set()
58
47
  }
59
48
  })
60
-
61
- $effect(() => {
62
- settings.current.transforming = selectedCustomGeometry !== undefined
63
- })
64
49
  </script>
65
50
 
66
51
  {#each entities as entity (entity)}
67
- <Frame {entity}>
68
- {#snippet children({ ref })}
69
- {#if selectedEntity.current === entity}
70
- {#key mode}
71
- <TransformControls
72
- object={ref}
73
- {mode}
74
- translationSnap={settings.current.snapping ? 0.1 : undefined}
75
- rotationSnap={settings.current.snapping ? Math.PI / 24 : undefined}
76
- scaleSnap={settings.current.snapping ? 0.1 : undefined}
77
- onmouseDown={() => {
78
- transformControls.setActive(true)
79
- }}
80
- onmouseUp={() => {
81
- transformControls.setActive(false)
82
-
83
- const pose = entity.get(traits.Pose)
84
- const box = entity.get(traits.Box)
85
-
86
- if (pose && mode === 'translate') {
87
- vector3ToPose(ref.getWorldPosition(vector3), pose)
88
- entity.set(traits.Pose, pose)
89
- } else if (pose && mode === 'rotate') {
90
- quaternionToPose(ref.getWorldQuaternion(quaternion), pose)
91
- ref.quaternion.copy(quaternion)
92
- entity.set(traits.Pose, pose)
93
- } else if (box && mode === 'scale') {
94
- box.x *= ref.scale.x
95
- box.y *= ref.scale.y
96
- box.z *= ref.scale.z
97
- entity.set(traits.Box, box)
98
- ref.scale.setScalar(1)
99
- }
100
- }}
101
- />
102
- {/key}
103
- {/if}
104
- {/snippet}
105
- </Frame>
52
+ <Frame {entity} />
106
53
  {/each}
@@ -11,12 +11,6 @@
11
11
  const quaternion = new Quaternion()
12
12
  const ov = new OrientationVector()
13
13
  const euler = new Euler()
14
-
15
- ThemeUtils.setGlobalDefaultTheme({
16
- ...ThemeUtils.presets.light,
17
- baseBackgroundColor: '#fbfbfc',
18
- baseShadowColor: 'transparent',
19
- })
20
14
  </script>
21
15
 
22
16
  <script lang="ts">
@@ -24,7 +18,7 @@
24
18
  import type { Snippet } from 'svelte'
25
19
 
26
20
  import { draggable } from '@neodrag/svelte'
27
- import { isInstanceOf, useTask } from '@threlte/core'
21
+ import { isInstanceOf, useTask, useThrelte } from '@threlte/core'
28
22
  import { Button, Icon, Tooltip } from '@viamrobotics/prime-core'
29
23
  import { Check, Copy } from 'lucide-svelte'
30
24
  import {
@@ -68,6 +62,7 @@
68
62
  const { details }: Props = $props()
69
63
 
70
64
  const world = useWorld()
65
+ const { invalidate } = useThrelte()
71
66
  const drawService = useDrawService()
72
67
  const controls = useCameraControls()
73
68
  const resourceByName = useResourceByName()
@@ -92,6 +87,7 @@
92
87
  const removable = useTrait(() => entity, traits.Removable)
93
88
  const points = useTrait(() => entity, traits.Points)
94
89
  const arrows = useTrait(() => entity, traits.Arrows)
90
+ const opacity = useTrait(() => entity, traits.Opacity)
95
91
 
96
92
  const framesAPI = useTrait(() => entity, traits.FramesAPI)
97
93
  const isFrameNode = $derived(!!framesAPI.current)
@@ -139,8 +135,6 @@
139
135
  }
140
136
  })
141
137
 
142
- const formatTwoDecimals = (value: number) => value.toFixed(2)
143
-
144
138
  const detailConfigUpdater = new FrameConfigUpdater(partConfig.updateFrame, partConfig.deleteFrame)
145
139
 
146
140
  const handlePositionChange = (event: PointChangeEvent) => {
@@ -205,6 +199,23 @@
205
199
  detailConfigUpdater.updateGeometry(entity, { type: 'capsule', l: event.detail.value })
206
200
  }
207
201
 
202
+ const opacityValue = $derived(opacity.current ?? 1)
203
+
204
+ const handleOpacityChange = (event: SliderChangeEvent) => {
205
+ if (event.detail.origin !== 'internal' || !entity) return
206
+ const next = event.detail.value
207
+ // No trait === fully opaque, so drop the trait when the user returns to 1
208
+ // instead of leaving an Opacity(1) entry on the entity.
209
+ if (next >= 1) {
210
+ entity.remove(traits.Opacity)
211
+ } else if (entity.has(traits.Opacity)) {
212
+ entity.set(traits.Opacity, next)
213
+ } else {
214
+ entity.add(traits.Opacity(next))
215
+ }
216
+ invalidate()
217
+ }
218
+
208
219
  const handleParentChange = (event: ListChangeEvent) => {
209
220
  if (event.detail.origin !== 'internal' || !entity) return
210
221
  const value = event.detail.value as string
@@ -295,6 +306,12 @@
295
306
  2
296
307
  )
297
308
  }
309
+
310
+ ThemeUtils.setGlobalDefaultTheme({
311
+ ...ThemeUtils.presets.light,
312
+ baseBackgroundColor: '#fbfbfc',
313
+ baseShadowColor: 'transparent',
314
+ })
298
315
  </script>
299
316
 
300
317
  {#snippet ImmutableField({
@@ -321,9 +338,7 @@
321
338
  {#if entity}
322
339
  <div
323
340
  id="details-panel"
324
- class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 {showEditFrameOptions
325
- ? 'w-80'
326
- : 'w-60'} border p-2 text-xs dark:text-black"
341
+ class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 w-70 border p-2 text-xs dark:text-black"
327
342
  use:draggable={{
328
343
  bounds: 'body',
329
344
  handle: dragElement,
@@ -492,7 +507,6 @@
492
507
  y: localPose.current.y,
493
508
  z: localPose.current.z,
494
509
  }}
495
- format={formatTwoDecimals}
496
510
  on:change={handlePositionChange}
497
511
  />
498
512
  </div>
@@ -531,7 +545,6 @@
531
545
  z: localPose.current.oZ,
532
546
  w: localPose.current.theta,
533
547
  }}
534
- format={formatTwoDecimals}
535
548
  on:change={handleOrientationOVChange}
536
549
  />
537
550
  </TabPage>
@@ -578,48 +591,49 @@
578
591
  <div aria-label="mutable geometry">
579
592
  <TabGroup bind:selectedIndex={geometryTabIndex}>
580
593
  <TabPage title="None" />
581
- <TabPage title="Box" />
582
- <TabPage title="Sphere" />
583
- <TabPage title="Capsule" />
594
+ <TabPage title="Box">
595
+ {#if box.current}
596
+ <div aria-label="mutable box dimensions">
597
+ <Point
598
+ value={{
599
+ x: box.current.x,
600
+ y: box.current.y,
601
+ z: box.current.z,
602
+ }}
603
+ on:change={handleBoxChange}
604
+ />
605
+ </div>
606
+ {/if}
607
+ </TabPage>
608
+ <TabPage title="Sphere">
609
+ {#if sphere.current}
610
+ <div aria-label="mutable sphere dimensions">
611
+ <Slider
612
+ label="r"
613
+ value={sphere.current.r}
614
+ on:change={handleSphereRChange}
615
+ />
616
+ </div>
617
+ {/if}
618
+ </TabPage>
619
+ <TabPage title="Capsule">
620
+ {#if capsule.current}
621
+ <div aria-label="mutable capsule dimensions">
622
+ <Slider
623
+ label="r"
624
+ value={capsule.current.r}
625
+ on:change={handleCapsuleRChange}
626
+ />
627
+ <Slider
628
+ label="l"
629
+ value={capsule.current.l}
630
+ on:change={handleCapsuleLChange}
631
+ />
632
+ </div>
633
+ {/if}
634
+ </TabPage>
584
635
  </TabGroup>
585
636
  </div>
586
- {#if geometryTabIndex === 1 && box.current}
587
- <div aria-label="mutable box dimensions">
588
- <Point
589
- value={{
590
- x: box.current.x,
591
- y: box.current.y,
592
- z: box.current.z,
593
- }}
594
- format={formatTwoDecimals}
595
- on:change={handleBoxChange}
596
- />
597
- </div>
598
- {:else if geometryTabIndex === 2 && sphere.current}
599
- <div aria-label="mutable sphere dimensions">
600
- <Slider
601
- label="r"
602
- value={sphere.current.r}
603
- format={formatTwoDecimals}
604
- on:change={handleSphereRChange}
605
- />
606
- </div>
607
- {:else if geometryTabIndex === 3 && capsule.current}
608
- <div aria-label="mutable capsule dimensions">
609
- <Slider
610
- label="r"
611
- value={capsule.current.r}
612
- format={formatTwoDecimals}
613
- on:change={handleCapsuleRChange}
614
- />
615
- <Slider
616
- label="l"
617
- value={capsule.current.l}
618
- format={formatTwoDecimals}
619
- on:change={handleCapsuleLChange}
620
- />
621
- </div>
622
- {/if}
623
637
  </div>
624
638
  {:else if box.current}
625
639
  <div>
@@ -673,6 +687,20 @@
673
687
  </div>
674
688
  {/if}
675
689
 
690
+ <div>
691
+ <strong class="font-semibold">opacity</strong>
692
+ <div aria-label="mutable opacity">
693
+ <Slider
694
+ value={opacityValue}
695
+ min={0}
696
+ max={1}
697
+ step={0.01}
698
+ format={(v) => v.toFixed(2)}
699
+ on:change={handleOpacityChange}
700
+ />
701
+ </div>
702
+ </div>
703
+
676
704
  {#if isInstanceOf(object3d, 'Points')}
677
705
  <div>
678
706
  <strong class="font-semibold">points</strong>
@@ -2,10 +2,10 @@
2
2
  import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements'
3
3
 
4
4
  import { Icon, type IconName, Tooltip } from '@viamrobotics/prime-core'
5
- import { Ruler } from 'lucide-svelte'
5
+ import { MousePointer2, Ruler } from 'lucide-svelte'
6
6
 
7
7
  interface Props extends HTMLButtonAttributes {
8
- icon: IconName | 'ruler'
8
+ icon: IconName | 'ruler' | 'mouse-pointer'
9
9
  active?: boolean
10
10
  description: string
11
11
  hotkey?: string
@@ -48,6 +48,8 @@
48
48
  >
49
49
  {#if icon === 'ruler'}
50
50
  <Ruler size="16" />
51
+ {:else if icon === 'mouse-pointer'}
52
+ <MousePointer2 size="16" />
51
53
  {:else}
52
54
  <Icon name={icon} />
53
55
  {/if}
@@ -1,7 +1,7 @@
1
1
  import type { ClassValue, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
2
2
  import { type IconName } from '@viamrobotics/prime-core';
3
3
  interface Props extends HTMLButtonAttributes {
4
- icon: IconName | 'ruler';
4
+ icon: IconName | 'ruler' | 'mouse-pointer';
5
5
  active?: boolean;
6
6
  description: string;
7
7
  hotkey?: string;
@@ -38,40 +38,50 @@
38
38
  </fieldset>
39
39
 
40
40
  <!-- transform -->
41
- {#if settings.current.transforming}
42
- <fieldset class="flex">
43
- <Button
44
- icon="cursor-move"
45
- active={settings.current.transformMode === 'translate'}
46
- description="Translate"
47
- hotkey="1"
48
- onclick={() => {
49
- settings.current.transformMode = 'translate'
50
- }}
51
- />
52
- <Button
53
- icon="sync"
54
- active={settings.current.transformMode === 'rotate'}
55
- description="Rotate"
56
- hotkey="2"
57
- class="-ml-px"
58
- onclick={() => {
59
- settings.current.transformMode = 'rotate'
60
- }}
61
- />
62
- <Button
63
- icon="resize"
64
- active={settings.current.transformMode === 'scale'}
65
- description="Scale"
66
- hotkey="3"
67
- class="-ml-px"
68
- onclick={() => {
69
- settings.current.transformMode = 'scale'
70
- }}
71
- />
72
- </fieldset>
41
+ <fieldset class="flex">
42
+ <Button
43
+ icon="mouse-pointer"
44
+ active={settings.current.transformMode === 'none'}
45
+ description="No transform controls"
46
+ hotkey="0"
47
+ onclick={() => {
48
+ settings.current.transformMode = 'none'
49
+ }}
50
+ />
51
+ <Button
52
+ icon="cursor-move"
53
+ active={settings.current.transformMode === 'translate'}
54
+ description="Translate"
55
+ hotkey="1"
56
+ class="-ml-px"
57
+ onclick={() => {
58
+ settings.current.transformMode = 'translate'
59
+ }}
60
+ />
61
+ <Button
62
+ icon="sync"
63
+ active={settings.current.transformMode === 'rotate'}
64
+ description="Rotate"
65
+ hotkey="2"
66
+ class="-ml-px"
67
+ onclick={() => {
68
+ settings.current.transformMode = 'rotate'
69
+ }}
70
+ />
71
+ <Button
72
+ icon="resize"
73
+ active={settings.current.transformMode === 'scale'}
74
+ description="Scale"
75
+ hotkey="3"
76
+ class="-ml-px"
77
+ onclick={() => {
78
+ settings.current.transformMode = 'scale'
79
+ }}
80
+ />
81
+ </fieldset>
73
82
 
74
- <!-- snapping -->
83
+ <!-- snapping -->
84
+ {#if settings.current.transformMode !== 'none'}
75
85
  <fieldset class="flex">
76
86
  <Button
77
87
  icon={settings.current.snapping ? 'magnet' : 'magnet-off'}
@@ -23,6 +23,15 @@ export declare const EditedPose: import("koota").Trait<{
23
23
  oZ: number;
24
24
  theta: number;
25
25
  }>;
26
+ export declare const LivePose: import("koota").Trait<{
27
+ x: number;
28
+ y: number;
29
+ z: number;
30
+ oX: number;
31
+ oY: number;
32
+ oZ: number;
33
+ theta: number;
34
+ }>;
26
35
  export declare const Center: import("koota").Trait<{
27
36
  x: number;
28
37
  y: number;
@@ -146,6 +155,12 @@ export declare const SnapshotAPI: import("koota").Trait<() => boolean>;
146
155
  * Marker trait for entities created from user-dropped files (PLY, PCD, etc.)
147
156
  */
148
157
  export declare const DroppedFile: import("koota").Trait<() => boolean>;
158
+ /**
159
+ * Marker trait for entities the dashboard's TransformControls may attach to —
160
+ * editable frames and ad-hoc custom geometries. Other entity kinds (lines,
161
+ * points, batched arrows, etc.) are deliberately excluded.
162
+ */
163
+ export declare const Transformable: import("koota").Trait<() => boolean>;
149
164
  export declare const ShowAxesHelper: import("koota").Trait<() => boolean>;
150
165
  /**
151
166
  * Marker trait for entities that should be rendered in screen space (CSS pixels)