@viamrobotics/motion-tools 0.6.3 → 0.8.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 (36) hide show
  1. package/dist/WorldObject.d.ts +2 -0
  2. package/dist/components/App.svelte +5 -9
  3. package/dist/components/CameraControls.svelte +10 -0
  4. package/dist/components/Details.svelte +4 -1
  5. package/dist/components/Details.svelte.d.ts +1 -1
  6. package/dist/components/DotSprite.svelte +44 -0
  7. package/dist/components/DotSprite.svelte.d.ts +9 -0
  8. package/dist/components/Geometry.svelte +1 -1
  9. package/dist/components/KeyboardControls.svelte +1 -1
  10. package/dist/components/Line.svelte +40 -0
  11. package/dist/components/Line.svelte.d.ts +7 -0
  12. package/dist/components/MeasureTool.svelte +131 -0
  13. package/dist/components/MeasureTool.svelte.d.ts +3 -0
  14. package/dist/components/Scene.svelte +2 -0
  15. package/dist/components/Shapes.svelte +5 -0
  16. package/dist/components/Tree/Settings.svelte +25 -0
  17. package/dist/components/Tree/TreeContainer.svelte +3 -0
  18. package/dist/components/Tree/TreeContainer.svelte.d.ts +1 -1
  19. package/dist/components/dashboard/Button.svelte +7 -2
  20. package/dist/components/dashboard/Button.svelte.d.ts +1 -1
  21. package/dist/components/dashboard/Dashboard.svelte +6 -1
  22. package/dist/components/dashboard/Dashboard.svelte.d.ts +4 -24
  23. package/dist/components/xr/XR.svelte +6 -4
  24. package/dist/components/xr/XR.svelte.d.ts +1 -1
  25. package/dist/hooks/useGeometries.svelte.js +3 -2
  26. package/dist/hooks/useObjectEvents.svelte.js +17 -3
  27. package/dist/hooks/useObjects.svelte.js +1 -0
  28. package/dist/hooks/useSettings.svelte.d.ts +3 -0
  29. package/dist/hooks/useSettings.svelte.js +4 -1
  30. package/dist/hooks/useShapes.svelte.d.ts +7 -0
  31. package/dist/hooks/useShapes.svelte.js +58 -0
  32. package/dist/portal.d.ts +2 -0
  33. package/dist/portal.js +9 -0
  34. package/package.json +2 -2
  35. package/dist/components/DomPortal.svelte +0 -28
  36. package/dist/components/DomPortal.svelte.d.ts +0 -8
@@ -17,6 +17,8 @@ export type Metadata = {
17
17
  };
18
18
  points?: Vector3[];
19
19
  pointSize?: number;
