@viamrobotics/motion-tools 1.5.0 → 1.9.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 (35) hide show
  1. package/README.md +16 -9
  2. package/dist/components/App.svelte +17 -0
  3. package/dist/components/Frame.svelte +0 -7
  4. package/dist/components/HoveredEntities.svelte +19 -0
  5. package/dist/components/HoveredEntities.svelte.d.ts +3 -0
  6. package/dist/components/HoveredEntityTooltip.svelte +241 -0
  7. package/dist/components/HoveredEntityTooltip.svelte.d.ts +7 -0
  8. package/dist/components/MeasureTool/MeasurePoint.svelte +47 -0
  9. package/dist/components/MeasureTool/MeasurePoint.svelte.d.ts +8 -0
  10. package/dist/components/MeasureTool/MeasureTool.svelte +176 -0
  11. package/dist/components/MeasureTool/MeasureTool.svelte.d.ts +3 -0
  12. package/dist/components/Overlay/Popover.svelte +28 -0
  13. package/dist/components/Overlay/Popover.svelte.d.ts +9 -0
  14. package/dist/components/Overlay/ToggleGroup.svelte +60 -0
  15. package/dist/components/Overlay/ToggleGroup.svelte.d.ts +13 -0
  16. package/dist/components/Scene.svelte +1 -1
  17. package/dist/components/Tree/Settings.svelte +23 -22
  18. package/dist/components/Tree/Widgets.svelte +44 -0
  19. package/dist/components/Tree/Widgets.svelte.d.ts +2 -17
  20. package/dist/components/dashboard/Button.svelte +7 -3
  21. package/dist/components/dashboard/Button.svelte.d.ts +3 -2
  22. package/dist/components/widgets/Camera.svelte +195 -0
  23. package/dist/components/widgets/Camera.svelte.d.ts +6 -0
  24. package/dist/ecs/traits.d.ts +18 -12
  25. package/dist/ecs/traits.js +17 -11
  26. package/dist/ecs/useQuery.svelte.js +10 -10
  27. package/dist/hooks/use3DModels.svelte.js +1 -3
  28. package/dist/hooks/useObjectEvents.svelte.d.ts +1 -0
  29. package/dist/hooks/useObjectEvents.svelte.js +24 -0
  30. package/dist/hooks/useSettings.svelte.d.ts +5 -0
  31. package/dist/hooks/useSettings.svelte.js +5 -0
  32. package/dist/hooks/useWeblabs.svelte.d.ts +1 -3
  33. package/dist/hooks/useWeblabs.svelte.js +1 -3
  34. package/dist/three/InstancedArrows/raycast.js +2 -6
  35. package/package.json +6 -2
package/README.md CHANGED
@@ -17,21 +17,28 @@ make setup
17
17
 
18
18
  This single command will:
19
19
 
20
- 1. Install and configure **nvm** (Node Version Manager)
21
- 2. Install the latest **Node.js LTS** version via nvm
22
- 3. Install **pnpm** package manager
23
- 4. Install **bun** runtime
20
+ 1. Install **fnm** (Fast Node Manager) and **Node.js 22**
21
+ 2. Install **pnpm** package manager
22
+ 3. Install **bun** runtime
23
+ 4. Install **Go** and **buf** (for protobuf generation)
24
24
  5. Install all project dependencies
25
+ 6. Generate protobuf code
26
+
27
+ After setup completes, add the shell configuration it prints to your shell config file (`~/.zshrc` or `~/.bashrc`), then restart your terminal.
25
28
 
26
29
  #### Manual setup
27
30
 
28
31
  If the above does not work for you, or if you prefer to install dependencies manually:
29
32
 
