@viamrobotics/motion-tools 1.7.0 → 1.9.1

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 (77) hide show
  1. package/dist/components/App.svelte +11 -6
  2. package/dist/components/CameraControls.svelte +1 -1
  3. package/dist/components/Focus.svelte +1 -1
  4. package/dist/components/Frame.svelte +1 -1
  5. package/dist/components/Geometry2.svelte +8 -5
  6. package/dist/components/HoveredEntities.svelte +19 -0
  7. package/dist/components/HoveredEntities.svelte.d.ts +3 -0
  8. package/dist/components/HoveredEntityTooltip.svelte +242 -0
  9. package/dist/components/HoveredEntityTooltip.svelte.d.ts +7 -0
  10. package/dist/components/Label.svelte +1 -1
  11. package/dist/components/{DotSprite.svelte → MeasureTool/MeasurePoint.svelte} +8 -18
  12. package/dist/components/MeasureTool/MeasurePoint.svelte.d.ts +8 -0
  13. package/dist/components/MeasureTool/MeasureTool.svelte +176 -0
  14. package/dist/components/Scene.svelte +1 -1
  15. package/dist/components/{Details.svelte → overlay/Details.svelte} +10 -10
  16. package/dist/components/{LiveUpdatesBanner.svelte → overlay/LiveUpdatesBanner.svelte} +2 -2
  17. package/dist/components/overlay/Popover.svelte +28 -0
  18. package/dist/components/overlay/Popover.svelte.d.ts +9 -0
  19. package/dist/components/overlay/ToggleGroup.svelte +60 -0
  20. package/dist/components/overlay/ToggleGroup.svelte.d.ts +13 -0
  21. package/dist/components/{__tests__ → overlay/__tests__}/__fixtures__/entity.js +1 -1
  22. package/dist/components/{dashboard → overlay/dashboard}/Button.svelte +8 -4
  23. package/dist/components/{dashboard → overlay/dashboard}/Button.svelte.d.ts +3 -2
  24. package/dist/components/{dashboard → overlay/dashboard}/Dashboard.svelte +2 -2
  25. package/dist/components/{Tree → overlay/left-pane}/AddFrames.svelte +2 -2
  26. package/dist/components/{Tree → overlay/left-pane}/Logs.svelte +1 -1
  27. package/dist/components/{RefreshRate.svelte → overlay/left-pane/RefreshRate.svelte} +1 -1
  28. package/dist/components/{Tree → overlay/left-pane}/Settings.svelte +12 -7
  29. package/dist/components/{Tree → overlay/left-pane}/Tree.svelte +3 -3
  30. package/dist/components/{Tree → overlay/left-pane}/TreeContainer.svelte +8 -8
  31. package/dist/components/{Tree → overlay/left-pane}/Widgets.svelte +3 -3
  32. package/dist/components/{Tree → overlay/left-pane}/buildTree.js +1 -1
  33. package/dist/components/{widgets → overlay/widgets}/ArmPositions.svelte +4 -4
  34. package/dist/components/{widgets → overlay/widgets}/Camera.svelte +5 -5
  35. package/dist/ecs/traits.d.ts +19 -12
  36. package/dist/ecs/traits.js +18 -11
  37. package/dist/ecs/useQuery.svelte.js +10 -10
  38. package/dist/hooks/useDrawAPI.svelte.js +1 -1
  39. package/dist/hooks/useFrames.svelte.js +1 -0
  40. package/dist/hooks/useGeometries.svelte.js +1 -1
  41. package/dist/hooks/useObjectEvents.svelte.d.ts +1 -0
  42. package/dist/hooks/useObjectEvents.svelte.js +24 -0
  43. package/dist/hooks/usePointcloudObjects.svelte.js +1 -1
  44. package/dist/hooks/usePointclouds.svelte.js +28 -41
  45. package/dist/hooks/usePose.svelte.js +1 -1
  46. package/dist/hooks/useSettings.svelte.d.ts +4 -0
  47. package/dist/hooks/useSettings.svelte.js +4 -0
  48. package/dist/hooks/useWorldState.svelte.js +1 -1
  49. package/dist/three/InstancedArrows/raycast.js +2 -6
  50. package/package.json +6 -2
  51. package/dist/components/DotSprite.svelte.d.ts +0 -10
  52. package/dist/components/MeasureTool.svelte +0 -123
  53. package/dist/components/null-states/Connection.svelte +0 -0
  54. package/dist/components/null-states/Connection.svelte.d.ts +0 -26
  55. /package/dist/components/{MeasureTool.svelte.d.ts → MeasureTool/MeasureTool.svelte.d.ts} +0 -0
  56. /package/dist/components/{Details.svelte.d.ts → overlay/Details.svelte.d.ts} +0 -0
  57. /package/dist/components/{LiveUpdatesBanner.svelte.d.ts → overlay/LiveUpdatesBanner.svelte.d.ts} +0 -0
  58. /package/dist/components/{shared → overlay}/Table.svelte +0 -0
  59. /package/dist/components/{shared → overlay}/Table.svelte.d.ts +0 -0
  60. /package/dist/components/{__tests__ → overlay/__tests__}/__fixtures__/entity.d.ts +0 -0
  61. /package/dist/components/{__tests__ → overlay/__tests__}/__fixtures__/resource.d.ts +0 -0
  62. /package/dist/components/{__tests__ → overlay/__tests__}/__fixtures__/resource.js +0 -0
  63. /package/dist/components/{dashboard → overlay/dashboard}/Dashboard.svelte.d.ts +0 -0
  64. /package/dist/components/{Tree → overlay/left-pane}/AddFrames.svelte.d.ts +0 -0
  65. /package/dist/components/{Tree → overlay/left-pane}/Drawer.svelte +0 -0
  66. /package/dist/components/{Tree → overlay/left-pane}/Drawer.svelte.d.ts +0 -0
  67. /package/dist/components/{Tree → overlay/left-pane}/Logs.svelte.d.ts +0 -0
  68. /package/dist/components/{RefreshRate.svelte.d.ts → overlay/left-pane/RefreshRate.svelte.d.ts} +0 -0
  69. /package/dist/components/{Tree → overlay/left-pane}/Settings.svelte.d.ts +0 -0
  70. /package/dist/components/{Tree → overlay/left-pane}/Tree.svelte.d.ts +0 -0
  71. /package/dist/components/{Tree → overlay/left-pane}/TreeContainer.svelte.d.ts +0 -0
  72. /package/dist/components/{Tree → overlay/left-pane}/Widgets.svelte.d.ts +0 -0
  73. /package/dist/components/{Tree → overlay/left-pane}/buildTree.d.ts +0 -0
  74. /package/dist/components/{Tree → overlay/left-pane}/useExpanded.svelte.d.ts +0 -0
  75. /package/dist/components/{Tree → overlay/left-pane}/useExpanded.svelte.js +0 -0
  76. /package/dist/components/{widgets → overlay/widgets}/ArmPositions.svelte.d.ts +0 -0
  77. /package/dist/components/{widgets → overlay/widgets}/Camera.svelte.d.ts +0 -0