20
+ lineWidth?: number;
21
+ lineDotColor?: ColorRepresentation;
20
22
  batched?: {
21
23
  id: number;
22
24
  object: BatchedMesh;
@@ -5,11 +5,11 @@
5
5
  import TreeContainer from './Tree/TreeContainer.svelte'
6
6
  import Details from './Details.svelte'
7
7
  import SceneProviders from './SceneProviders.svelte'
8
- import DomPortal from './DomPortal.svelte'
9
8
  import XR from './xr/XR.svelte'
10
9
  import { World } from '@threlte/rapier'
11
10
  import { createPartIDContext } from '../hooks/usePartID.svelte'
12
11
  import Dashboard from './dashboard/Dashboard.svelte'
12
+ import { domPortal } from '../portal'
13
13
 
14
14
  interface Props {
15
15
  partID?: string
@@ -35,17 +35,13 @@
35
35
  {@render appChildren?.()}
36
36
  </Scene>
37
37
 
38
- <XR />
38
+ <XR {@attach domPortal(root)} />
39
39
 
40
- <DomPortal element={root}>
41
- <Dashboard />
42
- <Details />
43
- </DomPortal>
40
+ <Dashboard {@attach domPortal(root)} />
41
+ <Details {@attach domPortal(root)} />
44
42
 
45
43
  {#if !focus}
46
- <DomPortal element={root}>
47
- <TreeContainer />
48
- </DomPortal>
44
+ <TreeContainer {@attach domPortal(root)} />
49
45
  {/if}
50
46
  {/snippet}
51
47
  </SceneProviders>
@@ -4,10 +4,20 @@
4
4
  import KeyboardControls from './KeyboardControls.svelte'
5
5
  import Portal from './portal/Portal.svelte'
6
6
  import Button from './dashboard/Button.svelte'
7
+ import { useShapes } from '../hooks/useShapes.svelte'
7
8
 
9
+ const shapes = useShapes()
8
10
  const transformControls = useTransformControls()
9
11
 
10
12
  let ref = $state.raw<CameraControlsRef>()
13
+
14
+ $effect(() => {
15
+ if (shapes.camera) {
16
+ const { position, lookAt, animate } = shapes.camera
17
+ ref?.setPosition(position.x, position.y, position.z, animate)
18
+ ref?.setLookAt(position.x, position.y, position.z, lookAt.x, lookAt.y, lookAt.z, animate)
19
+ }
20
+ })
11
21
  </script>
12
22
 
13
23
  <Portal id="dashboard">
@@ -12,6 +12,7 @@
12
12
 
13
13
  <script lang="ts">
14
14
  import { Check, Copy } from 'lucide-svelte'
15
+ import { useTask } from '@threlte/core'
15
16
  import { Button, Icon } from '@viamrobotics/prime-core'
16
17
  import {
17
18
  useSelectedObject,
@@ -21,7 +22,8 @@
21
22
  useSelectedObject3d,
22
23
  } from '../hooks/useSelection.svelte'
23
24
  import { useDraggable } from '../hooks/useDraggable.svelte'
24
- import { useTask } from '@threlte/core'
25
+
26
+ const { ...rest } = $props()
25
27
 
26
28
  const focused = useFocused()
27
29
  const focusedObject = useFocusedObject()
@@ -75,6 +77,7 @@
75
77
  <div
76
78
  class="border-medium bg-extralight absolute top-0 right-0 z-10 m-2 w-60 border p-2 text-xs"
77
79
  style:transform="translate({draggable.current.x}px, {draggable.current.y}px)"
80
+ {...rest}
78
81
  >
79
82
  <div class="flex items-center justify-between gap-2 pb-2">
80
83
  <div class="flex items-center gap-1">
@@ -1,3 +1,3 @@
1
- declare const Details: import("svelte").Component<Record<string, never>, {}, "">;
1
+ declare const Details: import("svelte").Component<Record<string, any>, {}, "">;
2
2
  type Details = ReturnType<typeof Details>;
3
3
  export default Details;
@@ -0,0 +1,44 @@
1
+ <script
2
+ module
3
+ lang="ts"
4
+ >
5
+ import { T, type Props as ThrelteProps } from '@threlte/core'
6
+ import { CanvasTexture, type Sprite, type ColorRepresentation } from 'three'
7
+
8
+ const size = 128
9
+ const canvas = new OffscreenCanvas(size, size)
10
+ const ctx = canvas.getContext('2d')
11
+
12
+ if (ctx) {
13
+ ctx.clearRect(0, 0, size, size)
14
+ ctx.beginPath()
15
+ ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2)
16
+ ctx.fillStyle = 'white'
17
+ ctx.fill()
18
+ }
19
+
20
+ const map = new CanvasTexture(canvas)
21
+ </script>
22
+
23
+ <script lang="ts">
24
+ interface Props extends ThrelteProps<typeof Sprite> {
25
+ color?: ColorRepresentation
26
+ opacity?: number
27
+ }
28
+
29
+ let { color, opacity = 1, ref = $bindable(), ...rest }: Props = $props()
30
+ </script>
31
+
32
+ <T.Sprite
33
+ bind:ref
34
+ scale={0.05}
35
+ {...rest}
36
+ >
37
+ <T.SpriteMaterial
38
+ transparent
39
+ depthTest={false}
40
+ {map}
41
+ {opacity}
42
+ color={color ?? 'black'}
43
+ />
44
+ </T.Sprite>
@@ -0,0 +1,9 @@
1
+ import { type Props as ThrelteProps } from '@threlte/core';
2
+ import { type Sprite, type ColorRepresentation } from 'three';
3
+ interface Props extends ThrelteProps<typeof Sprite> {
4
+ color?: ColorRepresentation;
5
+ opacity?: number;
6
+ }
7
+ declare const DotSprite: import("svelte").Component<Props, {}, "ref">;
8
+ type DotSprite = ReturnType<typeof DotSprite>;
9
+ export default DotSprite;
@@ -101,7 +101,7 @@
101
101
  {#if geometry?.case === 'line'}
102
102
  <MeshLineMaterial
103
103
  {color}
104
- width={0.005}
104
+ width={metadata.lineWidth ?? 0.005}
105
105
  />
106
106
  {:else if geometry}
107
107
  <T.MeshToonMaterial
@@ -111,7 +111,7 @@
111
111
  settings.current.transformMode = 'scale'
112
112
  })
113
113
 
114
- keys.onKeys(['ctrl', 'x'], () => {
114
+ keys.onKeys('x', () => {
115
115
  settings.current.enableXR = !settings.current.enableXR
116
116
  })
117
117
  </script>
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { T } from '@threlte/core'
3
+ import { Instance, InstancedMesh } from '@threlte/extras'
4
+ import Frame from './Frame.svelte'
5
+ import type { WorldObject } from '../WorldObject'
6
+ import { useSettings } from '../hooks/useSettings.svelte'
7
+
8
+ interface Props {
9
+ object: WorldObject
10
+ }
11
+
12
+ let { object }: Props = $props()
13
+
14
+ const settings = useSettings()
15
+ $inspect(object.metadata.points)
16
+ </script>
17
+
18
+ <Frame
19
+ {...object}
20
+ metadata={{
21
+ ...object.metadata,
22
+ lineWidth: settings.current.lineWidth,
23
+ }}
24
+ />
25
+
26
+ {#if object.metadata.lineDotColor && object.metadata.points}
27
+ <InstancedMesh frustumCulled={false}>
28
+ <T.SphereGeometry />
29
+ <T.MeshBasicMaterial color={object.metadata.lineDotColor} />
30
+
31
+ {#each object.metadata.points as { x, y, z }, i (i)}
32
+ <Instance
33
+ position.x={x}
34
+ position.y={y}
35
+ position.z={z}
36
+ scale={Number(settings.current.lineDotSize)}
37
+ />
38
+ {/each}
39
+ </InstancedMesh>
40
+ {/if}
@@ -0,0 +1,7 @@
1
+ import type { WorldObject } from '../WorldObject';
2
+ interface Props {
3
+ object: WorldObject;
4
+ }
5
+ declare const Line: import("svelte").Component<Props, {}, "">;
6
+ type Line = ReturnType<typeof Line>;
7
+ export default Line;
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte'
3
+ import { Raycaster, Vector2, Vector3, type Intersection } from 'three'
4
+ import { T, useThrelte, useTask } from '@threlte/core'
5
+ import { HTML, MeshLineGeometry, MeshLineMaterial, useInteractivity } from '@threlte/extras'
6
+ import { useSettings } from '../hooks/useSettings.svelte'
7
+ import Button from './dashboard/Button.svelte'
8
+ import Portal from './portal/Portal.svelte'
9
+ import DotSprite from './DotSprite.svelte'
10
+
11
+ const settings = useSettings()
12
+ const { camera } = useThrelte()
13
+ const interactivity = useInteractivity()
14
+ const raycaster = new Raycaster()
15
+
16
+ const htmlPosition = new Vector3()
17
+ const pointerDown = new Vector2()
18
+ const pointerUp = new Vector2()
19
+
20
+ let step: 'idle' | 'p1' | 'p2' = 'idle'
21
+
22
+ let intersection: Intersection | undefined
23
+ let p1 = $state.raw<Vector3>()
24
+ let p2 = $state.raw<Vector3>()
25
+
26
+ const enabled = $derived(settings.current.enableMeasure)
27
+
28
+ const onpointerdown = (event: PointerEvent) => {
29
+ pointerDown.set(event.clientX, event.clientY)
30
+ }
31
+
32
+ const onpointerup = (event: PointerEvent) => {
33
+ pointerUp.set(event.clientX, event.clientY)
34
+
35
+ if (pointerDown.distanceToSquared(pointerUp) > 0.1) {
36
+ return
37
+ }
38
+
39
+ if (step === 'idle' && intersection) {
40
+ p1 = intersection.point.clone()
41
+ step = 'p1'
42
+ } else if (step === 'p1' && intersection) {
43
+ p2 = intersection.point.clone()
44
+ step = 'p2'
45
+ } else if (step === 'p2') {
46
+ p1 = undefined
47
+ p2 = undefined
48
+ step = 'idle'
49
+ }
50
+ }
51
+
52
+ const { start, stop } = useTask(
53
+ () => {
54
+ if (interactivity.hovered.size === 0) {
55
+ return
56
+ }
57
+
58
+ for (const [, event] of interactivity.hovered) {
59
+ raycaster.setFromCamera(interactivity.pointer.current, camera.current)
60
+ intersection = raycaster.intersectObject(event.object)[0]
61
+ }
62
+ },
63
+ { autoStart: false }
64
+ )
65
+
66
+ $effect(() => {
67
+ if (!enabled) {
68
+ untrack(() => {
69
+ p1 = undefined
70
+ p2 = undefined
71
+ step = 'idle'
72
+ })
73
+ }
74
+ })
75
+
76
+ $effect(() => {
77
+ if (enabled) {
78
+ start()
79
+ } else {
80
+ stop()
81
+ }
82
+ })
83
+ </script>
84
+
85
+ <Portal id="dashboard">
86
+ <fieldset>
87
+ <Button
88
+ active
89
+ icon="ruler"
90
+ class={enabled ? '' : 'text-gray-4!'}
91
+ description="{enabled ? 'Disable' : 'Enable'} measurement"
92
+ onclick={() => {
93
+ settings.current.enableMeasure = !settings.current.enableMeasure
94
+ }}
95
+ />
96
+ </fieldset>
97
+ </Portal>
98
+
99
+ <svelte:window
100
+ onpointerdown={enabled ? onpointerdown : undefined}
101
+ onpointerup={enabled ? onpointerup : undefined}
102
+ />
103
+
104
+ {#if enabled}
105
+ {#if p1}
106
+ <DotSprite position={p1.toArray()} />
107
+ {/if}
108
+
109
+ {#if p2}
110
+ <DotSprite position={p2.toArray()} />
111
+ {/if}
112
+
113
+ {#if p1 && p2}
114
+ <T.Mesh>
115
+ <MeshLineGeometry points={[p1, p2]} />
116
+ <MeshLineMaterial
117
+ width={0.015}
118
+ depthTest={false}
119
+ color="black"
120
+ />
121
+ </T.Mesh>
122
+ <HTML
123
+ center
124
+ position={htmlPosition.lerpVectors(p1, p2, 0.5).toArray()}
125
+ >
126
+ <div class="border border-black bg-white px-1 py-0.5 text-xs">
127
+ {p1.distanceTo(p2).toFixed(2)}m
128
+ </div>
129
+ </HTML>
130
+ {/if}
131
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const MeasureTool: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type MeasureTool = ReturnType<typeof MeasureTool>;
3
+ export default MeasureTool;
@@ -17,6 +17,7 @@
17
17
  import { useOrigin } from './xr/useOrigin.svelte'
18
18
  import { useSettings } from '../hooks/useSettings.svelte'
19
19
  import CameraControls from './CameraControls.svelte'
20
+ import MeasureTool from './MeasureTool.svelte'
20
21
 
21
22
  interface Props {
22
23
  children?: Snippet
@@ -63,6 +64,7 @@
63
64
 
64
65
  <PortalTarget id="world" />
65
66
 
67
+ <MeasureTool />
66
68
  <StaticGeometries />
67
69
  <Frames />
68
70
  <Pointclouds />
@@ -4,6 +4,7 @@
4
4
  import { useShapes } from '../hooks/useShapes.svelte'
5
5
  import WorldObject from './WorldObject.svelte'
6
6
  import Frame from './Frame.svelte'
7
+ import Line from './Line.svelte'
7
8
 
8
9
  const shapes = useShapes()
9
10
  </script>
@@ -47,3 +48,7 @@
47
48
  <PortalTarget id={object.name} />
48
49
  </WorldObject>
49
50
  {/each}
51
+
52
+ {#each shapes.lines as object (object.uuid)}
53
+ <Line {object} />
54
+ {/each}
@@ -108,6 +108,31 @@
108
108
  </label>
109
109
  </div>
110
110
 
111
+ <h3 class="text-base"><strong>Lines</strong></h3>
112
+ <div class="flex flex-col gap-2.5">
113
+ <label class="flex items-center justify-between gap-2">
114
+ Thickness
115
+
116
+ <div class="w-20">
117
+ <Input
118
+ bind:value={settings.current.lineWidth}
119
+ on:keydown={(event) => event.stopImmediatePropagation()}
120
+ />
121
+ </div>
122
+ </label>
123
+
124
+ <label class="flex items-center justify-between gap-2">
125
+ Dot size
126
+
127
+ <div class="w-20">
128
+ <Input
129
+ bind:value={settings.current.lineDotSize}
130
+ on:keydown={(event) => event.stopImmediatePropagation()}
131
+ />
132
+ </div>
133
+ </label>
134
+ </div>
135
+
111
136
  <h3 class="text-base"><strong>Misc</strong></h3>
112
137
  <div class="flex flex-col gap-2.5">
113
138
  <label class="flex items-center justify-between gap-2">
@@ -10,6 +10,8 @@
10
10
  import Logs from './Logs.svelte'
11
11
  import { useDraggable } from '../../hooks/useDraggable.svelte'
12
12
 
13
+ const { ...rest } = $props()
14
+
13
15
  provideTreeExpandedContext()
14
16
 
15
17
  const selected = useSelected()
@@ -35,6 +37,7 @@
35
37
  <div
36
38
  class="bg-extralight border-medium absolute top-0 left-0 m-2 overflow-y-auto border text-xs"
37
39
  style:transform="translate({draggable.current.x}px, {draggable.current.y}px)"
40
+ {...rest}
38
41
  >
39
42
  {#key rootNode}
40
43
  <Tree
@@ -1,3 +1,3 @@
1
- declare const TreeContainer: import("svelte").Component<Record<string, never>, {}, "">;
1
+ declare const TreeContainer: import("svelte").Component<Record<string, any>, {}, "">;
2
2
  type TreeContainer = ReturnType<typeof TreeContainer>;
3
3
  export default TreeContainer;
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { Icon, type IconName, Tooltip } from '@viamrobotics/prime-core'
3
+ import { Ruler } from 'lucide-svelte'
3
4
 
4
5
  interface Props {
5
- icon: IconName
6
+ icon: IconName | 'ruler'
6
7
  active?: boolean
7
8
  description: string
8
9
  hotkey?: string
@@ -39,7 +40,11 @@
39
40
  aria-checked={active}
40
41
  {onclick}
41
42
  >
42
- <Icon name={icon} />
43
+ {#if icon === 'ruler'}
44
+ <Ruler size="16" />
45
+ {:else}
46
+ <Icon name={icon} />
47
+ {/if}
43
48
  </button>
44
49
  </label>
45
50
  <p slot="description">
@@ -1,6 +1,6 @@
1
1
  import { type IconName } from '@viamrobotics/prime-core';
2
2
  interface Props {
3
- icon: IconName;
3
+ icon: IconName | 'ruler';
4
4
  active?: boolean;
5
5
  description: string;
6
6
  hotkey?: string;
@@ -3,10 +3,15 @@
3
3
  import PortalTarget from '../portal/PortalTarget.svelte'
4
4
  import Button from './Button.svelte'
5
5
 
6
+ let { ...rest } = $props()
7
+
6
8
  const settings = useSettings()
7
9
  </script>
8
10
 
9
- <div class="absolute top-2 flex w-full justify-center gap-2">
11
+ <div
12
+ class="absolute top-2 flex w-full justify-center gap-2"
13
+ {...rest}
14
+ >
10
15
  <!-- camera view -->
11
16
  <fieldset class="flex">
12
17
  <Button
@@ -1,26 +1,6 @@
1
1
  export default Dashboard;
2
- type Dashboard = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
2
+ type Dashboard = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Record<string, any>>): void;
8
5
  };
9
- declare const Dashboard: $$__sveltets_2_IsomorphicComponent<{
10
- [x: string]: never;
11
- }, {
12
- [evt: string]: CustomEvent<any>;
13
- }, {}, {}, string>;
14
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
- new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
- $$bindings?: Bindings;
17
- } & Exports;
18
- (internal: unknown, props: {
19
- $$events?: Events;
20
- $$slots?: Slots;
21
- }): Exports & {
22
- $set?: any;
23
- $on?: any;
24
- };
25
- z_$$bindings?: Bindings;
26
- }
6
+ declare const Dashboard: import("svelte").Component<Record<string, any>, {}, "">;
@@ -2,10 +2,11 @@
2
2
  import { T } from '@threlte/core'
3
3
  import { useXR, XR, XRButton } from '@threlte/xr'
4
4
  import OriginMarker from './OriginMarker.svelte'
5
- import DomPortal from '../DomPortal.svelte'
6
5
  import { useSettings } from '../../hooks/useSettings.svelte'
7
6
  import Controllers from './Controllers.svelte'
8
7
 
8
+ const { ...rest } = $props()
9
+
9
10
  const { isPresenting } = useXR()
10
11
  const settings = useSettings()
11
12
  const enableXR = $derived(settings.current.enableXR)
@@ -20,7 +21,8 @@
20
21
  <Controllers />
21
22
  </XR>
22
23
 
23
- <DomPortal>
24
- <XRButton mode="immersive-ar" />
25
- </DomPortal>
24
+ <XRButton
25
+ mode="immersive-ar"
26
+ {...rest}
27
+ />
26
28
  {/if}
@@ -1,3 +1,3 @@
1
- declare const Xr: import("svelte").Component<Record<string, never>, {}, "">;
1
+ declare const Xr: import("svelte").Component<Record<string, any>, {}, "">;
2
2
  type Xr = ReturnType<typeof Xr>;
3
3
  export default Xr;
@@ -48,9 +48,10 @@ export const provideGeometries = (partID) => {
48
48
  continue;
49
49
  for (const { center, label, geometryType } of query.data.geometries) {
50
50
  const resourceName = resourceNames.current.find((item) => item.name === query.data.name);
51
- results.push(new WorldObject(label ? label : 'Unnamed geometry', center, query.data.name, geometryType, resourceName
51
+ const worldObject = new WorldObject(label ? label : 'Unnamed geometry', center, query.data.name, geometryType, resourceName
52
52
  ? { color: resourceColors[resourceName.subtype] }
53
- : undefined));
53
+ : undefined);
54
+ results.push(worldObject);
54
55
  }
55
56
  }
56
57
  updateUUIDs(results);
@@ -2,26 +2,34 @@ import { useCursor } from '@threlte/extras';
2
2
  import { useFocused, useSelected } from './useSelection.svelte';
3
3
  import { useVisibility } from './useVisibility.svelte';
4
4
  import { Vector2 } from 'three';
5
+ import { useSettings } from './useSettings.svelte';
5
6
  export const useObjectEvents = (uuid) => {
6
- const { onPointerEnter, onPointerLeave } = useCursor();
7
+ const settings = useSettings();
7
8
  const selected = useSelected();
8
9
  const focused = useFocused();
9
10
  const visibility = useVisibility();
10
11
  const down = new Vector2();
12
+ const measureCursor = useCursor('crosshair');
13
+ const hoverCursor = useCursor();
14
+ const measuring = $derived(settings.current.enableMeasure);
15
+ const cursor = $derived(measuring ? measureCursor : hoverCursor);
11
16
  return {
12
17
  get visible() {
13
18
  return visibility.get(uuid());
14
19
  },
15
20
  onpointerenter: (event) => {
16
21
  event.stopPropagation();
17
- onPointerEnter();
22
+ cursor.onPointerEnter();
18
23
  },
19
24
  onpointerleave: (event) => {
20
25
  event.stopPropagation();
21
- onPointerLeave();
26
+ cursor.onPointerLeave();
22
27
  },
23
28
  ondblclick: (event) => {
24
29
  event.stopPropagation();
30
+ if (measuring) {
31
+ return;
32
+ }
25
33
  focused.set(uuid());
26
34
  },
27
35
  onpointerdown: (event) => {
@@ -29,11 +37,17 @@ export const useObjectEvents = (uuid) => {
29
37
  },
30
38
  onclick: (event) => {
31
39
  event.stopPropagation();
40
+ if (measuring) {
41
+ return;
42
+ }
32
43
  if (down.distanceToSquared(event.pointer) < 0.1) {
33
44
  selected.set(uuid());
34
45
  }
35
46
  },
36
47
  onpointermissed: () => {
48
+ if (measuring) {
49
+ return;
50
+ }
37
51
  selected.set();
38
52
  },
39
53
  };
@@ -19,6 +19,7 @@ export const provideObjects = () => {
19
19
  ...shapes.models,
20
20
  ...shapes.nurbs,
21
21
  ...shapes.points,
22
+ ...shapes.lines,
22
23
  ...statics.current,
23
24
  ...shapes.poses,
24
25
  ]);
@@ -9,7 +9,10 @@ interface Settings {
9
9
  gridFadeDistance: number;
10
10
  pointSize: number;
11
11
  pointColor: string;
12
+ lineWidth: number;
13
+ lineDotSize: number;
12
14
  enableXR: boolean;
15
+ enableMeasure: boolean;
13
16
  renderStats: boolean;
14
17
  }
15
18
  interface Context {
@@ -11,7 +11,10 @@ const defaults = () => ({
11
11
  gridSectionSize: 10,
12
12
  gridFadeDistance: 25,
13
13
  pointSize: 0.01,
14
- pointColor: '#333',
14
+ pointColor: '#333333',
15
+ lineWidth: 0.005,
16
+ lineDotSize: 0.01,
17
+ enableMeasure: false,
15
18
  enableXR: false,
16
19
  renderStats: false,
17
20
  });
@@ -1,8 +1,10 @@
1
+ import { Vector3 } from 'three';
1
2
  import { BatchedArrow } from '../three/BatchedArrow';
2
3
  import { WorldObject, type PointsGeometry } from '../WorldObject';
3
4
  type ConnectionStatus = 'connecting' | 'open' | 'closed';
4
5
  interface Context {
5
6
  points: WorldObject<PointsGeometry>[];
7
+ lines: WorldObject[];
6
8
  meshes: WorldObject[];
7
9
  poses: WorldObject[];
8
10
  nurbs: WorldObject[];
@@ -11,6 +13,11 @@ interface Context {
11
13
  object3ds: {
12
14
  batchedArrow: BatchedArrow;
13
15
  };
16
+ camera: {
17
+ position: Vector3;
18
+ lookAt: Vector3;
19
+ animate: boolean;
20
+ } | undefined;
14
21
  }
15
22
  export declare const provideShapes: () => void;
16
23
  export declare const useShapes: () => Context;
@@ -39,10 +39,12 @@ export const provideShapes = () => {
39
39
  const maxReconnectDelay = 5_000;
40
40
  let ws;
41
41
  const points = $state([]);
42
+ const lines = $state([]);
42
43
  const meshes = $state([]);
43
44
  const poses = $state([]);
44
45
  const nurbs = $state([]);
45
46
  const models = $state([]);
47
+ let camera = $state();
46
48
  let connectionStatus = $state('connecting');
47
49
  const color = new Color();
48
50
  const direction = new Vector3();
@@ -177,6 +179,40 @@ export const provideShapes = () => {
177
179
  value: positions,
178
180
  }, metadata));
179
181
  };
182
+ const addLine = async (reader) => {
183
+ // Read label length
184
+ const labelLen = reader.read();
185
+ let label = '';
186
+ for (let i = 0; i < labelLen; i++) {
187
+ label += String.fromCharCode(reader.read());
188
+ }
189
+ // Read counts
190
+ const nPoints = reader.read();
191
+ // Read default color
192
+ const lineR = reader.read();
193
+ const lineG = reader.read();
194
+ const lineB = reader.read();
195
+ const dotR = reader.read();
196
+ const dotG = reader.read();
197
+ const dotB = reader.read();
198
+ // Read positions
199
+ const positions = new Float32Array(nPoints * 3);
200
+ for (let i = 0; i < nPoints * 3; i++) {
201
+ positions[i] = reader.read();
202
+ }
203
+ const points = [];
204
+ for (let i = 0; i < positions.length; i += 3) {
205
+ points.push(new Vector3(positions[i], positions[i + 1], positions[i + 2]));
206
+ }
207
+ lines.push(new WorldObject(label, undefined, undefined, {
208
+ case: 'line',
209
+ value: positions,
210
+ }, {
211
+ points,
212
+ color: lineR === -1 ? undefined : new Color().setRGB(lineR, lineG, lineB),
213
+ lineDotColor: dotR === -1 ? undefined : new Color().setRGB(dotR, dotG, dotB),
214
+ }));
215
+ };
180
216
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
217
  const addGeometries = (geometries, colors, parent) => {
182
218
  let i = 0;
@@ -224,10 +260,16 @@ export const provideShapes = () => {
224
260
  models.splice(index, 1);
225
261
  continue;
226
262
  }
263
+ index = lines.findIndex((m) => m.name === name);
264
+ if (index !== -1) {
265
+ lines.splice(index, 1);
266
+ continue;
267
+ }
227
268
  }
228
269
  };
229
270
  const removeAll = () => {
230
271
  points.splice(0, points.length);
272
+ lines.splice(0, lines.length);
231
273
  meshes.splice(0, meshes.length);
232
274
  nurbs.splice(0, nurbs.length);
233
275
  models.splice(0, models.length);
@@ -270,6 +312,9 @@ export const provideShapes = () => {
270
312
  return addPoses(reader);
271
313
  }
272
314
  else if (type === 2) {
315
+ return addLine(reader);
316
+ }
317
+ else if (type === 3) {
273
318
  return addPCD(reader.buffer);
274
319
  }
275
320
  else {
@@ -279,6 +324,13 @@ export const provideShapes = () => {
279
324
  const data = tryParse(event.data);
280
325
  if (!data)
281
326
  return;
327
+ if ('setCameraPose' in data) {
328
+ camera = {
329
+ position: new Vector3(data.Position.X, data.Position.Y, data.Position.Z),
330
+ lookAt: new Vector3(data.LookAt.X, data.LookAt.Y, data.LookAt.Z),
331
+ animate: data.Animate,
332
+ };
333
+ }
282
334
  if ('geometries' in data) {
283
335
  return addGeometries(data.geometries, data.colors, data.parent);
284
336
  }
@@ -310,6 +362,9 @@ export const provideShapes = () => {
310
362
  get points() {
311
363
  return points;
312
364
  },
365
+ get lines() {
366
+ return lines;
367
+ },
313
368
  get meshes() {
314
369
  return meshes;
315
370
  },
@@ -328,6 +383,9 @@ export const provideShapes = () => {
328
383
  object3ds: {
329
384
  batchedArrow,
330
385
  },
386
+ get camera() {
387
+ return camera;
388
+ },
331
389
  });
332
390
  };
333
391
  export const useShapes = () => {
@@ -0,0 +1,2 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ export declare const domPortal: (target?: HTMLElement) => Attachment;
package/dist/portal.js ADDED
@@ -0,0 +1,9 @@
1
+ export const domPortal = (target) => {
2
+ return (element) => {
3
+ const finalTarget = target ?? document.body;
4
+ finalTarget.append(element);
5
+ return () => {
6
+ element.remove();
7
+ };
8
+ };
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "@typescript-eslint/parser": "8.38.0",
38
38
  "@viamrobotics/prime-core": "0.1.5",
39
39
  "@viamrobotics/sdk": "0.46.0",
40
- "@viamrobotics/svelte-sdk": "0.4.4",
40
+ "@viamrobotics/svelte-sdk": "0.4.3",
41
41
  "@vitejs/plugin-basic-ssl": "2.1.0",
42
42
  "@zag-js/svelte": "1.19.0",
43
43
  "@zag-js/tree-view": "1.19.0",
@@ -1,28 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte'
3
-
4
- interface Props {
5
- element?: HTMLElement
6
- children: Snippet
7
- }
8
-
9
- let { children, element }: Props = $props()
10
-
11
- let div: HTMLDivElement
12
-
13
- $effect(() => {
14
- const parent = element ?? document.body
15
- parent.append(div)
16
- return () => {
17
- // eslint-disable-next-line svelte/no-dom-manipulating
18
- div.remove()
19
- }
20
- })
21
- </script>
22
-
23
- <div
24
- style="display: contents"
25
- bind:this={div}
26
- >
27
- {@render children()}
28
- </div>
@@ -1,8 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- interface Props {
3
- element?: HTMLElement;
4
- children: Snippet;
5
- }
6
- declare const DomPortal: import("svelte").Component<Props, {}, "">;
7
- type DomPortal = ReturnType<typeof DomPortal>;
8
- export default DomPortal;