30
- 1. [Install nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
31
- 2. Install Node.js LTS: `nvm install --lts && nvm use --lts`
32
- 3. [Install pnpm](https://pnpm.io/installation)
33
- 4. [Install bun](https://bun.sh/docs/installation)
34
- 5. Install dependencies: `pnpm i`
33
+ 1. [Install fnm](https://github.com/Schniz/fnm#installation): `curl -fsSL https://fnm.vercel.app/install | bash`
34
+ 2. Install Node.js: `fnm install 22 && fnm use 22`
35
+ 3. [Install pnpm](https://pnpm.io/installation): `curl -fsSL https://get.pnpm.io/install.sh | sh -`
36
+ 4. [Install bun](https://bun.sh/docs/installation): `curl -fsSL https://bun.sh/install | bash`
37
+ 5. [Install Go](https://go.dev/doc/install)
38
+ 6. [Install buf](https://buf.build/docs/installation): download from GitHub releases
39
+ 7. Install Go tools: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`
40
+ 8. Install dependencies: `pnpm install`
41
+ 9. Generate protobufs: `make proto`
35
42
 
36
43
  ### Env files for machine configs
37
44
 
@@ -26,6 +26,8 @@
26
26
  provideDrawConnectionConfig,
27
27
  type DrawConnectionConfig,
28
28
  } from '../hooks/useDrawConnectionConfig.svelte'
29
+ import Camera from './widgets/Camera.svelte'
30
+ import HoveredEntities from './HoveredEntities.svelte'
29
31
 
30
32
  interface LocalConfigProps {
31
33
  getLocalPartConfig: () => Struct
@@ -64,6 +66,8 @@
64
66
  const settings = provideSettings()
65
67
  const environment = provideEnvironment()
66
68
 
69
+ const currentRobotCameraWidgets = $derived(settings.current.openCameraWidgets[partID] || [])
70
+
67
71
  $effect(() => {
68
72
  settings.current.enableKeybindings = enableKeybindings
69
73
  })
@@ -125,6 +129,10 @@
125
129
  {@attach domPortal(root)}
126
130
  {dashboard}
127
131
  />
132
+
133
+ {#if settings.current.renderSubEntityHoverDetail}
134
+ <HoveredEntities {@attach domPortal(root)} />
135
+ {/if}
128
136
  <Details {@attach domPortal(root)} />
129
137
  {#if environment.current.isStandalone}
130
138
  <LiveUpdatesBanner {@attach domPortal(root)} />
@@ -138,6 +146,15 @@
138
146
  <ArmPositions {@attach domPortal(root)} />
139
147
  {/if}
140
148
 
149
+ {#if !focus}
150
+ {#each currentRobotCameraWidgets as cameraName (cameraName)}
151
+ <Camera
152
+ name={cameraName}
153
+ {@attach domPortal(root)}
154
+ />
155
+ {/each}
156
+ {/if}
157
+
141
158
  <FileDrop {@attach domPortal(root)} />
142
159
  {/snippet}
143
160
  </SceneProviders>
@@ -3,12 +3,10 @@
3
3
  import { useObjectEvents } from '../hooks/useObjectEvents.svelte'
4
4
  import { Color, Group, type Object3D } from 'three'
5
5
  import Geometry from './Geometry2.svelte'
6
- import { useWeblabs } from '../hooks/useWeblabs.svelte'
7
6
  import { useSelectedEntity } from '../hooks/useSelection.svelte'
8
7
  import { useSettings } from '../hooks/useSettings.svelte'
9
8
  import { use3DModels } from '../hooks/use3DModels.svelte'
10
9
  import { colors, darkenColor, resourceColors } from '../color'
11
- import { WEBLABS_EXPERIMENTS } from '../hooks/useWeblabs.svelte'
12
10
  import type { Entity } from 'koota'
13
11
  import { traits, useTrait } from '../ecs'
14
12
  import type { Pose } from '@viamrobotics/sdk'
@@ -31,7 +29,6 @@
31
29
  const componentModels = use3DModels()
32
30
  const selectedEntity = useSelectedEntity()
33
31
  const resourceByName = useResourceByName()
34
- const weblabs = useWeblabs()
35
32
 
36
33
  const name = useTrait(() => entity, traits.Name)
37
34
  const parent = useTrait(() => entity, traits.Parent)
@@ -57,10 +54,6 @@
57
54
  })
58
55
 
59
56
  const model = $derived.by(() => {
60
- if (!weblabs.isActive(WEBLABS_EXPERIMENTS.MOTION_TOOLS_RENDER_ARM_MODELS)) {
61
- return
62
- }
63
-
64
57
  if (!name.current) {
65
58
  return
66
59
  }
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import { useQuery } from '../ecs'
3
+ import { traits } from '../ecs'
4
+ import HoveredEntityTooltip from './HoveredEntityTooltip.svelte'
5
+ import { useSelectedEntity } from '../hooks/useSelection.svelte'
6
+ import { useFocusedEntity } from '../hooks/useSelection.svelte'
7
+
8
+ const hoveredEntities = useQuery(traits.Hover)
9
+ const selectedEntity = useSelectedEntity()
10
+ const focusedEntity = useFocusedEntity()
11
+
12
+ const displayEntity = $derived(selectedEntity.current ?? focusedEntity.current) // for now, only display hover tooltip if the entity is selected or focused
13
+ </script>
14
+
15
+ {#each hoveredEntities.current as entity (entity)}
16
+ {#if entity === displayEntity}
17
+ <HoveredEntityTooltip hoveredEntity={entity} />
18
+ {/if}
19
+ {/each}
@@ -0,0 +1,3 @@
1
+ declare const HoveredEntities: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type HoveredEntities = ReturnType<typeof HoveredEntities>;
3
+ export default HoveredEntities;
@@ -0,0 +1,241 @@
1
+ <script
2
+ module
3
+ lang="ts"
4
+ >
5
+ import { Vector3 } from 'three'
6
+
7
+ interface ClosestArrow {
8
+ index: number
9
+ x: number
10
+ y: number
11
+ z: number
12
+ oX: number
13
+ oY: number
14
+ oZ: number
15
+ }
16
+
17
+ interface ClosestPoint {
18
+ index: number
19
+ x: number
20
+ y: number
21
+ z: number
22
+ }
23
+
24
+ const getClosestArrow = (positions: Float32Array, point: Vector3): ClosestArrow => {
25
+ let smallestDistance = Infinity
26
+ let index = -1
27
+
28
+ for (let i = 0; i < positions.length; i += 6) {
29
+ const x = positions[i] / 1000
30
+ const y = positions[i + 1] / 1000
31
+ const z = positions[i + 2] / 1000
32
+
33
+ const distance = point.distanceToSquared(new Vector3(x, y, z))
34
+
35
+ if (distance < smallestDistance) {
36
+ smallestDistance = distance
37
+ index = i
38
+ }
39
+ }
40
+
41
+ return {
42
+ index: Math.floor(index / 6),
43
+ x: positions[index] / 1000,
44
+ y: positions[index + 1] / 1000,
45
+ z: positions[index + 2] / 1000,
46
+ oX: positions[index + 3],
47
+ oY: positions[index + 4],
48
+ oZ: positions[index + 5],
49
+ }
50
+ }
51
+
52
+ const getClosestPoint = (positions: Float32Array, point: Vector3): ClosestPoint => {
53
+ let smallestDistance = Infinity
54
+ let index = -1
55
+
56
+ for (let i = 0; i < positions.length; i += 3) {
57
+ const x = positions[i]
58
+ const y = positions[i + 1]
59
+ const z = positions[i + 2]
60
+
61
+ const distance = point.distanceToSquared(new Vector3(x, y, z))
62
+
63
+ if (distance < smallestDistance) {
64
+ smallestDistance = distance
65
+ index = i
66
+ }
67
+ }
68
+
69
+ return {
70
+ index: Math.floor(index / 3),
71
+ x: positions[index],
72
+ y: positions[index + 1],
73
+ z: positions[index + 2],
74
+ }
75
+ }
76
+
77
+ const getPointAtIndex = (positions: Float32Array, index: number): ClosestPoint => ({
78
+ index,
79
+ x: positions[index * 3],
80
+ y: positions[index * 3 + 1],
81
+ z: positions[index * 3 + 2],
82
+ })
83
+ </script>
84
+
85
+ <script lang="ts">
86
+ import { traits } from '../ecs'
87
+ import { HTML } from '@threlte/extras'
88
+ import type { Entity } from 'koota'
89
+ import { useWorld } from '../ecs'
90
+ import { onDestroy } from 'svelte'
91
+
92
+ interface Props {
93
+ hoveredEntity: Entity
94
+ }
95
+
96
+ let { hoveredEntity }: Props = $props()
97
+
98
+ const world = useWorld()
99
+
100
+ let tooltipData: {
101
+ subEntityPosition: Vector3 | undefined
102
+ closestArrow?: ClosestArrow
103
+ closestPoint?: ClosestPoint
104
+ } | null = $state.raw(null)
105
+
106
+ const getTooltipData = (entity: Entity) => {
107
+ if (entity !== hoveredEntity) {
108
+ return null
109
+ }
110
+
111
+ const hover = entity.get(traits.Hover)
112
+ if (!hover) return null
113
+
114
+ const hoverPosition = new Vector3(hover.x, hover.y, hover.z)
115
+ const index = hover.index >= 0 ? hover.index : undefined
116
+
117
+ let closestArrow: ClosestArrow | undefined
118
+ let closestPoint: ClosestPoint | undefined
119
+ let subEntityPosition: Vector3 | undefined
120
+
121
+ if (entity.has(traits.Arrows)) {
122
+ // TODO: maybe we could store the arrows in a buffered geometry to avoid the slow getClosestArrow
123
+ closestArrow = getClosestArrow(entity.get(traits.Positions) as Float32Array, hoverPosition)
124
+ subEntityPosition = new Vector3(closestArrow.x, closestArrow.y, closestArrow.z)
125
+ } else if (entity.has(traits.Points)) {
126
+ const positions = entity.get(traits.BufferGeometry)?.attributes.position.array as Float32Array
127
+
128
+ // we can skip the slow getClosestPoint if the points provided an index already
129
+ if (index !== undefined) {
130
+ closestPoint = getPointAtIndex(positions, index)
131
+ } else {
132
+ closestPoint = getClosestPoint(positions, hoverPosition)
133
+ }
134
+ subEntityPosition = new Vector3(closestPoint.x, closestPoint.y, closestPoint.z)
135
+ }
136
+ return { subEntityPosition, closestArrow, closestPoint }
137
+ }
138
+
139
+ const unsubChange = world.onChange(traits.Hover, (entity) => {
140
+ if (entity === hoveredEntity) {
141
+ tooltipData = getTooltipData(entity)
142
+ }
143
+ })
144
+
145
+ const unsubRemove = world.onRemove(traits.Hover, (entity) => {
146
+ if (entity === hoveredEntity) {
147
+ tooltipData = null
148
+ }
149
+ })
150
+
151
+ onDestroy(() => {
152
+ unsubChange()
153
+ unsubRemove()
154
+ })
155
+ </script>
156
+
157
+ {#if tooltipData?.subEntityPosition}
158
+ <HTML
159
+ position={tooltipData.subEntityPosition.toArray()}
160
+ class="pointer-events-none"
161
+ >
162
+ <div
163
+ class="border-medium pointer-events-none relative -mb-2 -translate-x-1/2 -translate-y-full border bg-white px-3 py-2.5 text-xs shadow-md"
164
+ >
165
+ <!-- Arrow -->
166
+ <div
167
+ class="border-medium absolute -bottom-[5px] left-1/2 size-2.5 -translate-x-1/2 rotate-45 border-r border-b bg-white"
168
+ ></div>
169
+
170
+ <div class="flex flex-col gap-2.5">
171
+ {#if tooltipData.closestArrow}
172
+ <div>
173
+ <div class="mb-1"><strong class="font-semibold">index</strong></div>
174
+ <div>{tooltipData.closestArrow.index}</div>
175
+ </div>
176
+
177
+ <div>
178
+ <div class="mb-1">
179
+ <strong class="font-semibold">world position</strong>
180
+ <span class="text-subtle-2"> (m)</span>
181
+ </div>
182
+ <div class="flex gap-3">
183
+ <div>
184
+ <span class="text-subtle-2 mr-1">x </span>{tooltipData.closestArrow.x.toFixed(2)}
185
+ </div>
186
+ <div>
187
+ <span class="text-subtle-2 mr-1">y </span>{tooltipData.closestArrow.y.toFixed(2)}
188
+ </div>
189
+ <div>
190
+ <span class="text-subtle-2 mr-1">z </span>{tooltipData.closestArrow.z.toFixed(2)}
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <div>
196
+ <div class="mb-1">
197
+ <strong class="font-semibold">world orientation</strong>
198
+ <span class="text-subtle-2"> (deg)</span>
199
+ </div>
200
+ <div class="flex gap-3">
201
+ <div>
202
+ <span class="text-subtle-2 mr-1">x </span>{tooltipData.closestArrow.oX.toFixed(2)}
203
+ </div>
204
+ <div>
205
+ <span class="text-subtle-2 mr-1">y </span>{tooltipData.closestArrow.oY.toFixed(2)}
206
+ </div>
207
+ <div>
208
+ <span class="text-subtle-2 mr-1">z </span>{tooltipData.closestArrow.oZ.toFixed(2)}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ {/if}
213
+
214
+ {#if tooltipData.closestPoint}
215
+ <div>
216
+ <div class="mb-1"><strong class="font-semibold">index</strong></div>
217
+ <div>{tooltipData.closestPoint.index}</div>
218
+ </div>
219
+
220
+ <div>
221
+ <div class="mb-1">
222
+ <strong class="font-semibold">world position</strong>
223
+ <span class="text-subtle-2"> (m)</span>
224
+ </div>
225
+ <div class="flex gap-3">
226
+ <div>
227
+ <span class="text-subtle-2">x </span>{tooltipData.closestPoint.x.toFixed(2)}
228
+ </div>
229
+ <div>
230
+ <span class="text-subtle-2">y </span>{tooltipData.closestPoint.y.toFixed(2)}
231
+ </div>
232
+ <div>
233
+ <span class="text-subtle-2">z </span>{tooltipData.closestPoint.z.toFixed(2)}
234
+ </div>
235
+ </div>
236
+ </div>
237
+ {/if}
238
+ </div>
239
+ </div>
240
+ </HTML>
241
+ {/if}
@@ -0,0 +1,7 @@
1
+ import type { Entity } from 'koota';
2
+ interface Props {
3
+ hoveredEntity: Entity;
4
+ }
5
+ declare const HoveredEntityTooltip: import("svelte").Component<Props, {}, "">;
6
+ type HoveredEntityTooltip = ReturnType<typeof HoveredEntityTooltip>;
7
+ export default HoveredEntityTooltip;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { T, type Props as ThrelteProps } from '@threlte/core'
3
+ import type { Vector3Tuple, Group } from 'three'
4
+ import { HTML } from '@threlte/extras'
5
+
6
+ interface Props extends ThrelteProps<typeof Group> {
7
+ position: Vector3Tuple
8
+ }
9
+
10
+ let { position, ref = $bindable(), ...rest }: Props = $props()
11
+ </script>
12
+
13
+ <T.Group
14
+ bind:ref
15
+ {...rest}
16
+ {position}
17
+ >
18
+ <HTML
19
+ center
20
+ class="h-2.5 w-2.5 rounded-full bg-black/70"
21
+ />
22
+
23
+ <HTML
24
+ class="pointer-events-none mb-2 w-16 -translate-x-1/2 -translate-y-[calc(100%+10px)] border border-black bg-white px-1 py-0.5 text-xs text-wrap"
25
+ >
26
+ <div class="flex justify-between">
27
+ <span class="text-subtle-2">x</span>
28
+ <div>
29
+ {position[0].toFixed(2)}<span class="text-subtle-2">m</span>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="flex justify-between">
34
+ <span class="text-subtle-2">y</span>
35
+ <div>
36
+ {position[1].toFixed(2)}<span class="text-subtle-2">m</span>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="flex justify-between">
41
+ <span class="text-subtle-2">z</span>
42
+ <div>
43
+ {position[2].toFixed(2)}<span class="text-subtle-2">m</span>
44
+ </div>
45
+ </div>
46
+ </HTML>
47
+ </T.Group>
@@ -0,0 +1,8 @@
1
+ import { type Props as ThrelteProps } from '@threlte/core';
2
+ import type { Vector3Tuple, Group } from 'three';
3
+ interface Props extends ThrelteProps<typeof Group> {
4
+ position: Vector3Tuple;
5
+ }
6
+ declare const MeasurePoint: import("svelte").Component<Props, {}, "ref">;
7
+ type MeasurePoint = ReturnType<typeof MeasurePoint>;
8
+ export default MeasurePoint;
@@ -0,0 +1,176 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte'
3
+ import { Vector3, type Intersection } from 'three'
4
+ import { T } from '@threlte/core'
5
+ import { HTML, MeshLineGeometry, MeshLineMaterial, Portal } from '@threlte/extras'
6
+ import { useSettings } from '../../hooks/useSettings.svelte'
7
+ import Button from '../dashboard/Button.svelte'
8
+ import MeasurePoint from './MeasurePoint.svelte'
9
+ import { useMouseRaycaster } from '../../hooks/useMouseRaycaster.svelte'
10
+ import { useFocusedEntity } from '../../hooks/useSelection.svelte'
11
+ import ToggleGroup from '../Overlay/ToggleGroup.svelte'
12
+ import Popover from '../Overlay/Popover.svelte'
13
+
14
+ const focusedEntity = useFocusedEntity()
15
+ const settings = useSettings()
16
+
17
+ const htmlPosition = new Vector3()
18
+
19
+ let step: 'idle' | 'p1' | 'p2' = 'idle'
20
+
21
+ let intersection = $state<Intersection>()
22
+ let p1 = $state.raw<Vector3>()
23
+ let p2 = $state.raw<Vector3>()
24
+
25
+ const enabled = $derived(settings.current.enableMeasure)
26
+
27
+ const { onclick, onmove, raycaster } = useMouseRaycaster(() => ({
28
+ enabled,
29
+ }))
30
+ raycaster.firstHitOnly = true
31
+ raycaster.params.Points.threshold = 0.005
32
+
33
+ onmove((event) => {
34
+ intersection = event.intersections[0]
35
+
36
+ // Only handle axis restrictions if a first point has been placed
37
+ if (!p1) {
38
+ return
39
+ }
40
+
41
+ if (settings.current.enableMeasureAxisX === false) {
42
+ intersection.point.x = p1.x
43
+ }
44
+
45
+ if (settings.current.enableMeasureAxisY === false) {
46
+ intersection.point.y = p1.y
47
+ }
48
+
49
+ if (settings.current.enableMeasureAxisZ === false) {
50
+ intersection.point.z = p1.z
51
+ }
52
+ })
53
+
54
+ onclick(() => {
55
+ if (step === 'idle' && intersection) {
56
+ p1 = intersection.point.clone()
57
+ step = 'p1'
58
+ } else if (step === 'p1' && intersection) {
59
+ p2 = intersection.point.clone()
60
+ step = 'p2'
61
+ } else if (step === 'p2') {
62
+ p1 = undefined
63
+ p2 = undefined
64
+ step = 'idle'
65
+ }
66
+ })
67
+
68
+ const clear = () => {
69
+ p1 = undefined
70
+ p2 = undefined
71
+ step = 'idle'
72
+ }
73
+
74
+ $effect(() => {
75
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
76
+ ;(focusedEntity.current, enabled)
77
+ untrack(() => clear())
78
+ })
79
+ </script>
80
+
81
+ <Portal id="dashboard">
82
+ <fieldset class="relative">
83
+ <div class="flex">
84
+ <Button
85
+ active={enabled}
86
+ icon="ruler"
87
+ description="{enabled ? 'Disable' : 'Enable'} measurement"
88
+ onclick={() => {
89
+ settings.current.enableMeasure = !settings.current.enableMeasure
90
+ }}
91
+ />
92
+ <Popover>
93
+ {#snippet trigger(triggerProps)}
94
+ <Button
95
+ {...triggerProps}
96
+ active={enabled}
97
+ class="border-l-0"
98
+ icon="filter-sliders"
99
+ description="Measurement settings"
100
+ />
101
+ {/snippet}
102
+
103
+ <div class="border-medium m-2 border bg-white p-2 text-xs">
104
+ <div class="flex items-center gap-2">
105
+ Enabled axes
106
+ <ToggleGroup
107
+ multiple
108
+ buttons={[
109
+ { value: 'x', on: settings.current.enableMeasureAxisX },
110
+ { value: 'y', on: settings.current.enableMeasureAxisY },
111
+ { value: 'z', on: settings.current.enableMeasureAxisZ },
112
+ ]}
113
+ onclick={(details) => {
114
+ settings.current.enableMeasureAxisX = details.includes('x')
115
+ settings.current.enableMeasureAxisY = details.includes('y')
116
+ settings.current.enableMeasureAxisZ = details.includes('z')
117
+ }}
118
+ />
119
+ </div>
120
+ </div>
121
+ </Popover>
122
+ </div>
123
+ </fieldset>
124
+ </Portal>
125
+
126
+ {#if enabled}
127
+ {#if intersection && step !== 'p2'}
128
+ <MeasurePoint
129
+ position={intersection?.point.toArray()}
130
+ opacity={0.5}
131
+ />
132
+ {/if}
133
+
134
+ {#if p1}
135
+ <MeasurePoint
136
+ position={p1.toArray()}
137
+ opacity={0.5}
138
+ />
139
+ {/if}
140
+
141
+ {#if p2}
142
+ <MeasurePoint
143
+ position={p2.toArray()}
144
+ opacity={0.5}
145
+ />
146
+ {/if}
147
+
148
+ {#if p1 && (p2 || intersection)}
149
+ <T.Mesh
150
+ raycast={() => null}
151
+ bvh={{ enabled: false }}
152
+ renderOrder={1}
153
+ >
154
+ <MeshLineGeometry points={[p1, p2 ?? intersection?.point ?? new Vector3()]} />
155
+ <MeshLineMaterial
156
+ width={2.5}
157
+ depthTest={false}
158
+ color="black"
159
+ opacity={p2 ? 0.5 : 0.2}
160
+ attenuate={false}
161
+ transparent
162
+ />
163
+ </T.Mesh>
164
+
165
+ {#if p2}
166
+ <HTML
167
+ center
168
+ position={htmlPosition.lerpVectors(p1, p2, 0.5).toArray()}
169
+ >
170
+ <div class="border border-black bg-white px-1 py-0.5 text-xs">
171
+ {p1.distanceTo(p2).toFixed(2)}<span class="text-subtle-2">m</span>
172
+ </div>
173
+ </HTML>
174
+ {/if}
175
+ {/if}
176
+ {/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;
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import * as popover from '@zag-js/popover'
3
+ import { portal, useMachine, normalizeProps } from '@zag-js/svelte'
4
+ import type { Snippet } from 'svelte'
5
+ import type { HTMLButtonAttributes } from 'svelte/elements'
6
+
7
+ interface Props {
8
+ trigger: Snippet<[HTMLButtonAttributes]>
9
+ children: Snippet
10
+ }
11
+
12
+ let { children, trigger }: Props = $props()
13
+
14
+ const id = $props.id()
15
+ const service = useMachine(popover.machine, { id })
16
+ const api = $derived(popover.connect(service, normalizeProps))
17
+ </script>
18
+
19
+ {@render trigger(api.getTriggerProps())}
20
+
21
+ <div
22
+ use:portal={{ disabled: !api.portalled }}
23
+ {...api.getPositionerProps()}
24
+ >
25
+ <div {...api.getContentProps()}>
26
+ {@render children()}
27
+ </div>
28
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ interface Props {
4
+ trigger: Snippet<[HTMLButtonAttributes]>;
5
+ children: Snippet;
6
+ }
7
+ declare const Popover: import("svelte").Component<Props, {}, "">;
8
+ type Popover = ReturnType<typeof Popover>;
9
+ export default Popover;