@@ -5,20 +5,20 @@
5
5
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
6
6
  import type { Struct } from '@viamrobotics/sdk'
7
7
  import Scene from './Scene.svelte'
8
- import TreeContainer from './Tree/TreeContainer.svelte'
9
- import Details from './Details.svelte'
8
+ import TreeContainer from './overlay/left-pane/TreeContainer.svelte'
9
+ import Details from './overlay/Details.svelte'
10
10
  import SceneProviders from './SceneProviders.svelte'
11
11
  import XR from './xr/XR.svelte'
12
12
  import { createPartIDContext } from '../hooks/usePartID.svelte'
13
- import Dashboard from './dashboard/Dashboard.svelte'
13
+ import Dashboard from './overlay/dashboard/Dashboard.svelte'
14
14
  import { domPortal } from '../portal'
15
15
  import { provideSettings } from '../hooks/useSettings.svelte'
16
16
  import FileDrop from './FileDrop/FileDrop.svelte'
17
17
  import { provideWeblabs } from '../hooks/useWeblabs.svelte'
18
18
  import { providePartConfig } from '../hooks/usePartConfig.svelte'
19
19
  import { useViamClient } from '@viamrobotics/svelte-sdk'
20
- import LiveUpdatesBanner from './LiveUpdatesBanner.svelte'
21
- import ArmPositions from './widgets/ArmPositions.svelte'
20
+ import LiveUpdatesBanner from './overlay/LiveUpdatesBanner.svelte'
21
+ import ArmPositions from './overlay/widgets/ArmPositions.svelte'
22
22
  import { provideEnvironment } from '../hooks/useEnvironment.svelte'
