@viamrobotics/motion-tools 0.6.0 → 0.6.2

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.
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { CameraControls, type CameraControlsRef, Gizmo } from '@threlte/extras'
3
+ import { useTransformControls } from '../hooks/useControls.svelte'
4
+ import KeyboardControls from './KeyboardControls.svelte'
5
+ import Portal from './portal/Portal.svelte'
6
+ import Button from './dashboard/Button.svelte'
7
+
8
+ const transformControls = useTransformControls()
9
+
10
+ let ref = $state.raw<CameraControlsRef>()
11
+ </script>
12
+
13
+ <Portal id="dashboard">
14
+ <fieldset>
15
+ <Button
16
+ active
17
+ icon="camera-outline"
18
+ description="Reset camera"
19
+ onclick={() => {
20
+ ref?.reset(true)
21
+ }}
22
+ />
23
+ </fieldset>
24
+ </Portal>
25
+
26
+ <CameraControls
27
+ bind:ref
28
+ enabled={!transformControls.active}
29
+ >
30
+ {#snippet children({ ref }: { ref: CameraControlsRef })}
31
+ <KeyboardControls cameraControls={ref} />
32
+ <Gizmo />
33
+ {/snippet}
34
+ </CameraControls>
@@ -0,0 +1,4 @@
1
+ import { CameraControls } from '@threlte/extras';
2
+ declare const CameraControls: import("svelte").Component<Record<string, never>, {}, "">;
3
+ type CameraControls = ReturnType<typeof CameraControls>;
4
+ export default CameraControls;
@@ -16,6 +16,7 @@
16
16
  const settings = useSettings()
17
17
 
18
18
  const keys = new PressedKeys()
19
+ const meta = $derived(keys.has('meta'))
19
20
  const w = $derived(keys.has('w'))
20
21
  const s = $derived(keys.has('s'))
21
22
  const a = $derived(keys.has('a'))
@@ -27,11 +28,15 @@
27
28
  const down = $derived(keys.has('arrowdown'))
28
29
  const right = $derived(keys.has('arrowright'))
29
30
  const any = $derived(w || s || a || d || r || f || up || left || down || right)
30
-
31
31
  const { start, stop } = useTask(
32
32
  (delta) => {
33
33
  const dt = delta * 1000
34
34
 
35
+ // Disallow keyboard navigation if the user is holding down the meta key
36
+ if (meta) {
37
+ return
38
+ }
39
+
35
40
  if (a) {
36
41
  cameraControls.truck(-0.01 * dt, 0, true)
37
42
  }
@@ -1,11 +1,18 @@
1
1
  <script lang="ts">
2
- import { Points, BufferAttribute, BufferGeometry, PointsMaterial } from 'three'
2
+ import {
3
+ Points,
4
+ BufferAttribute,
5
+ BufferGeometry,
6
+ PointsMaterial,
7
+ OrthographicCamera,
8
+ } from 'three'
3
9
 
4
- import { T } from '@threlte/core'
10
+ import { T, useTask, useThrelte } from '@threlte/core'
5
11
  import type { WorldObject } from '../WorldObject'
6
12
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
7
13
  import { meshBounds } from '@threlte/extras'
8
14
  import { poseToObject3d } from '../transform'
15
+ import { useSettings } from '../hooks/useSettings.svelte'
9
16
 
10
17
  interface Props {
11
18
  object: WorldObject<{ case: 'points'; value: Float32Array<ArrayBuffer> }>
@@ -13,15 +20,25 @@
13
20
 
14
21
  let { object }: Props = $props()
15
22
 
23
+ const { camera } = useThrelte()
24
+ const settings = useSettings()
25
+
16
26
  const colors = $derived(object.metadata.colors)
27
+ const pointSize = $derived(object.metadata.pointSize ?? settings.current.pointSize)
17
28
  const positions = $derived(object.geometry?.value ?? new Float32Array())
29
+ const orthographic = $derived(settings.current.cameraMode === 'orthographic')
18
30
 
19
31
  const points = new Points()
20
32
  const geometry = new BufferGeometry()
21
33
  const material = new PointsMaterial()
34
+ material.toneMapped = false
35
+
36
+ $effect.pre(() => {
37
+ material.size = pointSize
38
+ })
22
39
 
23
40
  $effect.pre(() => {
24
- material.size = object.metadata.pointSize ?? 0.01
41
+ material.color.set(colors ? 0xffffff : (object.metadata.color ?? settings.current.pointColor))
25
42
  })
26
43
 
27
44
  $effect.pre(() => {
@@ -30,9 +47,7 @@
30
47
 
31
48
  $effect.pre(() => {
32
49
  material.vertexColors = colors !== undefined
33
- material.color.set(colors ? 0xffffff : (object.metadata.color ?? '#888888'))
34
50
 
35
- material.toneMapped = false
36
51
  if (colors) {
37
52
  geometry.setAttribute('color', new BufferAttribute(colors, 3))
38
53
  geometry.attributes.color.needsUpdate = true
@@ -44,6 +59,24 @@
44
59
  })
45
60
 
46
61
  const events = useObjectEvents(() => object.uuid)
62
+
63
+ const { start, stop } = useTask(
64
+ () => {
65
+ // If using an orthographic camera, points need to be
66
+ // resized to half zoom to take up the same screen space.
67
+ material.size = pointSize * ((camera.current as OrthographicCamera).zoom / 2)
68
+ },
69
+ { autoStart: false }
70
+ )
71
+
72
+ $effect(() => {
73
+ if (orthographic) {
74
+ start()
75
+ } else {
76
+ stop()
77
+ material.size = pointSize
78
+ }
79
+ })
47
80
  </script>
48
81
 
49
82
  <T
@@ -1,13 +1,7 @@
1
1
  <script lang="ts">
2
- import { Color, Vector3 } from 'three'
2
+ import { Vector3 } from 'three'
3
3
  import { T } from '@threlte/core'
4
- import {
5
- CameraControls,
6
- type CameraControlsRef,
7
- Gizmo,
8
- Grid,
9
- interactivity,
10
- } from '@threlte/extras'
4
+ import { Grid, interactivity, PerfMonitor } from '@threlte/extras'
11
5
  import { PortalTarget } from './portal'
12
6
  import Frames from './Frames.svelte'
13
7
  import Pointclouds from './Pointclouds.svelte'
@@ -19,9 +13,10 @@
19
13
  import { useFocusedObject3d } from '../hooks/useSelection.svelte'
20
14
  import type { Snippet } from 'svelte'
21
15
  import { useXR } from '@threlte/xr'
22
- import { useTransformControls } from '../hooks/useControls.svelte'
23
- import KeyboardControls from './KeyboardControls.svelte'
16
+
24
17
  import { useOrigin } from './xr/useOrigin.svelte'
18
+ import { useSettings } from '../hooks/useSettings.svelte'
19
+ import CameraControls from './CameraControls.svelte'
25
20
 
26
21
  interface Props {
27
22
  children?: Snippet
@@ -39,8 +34,8 @@
39
34
  },
40
35
  })
41
36
 
37
+ const settings = useSettings()
42
38
  const focusedObject3d = useFocusedObject3d()
43
- const transformControls = useTransformControls()
44
39
  const origin = useOrigin()
45
40
 
46
41
  const object3d = $derived(focusedObject3d.current)
@@ -48,10 +43,9 @@
48
43
  const { isPresenting } = useXR()
49
44
  </script>
50
45
 
51
- <T.Color
52
- attach="background"
53
- args={[new Color('white')]}
54
- />
46
+ {#if settings.current.renderStats}
47
+ <PerfMonitor anchorX="right" />
48
+ {/if}
55
49
 
56
50
  <T.Group
57
51
  position={origin.position}
@@ -63,12 +57,7 @@
63
57
  {:else}
64
58
  {#if !$isPresenting}
65
59
  <Camera position={[3, 3, 3]}>
66
- <CameraControls enabled={!transformControls.active}>
67
- {#snippet children({ ref }: { ref: CameraControlsRef })}
68
- <KeyboardControls cameraControls={ref} />
69
- <Gizmo />
70
- {/snippet}
71
- </CameraControls>
60
+ <CameraControls />
72
61
  </Camera>
73
62
  {/if}
74
63
 
@@ -81,15 +70,15 @@
81
70
 
82
71
  <Selected />
83
72
 
84
- {#if !$isPresenting}
73
+ {#if !$isPresenting && settings.current.grid}
85
74
  <Grid
86
75
  plane="xy"
87
76
  sectionColor="#333"
88
77
  infiniteGrid
89
- cellSize={0.5}
90
- sectionSize={10}
78
+ cellSize={settings.current.gridCellSize}
79
+ sectionSize={settings.current.gridSectionSize}
91
80
  fadeOrigin={new Vector3()}
92
- fadeDistance={25}
81
+ fadeDistance={settings.current.gridFadeDistance}
93
82
  />
94
83
  {/if}
95
84
  {/if}
@@ -23,6 +23,12 @@
23
23
 
24
24
  keys.onKeys('=', () => geometries.add())
25
25
  keys.onKeys('-', () => geometries.remove(selected.current ?? ''))
26
+
27
+ $effect(() => {
28
+ settings.current.transforming = geometries.current.some(
29
+ (geometry) => selected.current === geometry.uuid
30
+ )
31
+ })
26
32
  </script>
27
33
 
28
34
  {#each geometries.current as object (object.uuid)}
@@ -39,6 +45,9 @@
39
45
  <TransformControls
40
46
  object={ref}
41
47
  {mode}
48
+ translationSnap={settings.current.snapping ? 0.1 : undefined}
49
+ rotationSnap={settings.current.snapping ? Math.PI / 24 : undefined}
50
+ scaleSnap={settings.current.snapping ? 0.1 : undefined}
42
51
  onmouseDown={() => {
43
52
  transformControls.setActive(true)
44
53
  }}
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
- import { Select } from '@viamrobotics/prime-core'
2
+ import { Select, Switch, Input } from '@viamrobotics/prime-core'
3
3
  import RefreshRate from '../RefreshRate.svelte'
4
4
  import { useMotionClient } from '../../hooks/useMotionClient.svelte'
5
5
  import Drawer from './Drawer.svelte'
6
+ import { useSettings } from '../../hooks/useSettings.svelte'
6
7
 
8
+ const settings = useSettings()
7
9
  const motionClient = useMotionClient()
8
10
  </script>
9
11
 
@@ -11,7 +13,9 @@
11
13
  name="Settings"
12
14
  defaultOpen
13
15
  >
14
- <div class="flex flex-col gap-2 p-3">
16
+ <div class="flex h-100 flex-col gap-2 overflow-scroll p-3">
17
+ <h3 class="text-base"><strong>Refresh rates</strong></h3>
18
+
15
19
  <RefreshRate name="Frames">
16
20
  <option value="0">Do not fetch</option>
17
21
  <option value="1">Fetch on reconfigure</option>
@@ -20,8 +24,10 @@
20
24
  <RefreshRate name="Geometries" />
21
25
  <RefreshRate name="Poses" />
22
26
 
27
+ <h3 class="text-base"><strong>Motion</strong></h3>
28
+
23
29
  <label class="flex flex-col gap-1">
24
- Motion client
30
+ Client
25
31
  <Select
26
32
  onchange={(event: InputEvent) => {
27
33
  if (event.target instanceof HTMLSelectElement) {
@@ -35,5 +41,78 @@
35
41
  {/each}
36
42
  </Select>
37
43
  </label>
44
+
45
+ <h3 class="text-base"><strong>Pointclouds</strong></h3>
46
+ <div class="flex flex-col gap-2.5">
47
+ <label class="flex items-center justify-between gap-2">
48
+ Default point size
49
+
50
+ <div class="w-20">
51
+ <Input
52
+ bind:value={settings.current.pointSize}
53
+ on:keydown={(event) => event.stopImmediatePropagation()}
54
+ />
55
+ </div>
56
+ </label>
57
+
58
+ <label class="flex items-center justify-between gap-2">
59
+ Default point color
60
+
61
+ <div class="w-20">
62
+ <Input
63
+ type="color"
64
+ bind:value={settings.current.pointColor}
65
+ on:keydown={(event) => event.stopImmediatePropagation()}
66
+ />
67
+ </div>
68
+ </label>
69
+ </div>
70
+
71
+ <h3 class="text-base"><strong>Grid</strong></h3>
72
+ <div class="flex flex-col gap-2.5">
73
+ <label class="flex items-center justify-between gap-2">
74
+ Enabled <Switch bind:on={settings.current.grid} />
75
+ </label>
76
+
77
+ <label class="flex items-center justify-between gap-2">
78
+ Cell size (m)
79
+
80
+ <div class="w-20">
81
+ <Input
82
+ bind:value={settings.current.gridCellSize}
83
+ on:keydown={(event) => event.stopImmediatePropagation()}
84
+ />
85
+ </div>
86
+ </label>
87
+
88
+ <label class="flex items-center justify-between gap-2">
89
+ Section size (m)
90
+
91
+ <div class="w-20">
92
+ <Input
93
+ bind:value={settings.current.gridSectionSize}
94
+ on:keydown={(event) => event.stopImmediatePropagation()}
95
+ />
96
+ </div>
97
+ </label>
98
+
99
+ <label class="flex items-center justify-between gap-2">
100
+ Fade distance (m)
101
+
102
+ <div class="w-20">
103
+ <Input
104
+ bind:value={settings.current.gridFadeDistance}
105
+ on:keydown={(event) => event.stopImmediatePropagation()}
106
+ />
107
+ </div>
108
+ </label>
109
+ </div>
110
+
111
+ <h3 class="text-base"><strong>Misc</strong></h3>
112
+ <div class="flex flex-col gap-2.5">
113
+ <label class="flex items-center justify-between gap-2">
114
+ Render stats <Switch bind:on={settings.current.renderStats} />
115
+ </label>
116
+ </div>
38
117
  </div>
39
118
  </Drawer>
@@ -5,7 +5,7 @@
5
5
  icon: IconName
6
6
  active?: boolean
7
7
  description: string
8
- hotkey: string
8
+ hotkey?: string
9
9
  class?: string
10
10
  onclick?: () => void
11
11
  }
@@ -14,7 +14,7 @@
14
14
  icon,
15
15
  active = false,
16
16
  description,
17
- hotkey,
17
+ hotkey = '',
18
18
  class: className = '',
19
19
  onclick,
20
20
  }: Props = $props()
@@ -3,7 +3,7 @@ interface Props {
3
3
  icon: IconName;
4
4
  active?: boolean;
5
5
  description: string;
6
- hotkey: string;
6
+ hotkey?: string;
7
7
  class?: string;
8
8
  onclick?: () => void;
9
9
  }
@@ -1,48 +1,12 @@
1
1
  <script>
2
2
  import { useSettings } from '../../hooks/useSettings.svelte'
3
+ import PortalTarget from '../portal/PortalTarget.svelte'
3
4
  import Button from './Button.svelte'
4
5
 
5
6
  const settings = useSettings()
6
7
  </script>
7
8
 
8
- <div class="absolute top-2 flex w-full justify-center">
9
- <!-- transform -->
10
- <!-- <fieldset>
11
- <Button
12
- icon="cursor-move"
13
- active={$transformMode === TransformModes.TRANSLATE}
14
- description="Translate"
15
- hotkey="T"
16
- onclick={() => transformMode.set(TransformModes.TRANSLATE)}
17
- />
18
- <Button
19
- icon="sync"
20
- active={$transformMode === TransformModes.ROTATE}
21
- description="Rotate"
22
- hotkey="R"
23
- class="-my-px"
24
- onclick={() => transformMode.set(TransformModes.ROTATE)}
25
- />
26
- <Button
27
- icon="resize"
28
- active={$transformMode === TransformModes.SCALE}
29
- description="Scale"
30
- hotkey="S"
31
- onclick={() => transformMode.set(TransformModes.SCALE)}
32
- />
33
- </fieldset> -->
34
-
35
- <!-- snapping -->
36
- <!-- <fieldset>
37
- <Button
38
- icon={$snapMode ? 'magnet' : 'magnet-off'}
39
- active={$snapMode === true}
40
- description="Snapping"
41
- hotkey="Spacebar"
42
- onClick={() => snapMode.set(!$snapMode)}
43
- />
44
- </fieldset> -->
45
-
9
+ <div class="absolute top-2 flex w-full justify-center gap-2">
46
10
  <!-- camera view -->
47
11
  <fieldset class="flex">
48
12
  <Button
@@ -65,4 +29,53 @@
65
29
  }}
66
30
  />
67
31
  </fieldset>
32
+
33
+ <!-- transform -->
34
+ {#if settings.current.transforming}
35
+ <fieldset class="flex">
36
+ <Button
37
+ icon="cursor-move"
38
+ active={settings.current.transformMode === 'translate'}
39
+ description="Translate"
40
+ hotkey="1"
41
+ onclick={() => {
42
+ settings.current.transformMode = 'translate'
43
+ }}
44
+ />
45
+ <Button
46
+ icon="sync"
47
+ active={settings.current.transformMode === 'rotate'}
48
+ description="Rotate"
49
+ hotkey="2"
50
+ class="-ml-px"
51
+ onclick={() => {
52
+ settings.current.transformMode = 'rotate'
53
+ }}
54
+ />
55
+ <Button
56
+ icon="resize"
57
+ active={settings.current.transformMode === 'scale'}
58
+ description="Scale"
59
+ hotkey="3"
60
+ class="-ml-px"
61
+ onclick={() => {
62
+ settings.current.transformMode = 'scale'
63
+ }}
64
+ />
65
+ </fieldset>
66
+
67
+ <!-- snapping -->
68
+ <fieldset class="flex">
69
+ <Button
70
+ icon={settings.current.snapping ? 'magnet' : 'magnet-off'}
71
+ active={settings.current.snapping}
72
+ description="Snapping"
73
+ onclick={() => {
74
+ settings.current.snapping = !settings.current.snapping
75
+ }}
76
+ />
77
+ </fieldset>
78
+ {/if}
79
+
80
+ <PortalTarget id="dashboard" />
68
81
  </div>
@@ -1,7 +1,16 @@
1
1
  interface Settings {
2
2
  cameraMode: 'orthographic' | 'perspective';
3
+ transforming: boolean;
4
+ snapping: boolean;
3
5
  transformMode: 'translate' | 'rotate' | 'scale';
6
+ grid: boolean;
7
+ gridCellSize: number;
8
+ gridSectionSize: number;
9
+ gridFadeDistance: number;
10
+ pointSize: number;
11
+ pointColor: string;
4
12
  enableXR: boolean;
13
+ renderStats: boolean;
5
14
  }
6
15
  interface Context {
7
16
  current: Settings;
@@ -3,8 +3,17 @@ import { getContext, setContext } from 'svelte';
3
3
  const key = Symbol('dashboard-context');
4
4
  const defaults = () => ({
5
5
  cameraMode: 'perspective',
6
+ transforming: false,
7
+ snapping: false,
6
8
  transformMode: 'translate',
9
+ grid: true,
10
+ gridCellSize: 0.5,
11
+ gridSectionSize: 10,
12
+ gridFadeDistance: 25,
13
+ pointSize: 0.01,
14
+ pointColor: '#333',
7
15
  enableXR: false,
16
+ renderStats: false,
8
17
  });
9
18
  export const provideSettings = () => {
10
19
  let settings = $state(defaults());
@@ -162,17 +162,16 @@ export const provideShapes = () => {
162
162
  }
163
163
  return colors;
164
164
  };
165
- const pointSize = 0.01;
166
165
  const metadata = nColors > 0
167
166
  ? {
168
167
  colors: getColors(),
169
168
  color: new Color(r, g, b).convertLinearToSRGB(),
170
- pointSize,
171
169
  }
172
- : {
173
- color: new Color(r, g, b).convertLinearToSRGB(),
174
- pointSize,
175
- };
170
+ : r === -1
171
+ ? undefined
172
+ : {
173
+ color: new Color(r, g, b).convertLinearToSRGB(),
174
+ };
176
175
  points.push(new WorldObject(label ?? `points ${++pointsIndex}`, undefined, undefined, {
177
176
  case: 'points',
178
177
  value: positions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -13,11 +13,11 @@
13
13
  "@eslint/compat": "1.3.1",
14
14
  "@eslint/js": "9.31.0",
15
15
  "@playwright/test": "1.54.1",
16
- "@sentry/sveltekit": "9.39.0",
16
+ "@sentry/sveltekit": "9.40.0",
17
17
  "@skeletonlabs/skeleton": "3.1.7",
18
- "@skeletonlabs/skeleton-svelte": "1.3.0",
18
+ "@skeletonlabs/skeleton-svelte": "1.3.1",
19
19
  "@sveltejs/adapter-static": "3.0.8",
20
- "@sveltejs/kit": "2.24.0",
20
+ "@sveltejs/kit": "2.25.1",
21
21
  "@sveltejs/package": "2.4.0",
22
22
  "@sveltejs/vite-plugin-svelte": "6.1.0",
23
23
  "@tailwindcss/forms": "0.5.10",
@@ -37,10 +37,10 @@
37
37
  "@typescript-eslint/parser": "8.37.0",
38
38
  "@viamrobotics/prime-core": "0.1.5",
39
39
  "@viamrobotics/sdk": "0.46.0",
40
- "@viamrobotics/svelte-sdk": "0.4.3",
40
+ "@viamrobotics/svelte-sdk": "0.4.4",
41
41
  "@vitejs/plugin-basic-ssl": "2.1.0",
42
- "@zag-js/svelte": "1.18.3",
43
- "@zag-js/tree-view": "1.18.3",
42
+ "@zag-js/svelte": "1.18.4",
43
+ "@zag-js/tree-view": "1.18.4",
44
44
  "camera-controls": "3.1.0",
45
45
  "eslint": "9.31.0",
46
46
  "eslint-config-prettier": "10.1.5",
@@ -55,8 +55,8 @@
55
55
  "prettier-plugin-tailwindcss": "0.6.14",
56
56
  "publint": "0.3.12",
57
57
  "runed": "0.31.0",
58
- "svelte": "5.36.1",
59
- "svelte-check": "4.2.2",
58
+ "svelte": "5.36.7",
59
+ "svelte-check": "4.3.0",
60
60
  "svelte-virtuallists": "1.4.2",
61
61
  "tailwindcss": "4.1.11",
62
62
  "three": "0.178.0",