@viamrobotics/motion-tools 1.34.4 → 1.34.6
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/dist/components/App.svelte +14 -2
- package/dist/components/App.svelte.d.ts +6 -2
- package/dist/components/AxesHelper.svelte +3 -1
- package/dist/components/AxesHelper.svelte.d.ts +1 -1
- package/dist/components/Entities/Arrows/Arrows.svelte +0 -9
- package/dist/components/Entities/AxesHelper.svelte +38 -0
- package/dist/components/Entities/AxesHelper.svelte.d.ts +8 -0
- package/dist/components/Entities/AxesHelpers.svelte +13 -0
- package/dist/{plugins/LLMSceneBuilder/AISettings.svelte.d.ts → components/Entities/AxesHelpers.svelte.d.ts} +6 -14
- package/dist/components/Entities/Boxes.svelte +290 -0
- package/dist/components/Entities/Boxes.svelte.d.ts +14 -0
- package/dist/components/Entities/Entities.svelte +10 -5
- package/dist/components/Entities/GLTF.svelte +0 -9
- package/dist/components/Entities/Line.svelte +0 -9
- package/dist/components/Entities/Mesh.svelte +5 -23
- package/dist/components/Entities/Points.svelte +1 -9
- package/dist/components/Entities/composeBoxMatrix.d.ts +12 -0
- package/dist/components/Entities/composeBoxMatrix.js +29 -0
- package/dist/components/Entities/hooks/useEntityEvents.svelte.d.ts +27 -0
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +87 -39
- package/dist/components/Scene.svelte +0 -1
- package/dist/components/Selected.svelte +14 -3
- package/dist/components/SelectedTransformControls.svelte +3 -5
- package/dist/components/overlay/Details.svelte +9 -4
- package/dist/hooks/plugins/bvh.svelte.js +9 -0
- package/dist/hooks/useConfigFrames.svelte.js +5 -3
- package/dist/hooks/useFragmentInfo.svelte.d.ts +24 -0
- package/dist/hooks/useFragmentInfo.svelte.js +86 -0
- package/dist/hooks/useFramelessComponents.svelte.js +3 -1
- package/dist/hooks/usePartConfig.svelte.d.ts +0 -6
- package/dist/hooks/usePartConfig.svelte.js +5 -60
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugins/Focus/FocusBox.svelte +12 -1
- package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.d.ts +9 -2
- package/dist/plugins/LLMSceneBuilder/frameDeltaAdapter.js +65 -10
- package/dist/plugins/LLMSceneBuilder/useSceneBuilder.svelte.js +29 -5
- package/dist/plugins/Selection/Ellipse.svelte +9 -26
- package/dist/plugins/Selection/Ellipse.svelte.d.ts +2 -1
- package/dist/plugins/Selection/Lasso.svelte +9 -26
- package/dist/plugins/Selection/Lasso.svelte.d.ts +2 -1
- package/dist/plugins/Selection/SelectionTool.svelte +18 -3
- package/dist/plugins/Selection/SelectionTool.svelte.d.ts +2 -0
- package/dist/plugins/TopDownLock/TopDownLock.svelte +25 -0
- package/dist/plugins/TopDownLock/TopDownLock.svelte.d.ts +3 -0
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/three/OBBHelper.d.ts +8 -1
- package/dist/three/OBBHelper.js +11 -1
- package/package.json +3 -2
- package/dist/plugins/LLMSceneBuilder/AISettings.svelte +0 -0
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
module
|
|
3
3
|
lang="ts"
|
|
4
4
|
>
|
|
5
|
-
import {
|
|
5
|
+
import { EdgesGeometry, SphereGeometry } from 'three'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Shared unit geometries — every mesh references these and sets
|
|
9
9
|
* dimensions through `mesh.scale`, so resizing never rebuilds GPU buffers.
|
|
10
10
|
*/
|
|
11
|
-
const unitBox = new BoxGeometry(1, 1, 1)
|
|
12
11
|
const unitSphere = new SphereGeometry(1, 16, 12)
|
|
13
|
-
const unitBoxEdges = new EdgesGeometry(unitBox, 0)
|
|
14
12
|
const unitSphereEdges = new EdgesGeometry(unitSphere, 0)
|
|
15
13
|
</script>
|
|
16
14
|
|
|
@@ -27,7 +25,6 @@
|
|
|
27
25
|
import { traits, useTrait } from '../../ecs'
|
|
28
26
|
import { poseToObject3d } from '../../transform'
|
|
29
27
|
|
|
30
|
-
import AxesHelper from '../AxesHelper.svelte'
|
|
31
28
|
import Capsule from './Capsule.svelte'
|
|
32
29
|
|
|
33
30
|
interface Props extends Omit<ThrelteProps<Mesh>, 'ref'> {
|
|
@@ -46,11 +43,9 @@
|
|
|
46
43
|
const entityColors = useTrait(() => entity, traits.Colors)
|
|
47
44
|
const entityColor = useTrait(() => entity, traits.Color)
|
|
48
45
|
const opacity = useTrait(() => entity, traits.Opacity)
|
|
49
|
-
const box = useTrait(() => entity, traits.Box)
|
|
50
46
|
const capsule = useTrait(() => entity, traits.Capsule)
|
|
51
47
|
const sphere = useTrait(() => entity, traits.Sphere)
|
|
52
48
|
const bufferGeometry = useTrait(() => entity, traits.BufferGeometry)
|
|
53
|
-
const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
|
|
54
49
|
const materialProps = useTrait(() => entity, traits.Material)
|
|
55
50
|
const renderOrder = useTrait(() => entity, traits.RenderOrder)
|
|
56
51
|
|
|
@@ -98,10 +93,7 @@
|
|
|
98
93
|
})
|
|
99
94
|
|
|
100
95
|
$effect(() => {
|
|
101
|
-
if (
|
|
102
|
-
const { x, y, z } = box.current
|
|
103
|
-
mesh.scale.set(x * 0.001, y * 0.001, z * 0.001)
|
|
104
|
-
} else if (sphere.current) {
|
|
96
|
+
if (sphere.current) {
|
|
105
97
|
mesh.scale.setScalar((sphere.current.r ?? 0) * 0.001)
|
|
106
98
|
} else {
|
|
107
99
|
mesh.scale.set(1, 1, 1)
|
|
@@ -137,9 +129,7 @@
|
|
|
137
129
|
renderOrder={renderOrder.current}
|
|
138
130
|
{...rest}
|
|
139
131
|
>
|
|
140
|
-
{#if
|
|
141
|
-
{@const meshGeometry = box.current ? unitBox : unitSphere}
|
|
142
|
-
{@const edgesGeometry = box.current ? unitBoxEdges : unitSphereEdges}
|
|
132
|
+
{#if sphere.current}
|
|
143
133
|
<!--
|
|
144
134
|
Switch via a derived `is` on the same <T> so `useAttach`'s effect
|
|
145
135
|
cleanup runs before the new attach. Splitting these across two
|
|
@@ -148,7 +138,7 @@
|
|
|
148
138
|
it to the pre-attach value (null), leaving the mesh geometryless.
|
|
149
139
|
-->
|
|
150
140
|
<T
|
|
151
|
-
is={
|
|
141
|
+
is={unitSphere}
|
|
152
142
|
dispose={false}
|
|
153
143
|
/>
|
|
154
144
|
<T.LineSegments
|
|
@@ -156,7 +146,7 @@
|
|
|
156
146
|
bvh={{ enabled: false }}
|
|
157
147
|
>
|
|
158
148
|
<T
|
|
159
|
-
is={
|
|
149
|
+
is={unitSphereEdges}
|
|
160
150
|
dispose={false}
|
|
161
151
|
/>
|
|
162
152
|
<T.LineBasicMaterial color={darkenColor(color, 10)} />
|
|
@@ -193,11 +183,3 @@
|
|
|
193
183
|
{@render children?.()}
|
|
194
184
|
</T>
|
|
195
185
|
{/if}
|
|
196
|
-
|
|
197
|
-
{#if showAxesHelper.current}
|
|
198
|
-
<AxesHelper
|
|
199
|
-
name={entity}
|
|
200
|
-
width={3}
|
|
201
|
-
length={0.1}
|
|
202
|
-
/>
|
|
203
|
-
{/if}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { traits, useTrait } from '../../ecs'
|
|
10
10
|
import { useSettings } from '../../hooks/useSettings.svelte'
|
|
11
11
|
|
|
12
|
-
import AxesHelper from '../AxesHelper.svelte'
|
|
13
12
|
import { useEntityEvents } from './hooks/useEntityEvents.svelte'
|
|
14
13
|
|
|
15
14
|
interface Props {
|
|
@@ -29,7 +28,6 @@
|
|
|
29
28
|
const entityPointSize = useTrait(() => entity, traits.PointSize)
|
|
30
29
|
const opacity = useTrait(() => entity, traits.Opacity)
|
|
31
30
|
const invisible = useTrait(() => entity, traits.InheritedInvisible)
|
|
32
|
-
const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
|
|
33
31
|
const renderOrder = useTrait(() => entity, traits.RenderOrder)
|
|
34
32
|
const materialProps = useTrait(() => entity, traits.Material)
|
|
35
33
|
|
|
@@ -134,13 +132,7 @@
|
|
|
134
132
|
>
|
|
135
133
|
<T is={geometry.current} />
|
|
136
134
|
<T is={material} />
|
|
137
|
-
|
|
138
|
-
<AxesHelper
|
|
139
|
-
name={entity}
|
|
140
|
-
width={3}
|
|
141
|
-
length={0.1}
|
|
142
|
-
/>
|
|
143
|
-
{/if}
|
|
135
|
+
|
|
144
136
|
{@render children?.()}
|
|
145
137
|
</T>
|
|
146
138
|
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Entity } from 'koota';
|
|
2
|
+
import { Matrix4 } from 'three';
|
|
3
|
+
/**
|
|
4
|
+
* Compose a box entity's full render transform into `out`:
|
|
5
|
+
* `WorldMatrix × Center pose × box dimensions (mm → m)` — the same
|
|
6
|
+
* composition the per-entity path produced by nesting a dimension-scaled,
|
|
7
|
+
* center-offset mesh inside a `WorldMatrix`-driven group.
|
|
8
|
+
*
|
|
9
|
+
* Returns `false` (leaving `out` untouched) when the entity is missing the
|
|
10
|
+
* traits needed to place a box.
|
|
11
|
+
*/
|
|
12
|
+
export declare const composeBoxMatrix: (entity: Entity, out: Matrix4) => boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Matrix4, Vector3 } from 'three';
|
|
2
|
+
import { traits } from '../../ecs';
|
|
3
|
+
import { poseToMatrix } from '../../transform';
|
|
4
|
+
const centerMatrix = new Matrix4();
|
|
5
|
+
const dimensions = new Vector3();
|
|
6
|
+
const MM_TO_M = 0.001;
|
|
7
|
+
/**
|
|
8
|
+
* Compose a box entity's full render transform into `out`:
|
|
9
|
+
* `WorldMatrix × Center pose × box dimensions (mm → m)` — the same
|
|
10
|
+
* composition the per-entity path produced by nesting a dimension-scaled,
|
|
11
|
+
* center-offset mesh inside a `WorldMatrix`-driven group.
|
|
12
|
+
*
|
|
13
|
+
* Returns `false` (leaving `out` untouched) when the entity is missing the
|
|
14
|
+
* traits needed to place a box.
|
|
15
|
+
*/
|
|
16
|
+
export const composeBoxMatrix = (entity, out) => {
|
|
17
|
+
const box = entity.get(traits.Box);
|
|
18
|
+
const worldMatrix = entity.get(traits.WorldMatrix);
|
|
19
|
+
if (!box || !worldMatrix) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
out.copy(worldMatrix);
|
|
23
|
+
const center = entity.get(traits.Center);
|
|
24
|
+
if (center) {
|
|
25
|
+
out.multiply(poseToMatrix(center, centerMatrix));
|
|
26
|
+
}
|
|
27
|
+
out.scale(dimensions.set(box.x * MM_TO_M, box.y * MM_TO_M, box.z * MM_TO_M));
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { Entity } from 'koota';
|
|
2
2
|
import { type IntersectionEvent } from '@threlte/extras';
|
|
3
|
+
/**
|
|
4
|
+
* Pointer handlers for a renderer that draws a single entity — every event
|
|
5
|
+
* targets the closed-over entity.
|
|
6
|
+
*
|
|
7
|
+
* Layers invisibility on top of the shared handlers: enter/move/down/click are
|
|
8
|
+
* suppressed while the entity is invisible (raycasting still hits the visible
|
|
9
|
+
* leaf mesh of Frame/Geometry/GLTF, so the scene's visibility filter can't
|
|
10
|
+
* block them — added in #577, migrated to InheritedInvisible in #710).
|
|
11
|
+
* `onpointerleave` is intentionally left active. The effect tears down a stale
|
|
12
|
+
* Hovered/InstancedMatrix for an entity that turns invisible while hovered,
|
|
13
|
+
* since the guarded handlers can no longer fire to clean it up.
|
|
14
|
+
*/
|
|
3
15
|
export declare const useEntityEvents: (entity: () => Entity | undefined) => {
|
|
4
16
|
onpointerenter: (event: IntersectionEvent<MouseEvent>) => void;
|
|
5
17
|
onpointermove: (event: IntersectionEvent<MouseEvent>) => void;
|
|
@@ -7,3 +19,18 @@ export declare const useEntityEvents: (entity: () => Entity | undefined) => {
|
|
|
7
19
|
onpointerdown: (event: IntersectionEvent<MouseEvent>) => void;
|
|
8
20
|
onclick: (event: IntersectionEvent<MouseEvent>) => void;
|
|
9
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Pointer handlers for an instanced renderer that draws many entities through
|
|
24
|
+
* one object — `entityForEvent` maps each event back to the entity it targets
|
|
25
|
+
* (typically via `event.instanceId`). Threlte keys hover identity by object
|
|
26
|
+
* uuid + instance id, so enter/leave fire per instance with the id on the
|
|
27
|
+
* event. No invisibility watcher: invisible instances are skipped by the
|
|
28
|
+
* instanced raycast, so they never receive events.
|
|
29
|
+
*/
|
|
30
|
+
export declare const useInstancedEntityEvents: (entityForEvent: (event: IntersectionEvent<MouseEvent>) => Entity | undefined) => {
|
|
31
|
+
onpointerenter: (event: IntersectionEvent<MouseEvent>) => void;
|
|
32
|
+
onpointermove: (event: IntersectionEvent<MouseEvent>) => void;
|
|
33
|
+
onpointerleave: (event: IntersectionEvent<MouseEvent>) => void;
|
|
34
|
+
onpointerdown: (event: IntersectionEvent<MouseEvent>) => void;
|
|
35
|
+
onclick: (event: IntersectionEvent<MouseEvent>) => void;
|
|
36
|
+
};
|
|
@@ -12,43 +12,49 @@ const infoToLocalMatrix = (info, out) => {
|
|
|
12
12
|
out.makeRotationFromQuaternion(hoverQuat);
|
|
13
13
|
out.setPosition(info.x, info.y, info.z);
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Shared pointer handlers behind `useEntityEvents` and
|
|
17
|
+
* `useInstancedEntityEvents`. `entityForEvent` maps an event to the entity it
|
|
18
|
+
* targets. No invisibility handling lives here: single-entity renderers layer
|
|
19
|
+
* that on in `useEntityEvents`; instanced renderers don't need it because
|
|
20
|
+
* invisible instances are skipped by the instanced raycast.
|
|
21
|
+
*/
|
|
22
|
+
const createEntityEvents = (entityForEvent, cursor) => {
|
|
16
23
|
const down = new Vector2();
|
|
17
24
|
const world = useWorld();
|
|
18
|
-
const
|
|
19
|
-
|
|
25
|
+
const hoverEntity = (currentEntity, event) => {
|
|
26
|
+
const hoverInfo = updateHoverInfo(currentEntity, event);
|
|
27
|
+
if (hoverInfo) {
|
|
28
|
+
infoToLocalMatrix(hoverInfo, tempHoverMatrix);
|
|
29
|
+
const worldMatrix = currentEntity.get(traits.WorldMatrix);
|
|
30
|
+
const composed = new Matrix4();
|
|
31
|
+
if (worldMatrix) {
|
|
32
|
+
composed.copy(worldMatrix).multiply(tempHoverMatrix);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
composed.copy(tempHoverMatrix);
|
|
36
|
+
}
|
|
37
|
+
currentEntity.add(traits.InstancedMatrix({
|
|
38
|
+
matrix: composed,
|
|
39
|
+
index: hoverInfo.index,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
currentEntity.add(traits.Hovered);
|
|
43
|
+
};
|
|
20
44
|
const onpointerenter = (event) => {
|
|
21
|
-
if (invisible.current)
|
|
22
|
-
return;
|
|
23
45
|
event.stopPropagation();
|
|
24
46
|
cursor.onPointerEnter();
|
|
25
|
-
const currentEntity =
|
|
47
|
+
const currentEntity = entityForEvent(event);
|
|
26
48
|
if (currentEntity && !currentEntity.has(traits.Hovered)) {
|
|
27
|
-
|
|
28
|
-
if (hoverInfo) {
|
|
29
|
-
infoToLocalMatrix(hoverInfo, tempHoverMatrix);
|
|
30
|
-
const worldMatrix = currentEntity.get(traits.WorldMatrix);
|
|
31
|
-
const composed = new Matrix4();
|
|
32
|
-
if (worldMatrix) {
|
|
33
|
-
composed.copy(worldMatrix).multiply(tempHoverMatrix);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
composed.copy(tempHoverMatrix);
|
|
37
|
-
}
|
|
38
|
-
currentEntity.add(traits.InstancedMatrix({
|
|
39
|
-
matrix: composed,
|
|
40
|
-
index: hoverInfo.index,
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
currentEntity.add(traits.Hovered);
|
|
49
|
+
hoverEntity(currentEntity, event);
|
|
44
50
|
}
|
|
45
51
|
};
|
|
46
52
|
const onpointermove = (event) => {
|
|
47
|
-
if (invisible.current)
|
|
48
|
-
return;
|
|
49
53
|
event.stopPropagation();
|
|
50
|
-
const currentEntity =
|
|
51
|
-
if (currentEntity
|
|
54
|
+
const currentEntity = entityForEvent(event);
|
|
55
|
+
if (!currentEntity)
|
|
56
|
+
return;
|
|
57
|
+
if (currentEntity.has(traits.Hovered)) {
|
|
52
58
|
const hoverInfo = updateHoverInfo(currentEntity, event);
|
|
53
59
|
if (!hoverInfo)
|
|
54
60
|
return;
|
|
@@ -66,11 +72,17 @@ export const useEntityEvents = (entity) => {
|
|
|
66
72
|
instanced.index = hoverInfo.index;
|
|
67
73
|
currentEntity.changed(traits.InstancedMatrix);
|
|
68
74
|
}
|
|
75
|
+
else {
|
|
76
|
+
// A move can target an entity that never got an enter event — e.g.
|
|
77
|
+
// an instanced renderer recycled an instance id to a new entity
|
|
78
|
+
// under a motionless cursor — so promote the move to a hover.
|
|
79
|
+
hoverEntity(currentEntity, event);
|
|
80
|
+
}
|
|
69
81
|
};
|
|
70
82
|
const onpointerleave = (event) => {
|
|
71
83
|
event.stopPropagation();
|
|
72
84
|
cursor.onPointerLeave();
|
|
73
|
-
const currentEntity =
|
|
85
|
+
const currentEntity = entityForEvent(event);
|
|
74
86
|
if (currentEntity?.has(traits.Hovered)) {
|
|
75
87
|
currentEntity.remove(traits.Hovered);
|
|
76
88
|
}
|
|
@@ -79,19 +91,14 @@ export const useEntityEvents = (entity) => {
|
|
|
79
91
|
}
|
|
80
92
|
};
|
|
81
93
|
const onpointerdown = (event) => {
|
|
82
|
-
if (invisible.current)
|
|
83
|
-
return;
|
|
84
94
|
down.copy(event.pointer);
|
|
85
95
|
};
|
|
86
96
|
const onclick = (event) => {
|
|
87
|
-
if (invisible.current) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
97
|
event.stopPropagation();
|
|
91
98
|
if (down.distanceToSquared(event.pointer) >= 0.1) {
|
|
92
99
|
return;
|
|
93
100
|
}
|
|
94
|
-
const currentEntity =
|
|
101
|
+
const currentEntity = entityForEvent(event);
|
|
95
102
|
if (!currentEntity)
|
|
96
103
|
return;
|
|
97
104
|
if (event.nativeEvent.shiftKey) {
|
|
@@ -116,6 +123,35 @@ export const useEntityEvents = (entity) => {
|
|
|
116
123
|
currentEntity.add(traits.InstanceId(event.instanceId ?? event.batchId));
|
|
117
124
|
}
|
|
118
125
|
};
|
|
126
|
+
return {
|
|
127
|
+
onpointerenter,
|
|
128
|
+
onpointermove,
|
|
129
|
+
onpointerleave,
|
|
130
|
+
onpointerdown,
|
|
131
|
+
onclick,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Pointer handlers for a renderer that draws a single entity — every event
|
|
136
|
+
* targets the closed-over entity.
|
|
137
|
+
*
|
|
138
|
+
* Layers invisibility on top of the shared handlers: enter/move/down/click are
|
|
139
|
+
* suppressed while the entity is invisible (raycasting still hits the visible
|
|
140
|
+
* leaf mesh of Frame/Geometry/GLTF, so the scene's visibility filter can't
|
|
141
|
+
* block them — added in #577, migrated to InheritedInvisible in #710).
|
|
142
|
+
* `onpointerleave` is intentionally left active. The effect tears down a stale
|
|
143
|
+
* Hovered/InstancedMatrix for an entity that turns invisible while hovered,
|
|
144
|
+
* since the guarded handlers can no longer fire to clean it up.
|
|
145
|
+
*/
|
|
146
|
+
export const useEntityEvents = (entity) => {
|
|
147
|
+
const cursor = useCursor();
|
|
148
|
+
const invisible = useTrait(entity, traits.InheritedInvisible);
|
|
149
|
+
const events = createEntityEvents(entity, cursor);
|
|
150
|
+
const whenVisible = (handler) => (event) => {
|
|
151
|
+
if (invisible.current)
|
|
152
|
+
return;
|
|
153
|
+
handler(event);
|
|
154
|
+
};
|
|
119
155
|
$effect(() => {
|
|
120
156
|
if (invisible.current) {
|
|
121
157
|
cursor.onPointerLeave();
|
|
@@ -129,10 +165,22 @@ export const useEntityEvents = (entity) => {
|
|
|
129
165
|
}
|
|
130
166
|
});
|
|
131
167
|
return {
|
|
132
|
-
onpointerenter,
|
|
133
|
-
onpointermove,
|
|
134
|
-
onpointerleave,
|
|
135
|
-
onpointerdown,
|
|
136
|
-
onclick,
|
|
168
|
+
onpointerenter: whenVisible(events.onpointerenter),
|
|
169
|
+
onpointermove: whenVisible(events.onpointermove),
|
|
170
|
+
onpointerleave: events.onpointerleave,
|
|
171
|
+
onpointerdown: whenVisible(events.onpointerdown),
|
|
172
|
+
onclick: whenVisible(events.onclick),
|
|
137
173
|
};
|
|
138
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Pointer handlers for an instanced renderer that draws many entities through
|
|
177
|
+
* one object — `entityForEvent` maps each event back to the entity it targets
|
|
178
|
+
* (typically via `event.instanceId`). Threlte keys hover identity by object
|
|
179
|
+
* uuid + instance id, so enter/leave fire per instance with the id on the
|
|
180
|
+
* event. No invisibility watcher: invisible instances are skipped by the
|
|
181
|
+
* instanced raycast, so they never receive events.
|
|
182
|
+
*/
|
|
183
|
+
export const useInstancedEntityEvents = (entityForEvent) => {
|
|
184
|
+
const cursor = useCursor();
|
|
185
|
+
return createEntityEvents(entityForEvent, cursor);
|
|
186
|
+
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { T, useTask, useThrelte } from '@threlte/core'
|
|
3
|
-
import { BatchedMesh, Box3 } from 'three'
|
|
3
|
+
import { BatchedMesh, Box3, Matrix4 } from 'three'
|
|
4
4
|
import { OBB } from 'three/addons/math/OBB.js'
|
|
5
5
|
|
|
6
|
+
import { composeBoxMatrix } from './Entities/composeBoxMatrix'
|
|
6
7
|
import { traits, useQuery } from '../ecs'
|
|
7
8
|
import { OBBHelper } from '../three/OBBHelper'
|
|
8
9
|
|
|
9
10
|
const box3 = new Box3()
|
|
10
11
|
const obb = new OBB()
|
|
12
|
+
const boxMatrix = new Matrix4()
|
|
11
13
|
|
|
12
14
|
const { scene, invalidate } = useThrelte()
|
|
13
15
|
const selected = useQuery(traits.Selected)
|
|
@@ -17,12 +19,21 @@
|
|
|
17
19
|
useTask(
|
|
18
20
|
() => {
|
|
19
21
|
for (const [entity, obbHelper] of obbHelpers) {
|
|
22
|
+
/**
|
|
23
|
+
* Boxes render instanced, so the entity's named scene object
|
|
24
|
+
* carries no geometry — derive the OBB straight from traits.
|
|
25
|
+
*/
|
|
26
|
+
if (composeBoxMatrix(entity, boxMatrix)) {
|
|
27
|
+
obbHelper.setFromMatrix4(boxMatrix)
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
const object = scene.getObjectByName(entity as unknown as string)
|
|
21
32
|
if (!object) continue
|
|
22
33
|
|
|
23
34
|
const instance = entity.get(traits.InstanceId)
|
|
24
|
-
if (instance !== undefined && instance >= 0) {
|
|
25
|
-
|
|
35
|
+
if (instance !== undefined && instance >= 0 && object instanceof BatchedMesh) {
|
|
36
|
+
object.getBoundingBoxAt(instance, box3)
|
|
26
37
|
obb.fromBox3(box3)
|
|
27
38
|
obbHelper.setFromOBB(obb)
|
|
28
39
|
} else {
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
import { relations, traits, useQuery, useTrait } from '../ecs'
|
|
9
9
|
import { useTransformControls } from '../hooks/useControls.svelte'
|
|
10
10
|
import { useEnvironment } from '../hooks/useEnvironment.svelte'
|
|
11
|
+
import { useFragmentInfo } from '../hooks/useFragmentInfo.svelte'
|
|
11
12
|
import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
|
|
12
|
-
import { usePartConfig } from '../hooks/usePartConfig.svelte'
|
|
13
13
|
import { useSettings } from '../hooks/useSettings.svelte'
|
|
14
14
|
import { createPose, matrixToPose, poseToMatrix, solveEditedMatrix } from '../transform'
|
|
15
15
|
|
|
16
16
|
const { scene } = useThrelte()
|
|
17
17
|
const settings = useSettings()
|
|
18
18
|
const environment = useEnvironment()
|
|
19
|
-
const
|
|
19
|
+
const fragmentInfo = useFragmentInfo()
|
|
20
20
|
const transformControls = useTransformControls()
|
|
21
21
|
const sessions = useFrameEditSession()
|
|
22
22
|
const selected = useQuery(traits.Selected)
|
|
@@ -36,9 +36,7 @@
|
|
|
36
36
|
box.current !== undefined || sphere.current !== undefined || capsule.current !== undefined
|
|
37
37
|
)
|
|
38
38
|
const isFragmentComponentWithVariables = $derived(
|
|
39
|
-
name.current &&
|
|
40
|
-
Object.keys(partConfig.componentNameToFragmentInfo?.[name.current]?.variables ?? {}).length >
|
|
41
|
-
0
|
|
39
|
+
name.current && Object.keys(fragmentInfo.current?.[name.current]?.variables ?? {}).length > 0
|
|
42
40
|
)
|
|
43
41
|
|
|
44
42
|
// Mesh sets name={entity} on its inner mesh, so getObjectByName resolves
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
import { useConfigFrames } from '../../hooks/useConfigFrames.svelte'
|
|
47
47
|
import { useCameraControls } from '../../hooks/useControls.svelte'
|
|
48
48
|
import { useEnvironment } from '../../hooks/useEnvironment.svelte'
|
|
49
|
+
import { useFragmentInfo } from '../../hooks/useFragmentInfo.svelte'
|
|
49
50
|
import { useLinkedEntities } from '../../hooks/useLinked.svelte'
|
|
50
51
|
import { usePartConfig } from '../../hooks/usePartConfig.svelte'
|
|
51
52
|
import { usePartID } from '../../hooks/usePartID.svelte'
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
const resourceByName = useResourceByName()
|
|
67
68
|
const configFrames = useConfigFrames()
|
|
68
69
|
const partConfig = usePartConfig()
|
|
70
|
+
const fragmentInfo = useFragmentInfo()
|
|
69
71
|
const partID = usePartID()
|
|
70
72
|
const settings = useSettings()
|
|
71
73
|
const environment = useEnvironment()
|
|
@@ -104,9 +106,7 @@
|
|
|
104
106
|
const isFrameNode = $derived(!!framesAPI.current)
|
|
105
107
|
const isGeometry = $derived(!!geometriesAPI.current)
|
|
106
108
|
const isFragmentComponentWithVariables = $derived(
|
|
107
|
-
name.current &&
|
|
108
|
-
Object.keys(partConfig.componentNameToFragmentInfo?.[name.current]?.variables ?? {}).length >
|
|
109
|
-
0
|
|
109
|
+
name.current && Object.keys(fragmentInfo.current?.[name.current]?.variables ?? {}).length > 0
|
|
110
110
|
)
|
|
111
111
|
const showEditFrameOptions = $derived(
|
|
112
112
|
isFrameNode && partConfig.hasEditPermissions && !isFragmentComponentWithVariables
|
|
@@ -435,7 +435,12 @@
|
|
|
435
435
|
</p>
|
|
436
436
|
{/if}
|
|
437
437
|
|
|
438
|
-
<h3
|
|
438
|
+
<h3
|
|
439
|
+
class="text-subtle-2 pt-3 pb-2"
|
|
440
|
+
data-testid="details-header"
|
|
441
|
+
>
|
|
442
|
+
Details
|
|
443
|
+
</h3>
|
|
439
444
|
|
|
440
445
|
<div class="flex flex-col gap-2.5">
|
|
441
446
|
{#if !customDetails.current}
|
|
@@ -25,6 +25,15 @@ export const bvh = (raycaster, options) => {
|
|
|
25
25
|
return;
|
|
26
26
|
if (opts.enabled === false)
|
|
27
27
|
return;
|
|
28
|
+
/**
|
|
29
|
+
* `InstancedMesh2` brings its own per-instance BVH raycast (and a
|
|
30
|
+
* real `bvh` field, so it can't even take a `bvh` opt-out prop —
|
|
31
|
+
* Threlte would assign the prop onto the object and clobber it).
|
|
32
|
+
* Patching it with three-mesh-bvh's `acceleratedRaycast` would
|
|
33
|
+
* test only the unit geometry, so skip it entirely.
|
|
34
|
+
*/
|
|
35
|
+
if (ref.isInstancedMesh2)
|
|
36
|
+
return;
|
|
28
37
|
if (isInstanceOf(ref, 'Points') &&
|
|
29
38
|
/**
|
|
30
39
|
* This check is necessary, there are some strange cases where points are coming in from PCDs without any position data
|
|
@@ -2,11 +2,13 @@ import { Transform } from '@viamrobotics/sdk';
|
|
|
2
2
|
import { getContext, setContext } from 'svelte';
|
|
3
3
|
import { createTransformFromFrame } from '../frame';
|
|
4
4
|
import { useEnvironment } from './useEnvironment.svelte';
|
|
5
|
+
import { useFragmentInfo } from './useFragmentInfo.svelte';
|
|
5
6
|
import { usePartConfig } from './usePartConfig.svelte';
|
|
6
7
|
const key = Symbol('config-frames-context');
|
|
7
8
|
export const provideConfigFrames = () => {
|
|
8
9
|
const environment = useEnvironment();
|
|
9
10
|
const partConfig = usePartConfig();
|
|
11
|
+
const fragmentInfo = useFragmentInfo();
|
|
10
12
|
$effect(() => {
|
|
11
13
|
environment.current.viewerMode = partConfig.isDirty ? 'edit' : 'monitor';
|
|
12
14
|
});
|
|
@@ -25,12 +27,12 @@ export const provideConfigFrames = () => {
|
|
|
25
27
|
});
|
|
26
28
|
const [fragmentFrames, fragmentUnsetFrameNames] = $derived.by(() => {
|
|
27
29
|
const { fragment_mods: fragmentMods = [] } = partConfig.current;
|
|
28
|
-
const fragmentDefinedComponents = Object.keys(
|
|
30
|
+
const fragmentDefinedComponents = Object.keys(fragmentInfo.current ?? {});
|
|
29
31
|
const results = {};
|
|
30
32
|
const unsetResults = [];
|
|
31
33
|
// deal with fragment defined components
|
|
32
34
|
for (const fragmentComponentName of fragmentDefinedComponents || []) {
|
|
33
|
-
const fragmentId =
|
|
35
|
+
const fragmentId = fragmentInfo.current[fragmentComponentName].id;
|
|
34
36
|
const fragmentMod = fragmentMods?.find((mod) => mod.fragment_id === fragmentId);
|
|
35
37
|
if (!fragmentMod) {
|
|
36
38
|
continue;
|
|
@@ -64,7 +66,7 @@ export const provideConfigFrames = () => {
|
|
|
64
66
|
* any whose frame the user has $unset.
|
|
65
67
|
*/
|
|
66
68
|
const unsetFragmentNames = new Set(fragmentUnsetFrameNames);
|
|
67
|
-
for (const name of Object.keys(
|
|
69
|
+
for (const name of Object.keys(fragmentInfo.current)) {
|
|
68
70
|
if (!unsetFragmentNames.has(name)) {
|
|
69
71
|
validFrames.add(name);
|
|
70
72
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Frame } from '../frame';
|
|
2
|
+
export interface FragmentInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
frame?: Frame;
|
|
5
|
+
variables: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
interface FragmentInfoContext {
|
|
8
|
+
/** componentName -> the fragment that defines it ({ id, variables }) */
|
|
9
|
+
current: Record<string, FragmentInfo>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Single source of truth for which components are defined by a fragment.
|
|
13
|
+
*
|
|
14
|
+
* Embedded hosts own this knowledge and pass it as a top-level App prop; in
|
|
15
|
+
* standalone we derive it from the part's `getRobotPart` -> `getFragment`
|
|
16
|
+
* queries. Mode is fixed for the session (the prop is either always defined or
|
|
17
|
+
* always undefined), mirroring `providePartConfig`.
|
|
18
|
+
*
|
|
19
|
+
* Must be provided BEFORE `providePartConfig`, whose frame-edit routing
|
|
20
|
+
* consumes `useFragmentInfo()` to choose part-frame vs fragment-mod writes.
|
|
21
|
+
*/
|
|
22
|
+
export declare const provideFragmentInfo: (partID: () => string, embeddedMap: () => Record<string, FragmentInfo> | undefined) => void;
|
|
23
|
+
export declare const useFragmentInfo: () => FragmentInfoContext;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createAppQuery } from '@viamrobotics/svelte-sdk';
|
|
2
|
+
import { getContext, setContext } from 'svelte';
|
|
3
|
+
const key = Symbol('fragment-info-context');
|
|
4
|
+
/**
|
|
5
|
+
* Single source of truth for which components are defined by a fragment.
|
|
6
|
+
*
|
|
7
|
+
* Embedded hosts own this knowledge and pass it as a top-level App prop; in
|
|
8
|
+
* standalone we derive it from the part's `getRobotPart` -> `getFragment`
|
|
9
|
+
* queries. Mode is fixed for the session (the prop is either always defined or
|
|
10
|
+
* always undefined), mirroring `providePartConfig`.
|
|
11
|
+
*
|
|
12
|
+
* Must be provided BEFORE `providePartConfig`, whose frame-edit routing
|
|
13
|
+
* consumes `useFragmentInfo()` to choose part-frame vs fragment-mod writes.
|
|
14
|
+
*/
|
|
15
|
+
export const provideFragmentInfo = (partID, embeddedMap) => {
|
|
16
|
+
const embedded = $derived(embeddedMap());
|
|
17
|
+
const config = $derived(embedded ? { current: embedded } : useStandaloneFragmentInfo(partID));
|
|
18
|
+
setContext(key, {
|
|
19
|
+
get current() {
|
|
20
|
+
return config.current;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export const useFragmentInfo = () => {
|
|
25
|
+
return getContext(key);
|
|
26
|
+
};
|
|
27
|
+
const useStandaloneFragmentInfo = (partID) => {
|
|
28
|
+
const partQuery = createAppQuery('getRobotPart', () => [partID()], {
|
|
29
|
+
refetchInterval: false,
|
|
30
|
+
});
|
|
31
|
+
const networkPartConfig = $derived(partQuery.data?.part?.robotConfig);
|
|
32
|
+
const configJSON = $derived.by(() => {
|
|
33
|
+
if (!networkPartConfig) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return networkPartConfig.toJson();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const fragmentQueries = $derived((configJSON?.fragments ?? []).map((fragmentId) => {
|
|
44
|
+
const id = typeof fragmentId === 'string' ? fragmentId : fragmentId.id;
|
|
45
|
+
return createAppQuery('getFragment', () => [id], { refetchInterval: false });
|
|
46
|
+
}));
|
|
47
|
+
const fragmentIdToVariables = $derived.by(() => {
|
|
48
|
+
const results = {};
|
|
49
|
+
for (const fragment of configJSON?.fragments ?? []) {
|
|
50
|
+
const id = typeof fragment === 'string' ? fragment : fragment.id;
|
|
51
|
+
const variables = typeof fragment === 'string' ? {} : (fragment.variables ?? {});
|
|
52
|
+
results[id] = variables;
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
});
|
|
56
|
+
const componentNameToFragmentInfo = $derived.by(() => {
|
|
57
|
+
const results = {};
|
|
58
|
+
for (const query of fragmentQueries) {
|
|
59
|
+
if (!query.data) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fragmentId = query.data.id;
|
|
63
|
+
const components = query.data?.fragment?.fields['components']?.kind;
|
|
64
|
+
if (components?.case === 'listValue') {
|
|
65
|
+
for (const component of components.value.values) {
|
|
66
|
+
if (component.kind.case === 'structValue') {
|
|
67
|
+
const componentName = component.kind.value.fields['name']?.kind;
|
|
68
|
+
if (componentName?.case === 'stringValue') {
|
|
69
|
+
results[componentName.value] = {
|
|
70
|
+
id: fragmentId,
|
|
71
|
+
frame: component.kind.value.fields['frame'].toJson(),
|
|
72
|
+
variables: fragmentIdToVariables[fragmentId] ?? {},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
get current() {
|
|
83
|
+
return componentNameToFragmentInfo;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|