23
23
  import type { CameraPose } from '../hooks/useControls.svelte'
24
24
  import { provideWorld } from '../ecs'
@@ -26,7 +26,8 @@
26
26
  provideDrawConnectionConfig,
27
27
  type DrawConnectionConfig,
28
28
  } from '../hooks/useDrawConnectionConfig.svelte'
29
- import Camera from './widgets/Camera.svelte'
29
+ import Camera from './overlay/widgets/Camera.svelte'
30
+ import HoveredEntities from './HoveredEntities.svelte'
30
31
 
31
32
  interface LocalConfigProps {
32
33
  getLocalPartConfig: () => Struct
@@ -128,6 +129,10 @@
128
129
  {@attach domPortal(root)}
129
130
  {dashboard}
130
131
  />
132
+
133
+ {#if settings.current.renderSubEntityHoverDetail}
134
+ <HoveredEntities {@attach domPortal(root)} />
135
+ {/if}
131
136
  <Details {@attach domPortal(root)} />
132
137
  {#if environment.current.isStandalone}
133
138
  <LiveUpdatesBanner {@attach domPortal(root)} />
@@ -3,7 +3,7 @@
3
3
  import { CameraControls, type CameraControlsRef, Gizmo, Portal } from '@threlte/extras'
4
4
  import { useCameraControls, useTransformControls } from '../hooks/useControls.svelte'
5
5
  import KeyboardControls from './KeyboardControls.svelte'
6
- import Button from './dashboard/Button.svelte'
6
+ import Button from './overlay/dashboard/Button.svelte'
7
7
  import { useSettings } from '../hooks/useSettings.svelte'
8
8
 
9
9
  const cameraControls = useCameraControls()
@@ -4,7 +4,7 @@
4
4
  import { Box3, type Object3D, Vector3 } from 'three'
5
5
  import { TrackballControls as ThreeTrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
6
6
  import Camera from './Camera.svelte'
7
- import Button from './dashboard/Button.svelte'
7
+ import Button from './overlay/dashboard/Button.svelte'
8
8
 
9
9
  interface Props {
10
10
  object3d: Object3D
@@ -62,7 +62,7 @@
62
62
  if (!componentName || !id) {
63
63
  return
64
64
  }
65
- return componentModels.current?.[componentName]?.[id]
65
+ return componentModels.current?.[componentName]?.[id].clone()
66
66
  })
67
67
  </script>
68
68
 
@@ -46,6 +46,7 @@
46
46
  const linePositions = useTrait(() => entity, traits.LinePositions)
47
47
  const lineWidth = useTrait(() => entity, traits.LineWidth)
48
48
  const center = useTrait(() => entity, traits.Center)
49
+ const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
49
50
 
50
51
  const geometryType = $derived.by(() => {
51
52
  if (box.current) return 'box'
@@ -124,10 +125,12 @@
124
125
  {...rest}
125
126
  >
126
127
  {#if geometryType}
127
- <AxesHelper
128
- width={3}
129
- length={0.1}
130
- />
128
+ {#if showAxesHelper.current}
129
+ <AxesHelper
130
+ width={3}
131
+ length={0.1}
132
+ />
133
+ {/if}
131
134
 
132
135
  <T
133
136
  is={mesh}
@@ -196,7 +199,7 @@
196
199
  {/if}
197
200
  {/if}
198
201
  </T>
199
- {:else}
202
+ {:else if showAxesHelper.current}
200
203
  <AxesHelper
201
204
  name={name.current}
202
205
  width={3}
@@ -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,242 @@
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
+ zIndexRange={[3, 0]}
162
+ >
163
+ <div
164
+ 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"
165
+ >
166
+ <!-- Arrow -->
167
+ <div
168
+ class="border-medium absolute -bottom-[5px] left-1/2 size-2.5 -translate-x-1/2 rotate-45 border-r border-b bg-white"
169
+ ></div>
170
+
171
+ <div class="flex flex-col gap-2.5">
172
+ {#if tooltipData.closestArrow}
173
+ <div>
174
+ <div class="mb-1"><strong class="font-semibold">index</strong></div>
175
+ <div>{tooltipData.closestArrow.index}</div>
176
+ </div>
177
+
178
+ <div>
179
+ <div class="mb-1">
180
+ <strong class="font-semibold">world position</strong>
181
+ <span class="text-subtle-2"> (m)</span>
182
+ </div>
183
+ <div class="flex gap-3">
184
+ <div>
185
+ <span class="text-subtle-2 mr-1">x </span>{tooltipData.closestArrow.x.toFixed(2)}
186
+ </div>
187
+ <div>
188
+ <span class="text-subtle-2 mr-1">y </span>{tooltipData.closestArrow.y.toFixed(2)}
189
+ </div>
190
+ <div>
191
+ <span class="text-subtle-2 mr-1">z </span>{tooltipData.closestArrow.z.toFixed(2)}
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <div>
197
+ <div class="mb-1">
198
+ <strong class="font-semibold">world orientation</strong>
199
+ <span class="text-subtle-2"> (deg)</span>
200
+ </div>
201
+ <div class="flex gap-3">
202
+ <div>
203
+ <span class="text-subtle-2 mr-1">x </span>{tooltipData.closestArrow.oX.toFixed(2)}
204
+ </div>
205
+ <div>
206
+ <span class="text-subtle-2 mr-1">y </span>{tooltipData.closestArrow.oY.toFixed(2)}
207
+ </div>
208
+ <div>
209
+ <span class="text-subtle-2 mr-1">z </span>{tooltipData.closestArrow.oZ.toFixed(2)}
210
+ </div>
211
+ </div>
212
+ </div>
213
+ {/if}
214
+
215
+ {#if tooltipData.closestPoint}
216
+ <div>
217
+ <div class="mb-1"><strong class="font-semibold">index</strong></div>
218
+ <div>{tooltipData.closestPoint.index}</div>
219
+ </div>
220
+
221
+ <div>
222
+ <div class="mb-1">
223
+ <strong class="font-semibold">world position</strong>
224
+ <span class="text-subtle-2"> (m)</span>
225
+ </div>
226
+ <div class="flex gap-3">
227
+ <div>
228
+ <span class="text-subtle-2">x </span>{tooltipData.closestPoint.x.toFixed(2)}
229
+ </div>
230
+ <div>
231
+ <span class="text-subtle-2">y </span>{tooltipData.closestPoint.y.toFixed(2)}
232
+ </div>
233
+ <div>
234
+ <span class="text-subtle-2">z </span>{tooltipData.closestPoint.z.toFixed(2)}
235
+ </div>
236
+ </div>
237
+ </div>
238
+ {/if}
239
+ </div>
240
+ </div>
241
+ </HTML>
242
+ {/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;
@@ -17,7 +17,7 @@
17
17
  {#if labels && text}
18
18
  <HTML
19
19
  center
20
- zIndexRange={[100, 0]}
20
+ zIndexRange={[3, 0]}
21
21
  class="border-gray-7 border bg-white px-2 py-1 text-xs"
22
22
  >
23
23
  {text}
@@ -1,15 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { T, type Props as ThrelteProps } from '@threlte/core'
3
- import type { ColorRepresentation, Vector3Tuple, Group } from 'three'
3
+ import type { Vector3Tuple, Group } from 'three'
4
4
  import { HTML } from '@threlte/extras'
5
5
 
6
6
  interface Props extends ThrelteProps<typeof Group> {
7
7
  position: Vector3Tuple
8
- color?: ColorRepresentation
9
- opacity?: number
10
8
  }
11
9
 
12
- let { position, color, opacity = 1, ref = $bindable(), ...rest }: Props = $props()
10
+ let { position, ref = $bindable(), ...rest }: Props = $props()
13
11
  </script>
14
12
 
15
13
  <T.Group
@@ -17,23 +15,15 @@
17
15
  {...rest}
18
16
  {position}
19
17
  >
20
- <T.Mesh
21
- bvh={{ enabled: false }}
22
- raycast={() => null}
23
- scale={0.01}
24
- renderOrder={1}
25
- >
26
- <T.SphereGeometry />
27
- <T.MeshBasicMaterial
28
- color={color ?? 'black'}
29
- transparent
30
- depthTest={false}
31
- {opacity}
32
- />
33
- </T.Mesh>
18
+ <HTML
19
+ center
20
+ zIndexRange={[3, 0]}
21
+ class="h-2.5 w-2.5 rounded-full bg-black/70"
22
+ />
34
23
 
35
24
  <HTML
36
25
  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"
26
+ zIndexRange={[3, 0]}
37
27
  >
38
28
  <div class="flex justify-between">
39
29
  <span class="text-subtle-2">x</span>
@@ -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 '../overlay/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 = $state<'idle' | 'p1' | 'p2'>('idle')
20
+ let intersection = $state<Intersection>()
21
+ let p1 = $state.raw<Vector3>()
22
+ let p2 = $state.raw<Vector3>()
23
+
24
+ const enabled = $derived(settings.current.enableMeasure)
25
+
26
+ const { onclick, onmove, raycaster } = useMouseRaycaster(() => ({
27
+ enabled,
28
+ }))
29
+ raycaster.firstHitOnly = true
30
+ raycaster.params.Points.threshold = 0.005
31
+
32
+ onmove((event) => {
33
+ intersection = event.intersections[0]
34
+
35
+ // Only handle axis restrictions if a first point has been placed
36
+ if (!p1) {
37
+ return
38
+ }
39
+
40
+ if (settings.current.enableMeasureAxisX === false) {
41
+ intersection.point.x = p1.x
42
+ }
43
+
44
+ if (settings.current.enableMeasureAxisY === false) {
45
+ intersection.point.y = p1.y
46
+ }
47
+
48
+ if (settings.current.enableMeasureAxisZ === false) {
49
+ intersection.point.z = p1.z
50
+ }
51
+ })
52
+
53
+ onclick(() => {
54
+ if (step === 'idle' && intersection) {
55
+ p1 = intersection.point.clone()
56
+ step = 'p1'
57
+ } else if (step === 'p1' && intersection) {
58
+ p2 = intersection.point.clone()
59
+ step = 'p2'
60
+ } else if (step === 'p2') {
61
+ p1 = undefined
62
+ p2 = undefined
63
+ step = 'idle'
64
+ }
65
+ })
66
+
67
+ const clear = () => {
68
+ p1 = undefined
69
+ p2 = undefined
70
+ step = 'idle'
71
+ }
72
+
73
+ $effect(() => {
74
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
75
+ ;(focusedEntity.current, enabled)
76
+ untrack(() => clear())
77
+ })
78
+ </script>
79
+
80
+ <Portal id="dashboard">
81
+ <fieldset class="relative">
82
+ <div class="flex">
83
+ <Button
84
+ active={enabled}
85
+ icon="ruler"
86
+ description="{enabled ? 'Disable' : 'Enable'} measurement"
87
+ onclick={() => {
88
+ settings.current.enableMeasure = !settings.current.enableMeasure
89
+ }}
90
+ />
91
+ <Popover>
92
+ {#snippet trigger(triggerProps)}
93
+ <Button
94
+ {...triggerProps}
95
+ active={enabled}
96
+ class="border-l-0"
97
+ icon="filter-sliders"
98
+ description="Measurement settings"
99
+ />
100
+ {/snippet}
101
+
102
+ <div class="border-medium m-2 border bg-white p-2 text-xs">
103
+ <div class="flex items-center gap-2">
104
+ Enabled axes
105
+ <ToggleGroup
106
+ multiple
107
+ buttons={[
108
+ { value: 'x', on: settings.current.enableMeasureAxisX },
109
+ { value: 'y', on: settings.current.enableMeasureAxisY },
110
+ { value: 'z', on: settings.current.enableMeasureAxisZ },
111
+ ]}
112
+ onclick={(details) => {
113
+ settings.current.enableMeasureAxisX = details.includes('x')
114
+ settings.current.enableMeasureAxisY = details.includes('y')
115
+ settings.current.enableMeasureAxisZ = details.includes('z')
116
+ }}
117
+ />
118
+ </div>
119
+ </div>
120
+ </Popover>
121
+ </div>
122
+ </fieldset>
123
+ </Portal>
124
+
125
+ {#if enabled}
126
+ {#if intersection && step !== 'p2'}
127
+ <MeasurePoint
128
+ position={intersection?.point.toArray()}
129
+ opacity={0.5}
130
+ />
131
+ {/if}
132
+
133
+ {#if p1}
134
+ <MeasurePoint
135
+ position={p1.toArray()}
136
+ opacity={0.5}
137
+ />
138
+ {/if}
139
+
140
+ {#if p2}
141
+ <MeasurePoint
142
+ position={p2.toArray()}
143
+ opacity={0.5}
144
+ />
145
+ {/if}
146
+
147
+ {#if p1 && (p2 || intersection)}
148
+ <T.Mesh
149
+ raycast={() => null}
150
+ bvh={{ enabled: false }}
151
+ renderOrder={1}
152
+ >
153
+ <MeshLineGeometry points={[p1, p2 ?? intersection?.point ?? new Vector3()]} />
154
+ <MeshLineMaterial
155
+ width={2.5}
156
+ depthTest={false}
157
+ color="black"
158
+ opacity={p2 ? 0.5 : 0.2}
159
+ attenuate={false}
160
+ transparent
161
+ />
162
+ </T.Mesh>
163
+
164
+ {#if p2}
165
+ <HTML
166
+ center
167
+ position={htmlPosition.lerpVectors(p1, p2, 0.5).toArray()}
168
+ zIndexRange={[3, 0]}
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}
@@ -14,7 +14,7 @@
14
14
  import { useOrigin } from './xr/useOrigin.svelte'
15
15
  import { useSettings } from '../hooks/useSettings.svelte'
16
16
  import CameraControls from './CameraControls.svelte'
17
- import MeasureTool from './MeasureTool.svelte'
17
+ import MeasureTool from './MeasureTool/MeasureTool.svelte'
18
18
  import PointerMissBox from './PointerMissBox.svelte'
19
19
  import BatchedArrows from './BatchedArrows.svelte'
20
20
  import Arrows from './Arrows/ArrowGroups.svelte'
@@ -2,7 +2,7 @@
2
2
  module
3
3
  lang="ts"
4
4
  >
5
- import { OrientationVector } from '../three/OrientationVector'
5
+ import { OrientationVector } from '../../three/OrientationVector'
6
6
  import { Quaternion, Vector3, MathUtils, BufferAttribute } from 'three'
7
7
 
8
8
  const vec3 = new Vector3()
@@ -20,14 +20,14 @@
20
20
  useFocusedEntity,
21
21
  useFocusedObject3d,
22
22
  useSelectedObject3d,
23
- } from '../hooks/useSelection.svelte'
24
- import { useFrames } from '../hooks/useFrames.svelte'
25
- import { usePartConfig } from '../hooks/usePartConfig.svelte'
26
- import { FrameConfigUpdater } from '../FrameConfigUpdater.svelte'
27
- import { useEnvironment } from '../hooks/useEnvironment.svelte'
28
- import { traits, useTrait, useWorld } from '../ecs'
29
- import { useResourceByName } from '../hooks/useResourceByName.svelte'
30
- import { useCameraControls } from '../hooks/useControls.svelte'
23
+ } from '../../hooks/useSelection.svelte'
24
+ import { useFrames } from '../../hooks/useFrames.svelte'
25
+ import { usePartConfig } from '../../hooks/usePartConfig.svelte'
26
+ import { FrameConfigUpdater } from '../../FrameConfigUpdater.svelte'
27
+ import { useEnvironment } from '../../hooks/useEnvironment.svelte'
28
+ import { traits, useTrait, useWorld } from '../../ecs'
29
+ import { useResourceByName } from '../../hooks/useResourceByName.svelte'
30
+ import { useCameraControls } from '../../hooks/useControls.svelte'
31
31
 
32
32
  const { ...rest } = $props()
33
33
 
@@ -232,7 +232,7 @@
232
232
 
233
233
  <div
234
234
  id="details-panel"
235
- class="border-medium bg-extralight absolute top-0 right-0 z-10 m-2 {showEditFrameOptions
235
+ class="border-medium bg-extralight absolute top-0 right-0 z-4 m-2 {showEditFrameOptions
236
236
  ? 'w-80'
237
237
  : 'w-60'} border p-2 text-xs"
238
238
  use:draggable={{