@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.
- package/README.md +16 -9
- package/dist/components/App.svelte +17 -0
- package/dist/components/Frame.svelte +0 -7
- package/dist/components/HoveredEntities.svelte +19 -0
- package/dist/components/HoveredEntities.svelte.d.ts +3 -0
- package/dist/components/HoveredEntityTooltip.svelte +241 -0
- package/dist/components/HoveredEntityTooltip.svelte.d.ts +7 -0
- package/dist/components/MeasureTool/MeasurePoint.svelte +47 -0
- package/dist/components/MeasureTool/MeasurePoint.svelte.d.ts +8 -0
- package/dist/components/MeasureTool/MeasureTool.svelte +176 -0
- package/dist/components/MeasureTool/MeasureTool.svelte.d.ts +3 -0
- package/dist/components/Overlay/Popover.svelte +28 -0
- package/dist/components/Overlay/Popover.svelte.d.ts +9 -0
- package/dist/components/Overlay/ToggleGroup.svelte +60 -0
- package/dist/components/Overlay/ToggleGroup.svelte.d.ts +13 -0
- package/dist/components/Scene.svelte +1 -1
- package/dist/components/Tree/Settings.svelte +23 -22
- package/dist/components/Tree/Widgets.svelte +44 -0
- package/dist/components/Tree/Widgets.svelte.d.ts +2 -17
- package/dist/components/dashboard/Button.svelte +7 -3
- package/dist/components/dashboard/Button.svelte.d.ts +3 -2
- package/dist/components/widgets/Camera.svelte +195 -0
- package/dist/components/widgets/Camera.svelte.d.ts +6 -0
- package/dist/ecs/traits.d.ts +18 -12
- package/dist/ecs/traits.js +17 -11
- package/dist/ecs/useQuery.svelte.js +10 -10
- package/dist/hooks/use3DModels.svelte.js +1 -3
- package/dist/hooks/useObjectEvents.svelte.d.ts +1 -0
- package/dist/hooks/useObjectEvents.svelte.js +24 -0
- package/dist/hooks/useSettings.svelte.d.ts +5 -0
- package/dist/hooks/useSettings.svelte.js +5 -0
- package/dist/hooks/useWeblabs.svelte.d.ts +1 -3
- package/dist/hooks/useWeblabs.svelte.js +1 -3
- package/dist/three/InstancedArrows/raycast.js +2 -6
- 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
|
|
21
|
-
2. Install
|
|
22
|
-
3. Install **
|
|
23
|
-
4. Install **
|
|
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
|
|
31
|
-
2. Install Node.js
|
|
32
|
-
3. [Install pnpm](https://pnpm.io/installation)
|
|
33
|
-
4. [Install bun](https://bun.sh/docs/installation)
|
|
34
|
-
5. Install
|
|
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,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,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;
|