@viamrobotics/motion-tools 1.19.1 → 1.22.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/dist/FrameConfigUpdater.svelte.d.ts +0 -1
- package/dist/FrameConfigUpdater.svelte.js +6 -24
- package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
- package/dist/buf/draw/v1/metadata_pb.js +55 -0
- package/dist/buf/draw/v1/service_connect.d.ts +34 -1
- package/dist/buf/draw/v1/service_connect.js +34 -1
- package/dist/buf/draw/v1/service_pb.d.ts +136 -0
- package/dist/buf/draw/v1/service_pb.js +201 -0
- package/dist/components/Entities/Arrows/ArrowGroups.svelte +1 -0
- package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
- package/dist/components/Entities/Points.svelte +23 -23
- package/dist/components/Entities/Pose.svelte +18 -13
- package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
- package/dist/components/FileDrop/FileDrop.svelte +8 -1
- package/dist/components/FileDrop/useFileDrop.svelte.js +16 -2
- package/dist/components/PCD.svelte +9 -1
- package/dist/components/PCD.svelte.d.ts +2 -0
- package/dist/components/PointerMissBox.svelte +1 -1
- package/dist/components/Scene.svelte +2 -0
- package/dist/components/SceneProviders.svelte +4 -0
- package/dist/components/SelectedTransformControls.svelte +227 -0
- package/dist/components/SelectedTransformControls.svelte.d.ts +3 -0
- package/dist/components/Snapshot.svelte +12 -7
- package/dist/components/StaticGeometries.svelte +3 -56
- package/dist/components/overlay/AddRelationship.svelte +25 -3
- package/dist/components/overlay/Details.svelte +290 -229
- package/dist/components/overlay/dashboard/Button.svelte +4 -2
- package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
- package/dist/components/overlay/dashboard/Dashboard.svelte +43 -33
- package/dist/draw.d.ts +22 -9
- package/dist/draw.js +71 -41
- package/dist/ecs/relations.js +1 -1
- package/dist/ecs/traits.d.ts +17 -0
- package/dist/ecs/traits.js +9 -0
- package/dist/editing/FrameEditSession.d.ts +37 -0
- package/dist/editing/FrameEditSession.js +178 -0
- package/dist/hooks/useDrawService.svelte.d.ts +2 -0
- package/dist/hooks/useDrawService.svelte.js +139 -20
- package/dist/hooks/useFrameEditSession.svelte.d.ts +15 -0
- package/dist/hooks/useFrameEditSession.svelte.js +36 -0
- package/dist/hooks/useFrames.svelte.js +37 -2
- package/dist/hooks/usePartConfig.svelte.js +10 -0
- package/dist/hooks/useRelationships.svelte.d.ts +12 -0
- package/dist/hooks/useRelationships.svelte.js +78 -0
- package/dist/hooks/useSettings.svelte.d.ts +1 -2
- package/dist/hooks/useSettings.svelte.js +1 -2
- package/dist/hooks/useWorldState.svelte.js +10 -4
- package/dist/metadata.d.ts +7 -3
- package/dist/metadata.js +26 -2
- package/dist/snapshot.d.ts +6 -1
- package/dist/snapshot.js +10 -5
- package/dist/transform.js +13 -0
- package/package.json +7 -4
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
name={entity}
|
|
44
44
|
{...events}
|
|
45
45
|
raycast={raycastFunction}
|
|
46
|
+
visible={invisible.current !== true}
|
|
46
47
|
>
|
|
47
48
|
<T
|
|
48
49
|
is={arrows.headMesh}
|
|
49
50
|
bvh={{ enabled: false }}
|
|
50
51
|
raycast={() => null}
|
|
51
|
-
visible={invisible.current !== true}
|
|
52
52
|
/>
|
|
53
53
|
<T
|
|
54
54
|
is={arrows.shaftMesh}
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
const opacity = useTrait(() => entity, traits.Opacity)
|
|
34
34
|
const invisible = useTrait(() => entity, traits.Invisible)
|
|
35
35
|
const showAxesHelper = useTrait(() => entity, traits.ShowAxesHelper)
|
|
36
|
+
const renderOrder = useTrait(() => entity, traits.RenderOrder)
|
|
37
|
+
const materialProps = useTrait(() => entity, traits.Material)
|
|
36
38
|
|
|
37
39
|
const pointSize = $derived(
|
|
38
40
|
entityPointSize.current ? entityPointSize.current * 0.001 : settings.current.pointSize
|
|
@@ -61,41 +63,38 @@
|
|
|
61
63
|
})
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
|
-
* Points
|
|
66
|
+
* Points transparency is very costly for the GPU, so we turn it on conservatively.
|
|
67
|
+
* Uniform opacity (entity trait) and per-vertex RGBA alpha are both considered here
|
|
68
|
+
* to avoid the two sources conflicting with each other.
|
|
65
69
|
*/
|
|
66
70
|
$effect.pre(() => {
|
|
67
|
-
|
|
68
|
-
material.transparent = true
|
|
69
|
-
material.opacity = opacity.current
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
material.transparent = false
|
|
73
|
-
material.opacity = 1
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
$effect.pre(() => {
|
|
79
|
-
const colors = geometry.current?.getAttribute('color')
|
|
71
|
+
const vertexColors = geometry.current?.getAttribute('color')
|
|
80
72
|
const positions = geometry.current?.getAttribute('position')
|
|
81
73
|
|
|
82
|
-
material.vertexColors =
|
|
74
|
+
material.vertexColors = vertexColors !== undefined
|
|
83
75
|
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
const hasUniformOpacity = opacity.current !== undefined && opacity.current < 1
|
|
77
|
+
material.opacity = hasUniformOpacity ? opacity.current! : 1
|
|
86
78
|
|
|
87
|
-
|
|
79
|
+
let hasVertexAlpha = false
|
|
80
|
+
if (vertexColors && positions) {
|
|
81
|
+
const hasAlphaChannel = positions.array.length / vertexColors.array.length === 0.75
|
|
88
82
|
if (hasAlphaChannel) {
|
|
89
|
-
for (let i = 3, l =
|
|
90
|
-
if (
|
|
91
|
-
|
|
83
|
+
for (let i = 3, l = vertexColors.array.length; i < l; i += 4) {
|
|
84
|
+
if (vertexColors.array[i] < 1) {
|
|
85
|
+
hasVertexAlpha = true
|
|
92
86
|
break
|
|
93
87
|
}
|
|
94
88
|
}
|
|
95
89
|
}
|
|
96
|
-
|
|
97
|
-
material.transparent = transparent
|
|
98
90
|
}
|
|
91
|
+
|
|
92
|
+
material.transparent = hasUniformOpacity || hasVertexAlpha
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
$effect.pre(() => {
|
|
96
|
+
material.depthTest = materialProps.current?.depthTest ?? true
|
|
97
|
+
material.depthWrite = materialProps.current?.depthWrite ?? true
|
|
99
98
|
})
|
|
100
99
|
|
|
101
100
|
$effect.pre(() => {
|
|
@@ -132,6 +131,7 @@
|
|
|
132
131
|
name={entity}
|
|
133
132
|
bvh={{ maxDepth: 40, maxLeafSize: 20 }}
|
|
134
133
|
visible={invisible.current !== true}
|
|
134
|
+
renderOrder={renderOrder.current}
|
|
135
135
|
{...events}
|
|
136
136
|
>
|
|
137
137
|
<T is={geometry.current} />
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { traits, useTrait } from '../../ecs'
|
|
7
7
|
import { usePartConfig } from '../../hooks/usePartConfig.svelte'
|
|
8
8
|
import { usePose } from '../../hooks/usePose.svelte'
|
|
9
|
-
import {
|
|
9
|
+
import { composeRenderedPose } from '../../transform'
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
entity: Entity
|
|
@@ -25,22 +25,27 @@
|
|
|
25
25
|
() => parent.current
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
if (pose.current === undefined
|
|
30
|
-
return editedPose.current
|
|
31
|
-
}
|
|
28
|
+
$effect.pre(() => {
|
|
29
|
+
if (pose.current === undefined) return
|
|
32
30
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
31
|
+
if (entity.has(traits.LivePose)) {
|
|
32
|
+
entity.set(traits.LivePose, pose.current)
|
|
33
|
+
} else {
|
|
34
|
+
entity.add(traits.LivePose(pose.current))
|
|
35
35
|
}
|
|
36
|
+
})
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Always render through the live blend: live × network⁻¹ × edited. With
|
|
39
|
+
// `edited === network` (no edits) this collapses to `live`, so the rendered
|
|
40
|
+
// pose tracks the robot's kinematics-resolved position. With edits, the
|
|
41
|
+
// formula composes the staged delta on top of live. Input handlers that
|
|
42
|
+
// drive edits (gizmo onChange, Details panel) compute `edited` such that
|
|
43
|
+
// the blend renders to the user's intent.
|
|
44
|
+
const resolvedPose = $derived.by(() => {
|
|
45
|
+
if (pose.current === undefined || partConfig.hasPendingSave) return editedPose.current
|
|
46
|
+
if (!entityPose.current || !editedPose.current) return undefined
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
const resultMatrix = poseUsePose.multiply(poseNetworkInverse).multiply(poseLocalEditedPose)
|
|
43
|
-
return matrixToPose(resultMatrix)
|
|
48
|
+
return composeRenderedPose(pose.current, entityPose.current, editedPose.current)
|
|
44
49
|
})
|
|
45
50
|
</script>
|
|
46
51
|
|
|
@@ -9,7 +9,10 @@ export const useEntityEvents = (entity) => {
|
|
|
9
9
|
const selectedEntity = useSelectedEntity();
|
|
10
10
|
const focusedEntity = useFocusedEntity();
|
|
11
11
|
const cursor = useCursor();
|
|
12
|
+
const invisible = useTrait(entity, traits.Invisible);
|
|
12
13
|
const onpointerenter = (event) => {
|
|
14
|
+
if (invisible.current)
|
|
15
|
+
return;
|
|
13
16
|
event.stopPropagation();
|
|
14
17
|
cursor.onPointerEnter();
|
|
15
18
|
const currentEntity = entity();
|
|
@@ -31,6 +34,8 @@ export const useEntityEvents = (entity) => {
|
|
|
31
34
|
}
|
|
32
35
|
};
|
|
33
36
|
const onpointermove = (event) => {
|
|
37
|
+
if (invisible.current)
|
|
38
|
+
return;
|
|
34
39
|
event.stopPropagation();
|
|
35
40
|
const currentEntity = entity();
|
|
36
41
|
if (currentEntity?.has(traits.Hovered)) {
|
|
@@ -77,24 +82,36 @@ export const useEntityEvents = (entity) => {
|
|
|
77
82
|
}
|
|
78
83
|
};
|
|
79
84
|
const ondblclick = (event) => {
|
|
85
|
+
if (invisible.current)
|
|
86
|
+
return;
|
|
80
87
|
event.stopPropagation();
|
|
81
88
|
const currentEntity = entity();
|
|
82
89
|
focusedEntity.set(currentEntity, event.instanceId ?? event.batchId);
|
|
83
90
|
};
|
|
84
91
|
const onpointerdown = (event) => {
|
|
92
|
+
if (invisible.current)
|
|
93
|
+
return;
|
|
85
94
|
down.copy(event.pointer);
|
|
86
95
|
};
|
|
87
96
|
const onclick = (event) => {
|
|
97
|
+
if (invisible.current)
|
|
98
|
+
return;
|
|
88
99
|
event.stopPropagation();
|
|
89
100
|
if (down.distanceToSquared(event.pointer) < 0.1) {
|
|
90
101
|
const currentEntity = entity();
|
|
91
102
|
selectedEntity.set(currentEntity, event.instanceId ?? event.batchId);
|
|
92
103
|
}
|
|
93
104
|
};
|
|
94
|
-
const invisible = useTrait(entity, traits.Invisible);
|
|
95
105
|
$effect(() => {
|
|
96
106
|
if (invisible.current) {
|
|
97
107
|
cursor.onPointerLeave();
|
|
108
|
+
const currentEntity = entity();
|
|
109
|
+
if (currentEntity?.has(traits.Hovered)) {
|
|
110
|
+
currentEntity.remove(traits.Hovered);
|
|
111
|
+
}
|
|
112
|
+
if (currentEntity?.has(traits.InstancedPose)) {
|
|
113
|
+
currentEntity.remove(traits.InstancedPose);
|
|
114
|
+
}
|
|
98
115
|
}
|
|
99
116
|
});
|
|
100
117
|
return {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { traits } from '../../ecs'
|
|
9
9
|
import { useWorld } from '../../ecs/useWorld'
|
|
10
10
|
import { useCameraControls } from '../../hooks/useControls.svelte'
|
|
11
|
+
import { useRelationships } from '../../hooks/useRelationships.svelte'
|
|
11
12
|
import { spawnSnapshotEntities } from '../../snapshot'
|
|
12
13
|
|
|
13
14
|
import type { FileDropperSuccess } from './file-dropper'
|
|
@@ -19,12 +20,18 @@
|
|
|
19
20
|
const world = useWorld()
|
|
20
21
|
const toast = useToast()
|
|
21
22
|
const cameraControls = useCameraControls()
|
|
23
|
+
const relationships = useRelationships()
|
|
22
24
|
|
|
23
25
|
const fileDrop = useFileDrop(
|
|
24
26
|
(result: FileDropperSuccess) => {
|
|
25
27
|
switch (result.type) {
|
|
26
28
|
case 'snapshot': {
|
|
27
|
-
spawnSnapshotEntities(world, result.snapshot)
|
|
29
|
+
const spawned = spawnSnapshotEntities(world, result.snapshot)
|
|
30
|
+
for (const entity of spawned) {
|
|
31
|
+
relationships.apply(entity.entity, entity.relationships)
|
|
32
|
+
const uuid = entity.entity.get(traits.UUID)
|
|
33
|
+
if (uuid) relationships.flush(uuid)
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
const { sceneCamera } = result.snapshot.sceneMetadata ?? {}
|
|
30
37
|
|
|
@@ -2,6 +2,9 @@ import { Extensions, parseFileName, Prefixes, readFile } from './file-names';
|
|
|
2
2
|
import { pcdDropper } from './pcd-dropper';
|
|
3
3
|
import { plyDropper } from './ply-dropper';
|
|
4
4
|
import { snapshotDropper } from './snapshot-dropper';
|
|
5
|
+
const hasDraggedFiles = (dataTransfer) => {
|
|
6
|
+
return dataTransfer?.types?.includes('Files') ?? false;
|
|
7
|
+
};
|
|
5
8
|
const createFileDropper = (extension, prefix) => {
|
|
6
9
|
switch (prefix) {
|
|
7
10
|
case Prefixes.Snapshot: {
|
|
@@ -22,11 +25,15 @@ export const useFileDrop = (onSuccess, onError) => {
|
|
|
22
25
|
let dropState = $state('inactive');
|
|
23
26
|
// prevent default to allow drop
|
|
24
27
|
const ondragenter = (event) => {
|
|
28
|
+
if (!hasDraggedFiles(event.dataTransfer))
|
|
29
|
+
return;
|
|
25
30
|
event.preventDefault();
|
|
26
31
|
dropState = 'hovering';
|
|
27
32
|
};
|
|
28
33
|
// prevent default to allow drop
|
|
29
34
|
const ondragover = (event) => {
|
|
35
|
+
if (!hasDraggedFiles(event.dataTransfer))
|
|
36
|
+
return;
|
|
30
37
|
event.preventDefault();
|
|
31
38
|
};
|
|
32
39
|
const ondragleave = (event) => {
|
|
@@ -40,10 +47,17 @@ export const useFileDrop = (onSuccess, onError) => {
|
|
|
40
47
|
dropState = 'inactive';
|
|
41
48
|
};
|
|
42
49
|
const ondrop = (event) => {
|
|
50
|
+
const { dataTransfer } = event;
|
|
51
|
+
if (dataTransfer === null || !hasDraggedFiles(dataTransfer)) {
|
|
52
|
+
dropState = 'inactive';
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
43
55
|
event.preventDefault();
|
|
44
|
-
|
|
56
|
+
const { files } = dataTransfer;
|
|
57
|
+
if (files.length === 0) {
|
|
58
|
+
dropState = 'inactive';
|
|
45
59
|
return;
|
|
46
|
-
|
|
60
|
+
}
|
|
47
61
|
let completed = 0;
|
|
48
62
|
for (const file of files) {
|
|
49
63
|
const fileName = parseFileName(file.name);
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
data: Uint8Array
|
|
13
13
|
name?: string
|
|
14
14
|
renderOrder?: number
|
|
15
|
+
depthTest?: boolean
|
|
16
|
+
depthWrite?: boolean
|
|
15
17
|
interactionLayers?: InteractionLayerValue[]
|
|
16
18
|
oncreate?: (positions: Float32Array, colors: Uint8Array | undefined) => void
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
let { data, name, renderOrder, interactionLayers, oncreate }: Props =
|
|
21
|
+
let { data, name, renderOrder, depthTest, depthWrite, interactionLayers, oncreate }: Props =
|
|
22
|
+
$props()
|
|
20
23
|
|
|
21
24
|
const world = useWorld()
|
|
22
25
|
|
|
@@ -38,6 +41,11 @@
|
|
|
38
41
|
if (renderOrder) {
|
|
39
42
|
entityTraits.push(traits.RenderOrder(renderOrder))
|
|
40
43
|
}
|
|
44
|
+
if (depthTest !== undefined || depthWrite !== undefined) {
|
|
45
|
+
entityTraits.push(
|
|
46
|
+
traits.Material({ depthTest: depthTest ?? true, depthWrite: depthWrite ?? true })
|
|
47
|
+
)
|
|
48
|
+
}
|
|
41
49
|
if (interactionLayers?.includes('selectTool')) {
|
|
42
50
|
entityTraits.push(traits.SelectToolInteractionLayer)
|
|
43
51
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import Entities from './Entities/Entities.svelte'
|
|
11
11
|
import Focus from './Focus.svelte'
|
|
12
12
|
import Selected from './Selected.svelte'
|
|
13
|
+
import SelectedTransformControls from './SelectedTransformControls.svelte'
|
|
13
14
|
import StaticGeometries from './StaticGeometries.svelte'
|
|
14
15
|
import { useFocusedObject3d } from '../hooks/useSelection.svelte'
|
|
15
16
|
import { useSettings } from '../hooks/useSettings.svelte'
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
|
|
78
79
|
<StaticGeometries />
|
|
79
80
|
<Selected />
|
|
81
|
+
<SelectedTransformControls />
|
|
80
82
|
|
|
81
83
|
{#if !$isPresenting && settings.current.grid}
|
|
82
84
|
<Grid
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
} from '../hooks/useControls.svelte'
|
|
13
13
|
import { provideDrawAPI } from '../hooks/useDrawAPI.svelte'
|
|
14
14
|
import { provideDrawService } from '../hooks/useDrawService.svelte'
|
|
15
|
+
import { provideFrameEditSession } from '../hooks/useFrameEditSession.svelte'
|
|
15
16
|
import { provideFramelessComponents } from '../hooks/useFramelessComponents.svelte'
|
|
16
17
|
import { provideFrames } from '../hooks/useFrames.svelte'
|
|
17
18
|
import { provideGeometries } from '../hooks/useGeometries.svelte'
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
import { usePartID } from '../hooks/usePartID.svelte'
|
|
21
22
|
import { providePointcloudObjects } from '../hooks/usePointcloudObjects.svelte'
|
|
22
23
|
import { providePointclouds } from '../hooks/usePointclouds.svelte'
|
|
24
|
+
import { provideRelationships } from '../hooks/useRelationships.svelte'
|
|
23
25
|
import { provideResourceByName } from '../hooks/useResourceByName.svelte'
|
|
24
26
|
import { provideSelection } from '../hooks/useSelection.svelte'
|
|
25
27
|
import { provideWorldStates } from '../hooks/useWorldState.svelte'
|
|
@@ -41,10 +43,12 @@
|
|
|
41
43
|
|
|
42
44
|
provideOrigin()
|
|
43
45
|
provideDrawAPI()
|
|
46
|
+
provideRelationships()
|
|
44
47
|
provideDrawService()
|
|
45
48
|
|
|
46
49
|
provideResourceByName(() => partID.current)
|
|
47
50
|
provideConfigFrames()
|
|
51
|
+
provideFrameEditSession(() => partID.current)
|
|
48
52
|
provideFrames(() => partID.current)
|
|
49
53
|
provideGeometries(() => partID.current)
|
|
50
54
|
provide3DModels(() => partID.current)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TransformControls } from '@threlte/extras'
|
|
3
|
+
import { Quaternion, Vector3 } from 'three'
|
|
4
|
+
|
|
5
|
+
import type { FrameEditSession } from '../editing/FrameEditSession'
|
|
6
|
+
|
|
7
|
+
import { traits, useTrait } from '../ecs'
|
|
8
|
+
import { useTransformControls } from '../hooks/useControls.svelte'
|
|
9
|
+
import { useEnvironment } from '../hooks/useEnvironment.svelte'
|
|
10
|
+
import { useFrameEditSession } from '../hooks/useFrameEditSession.svelte'
|
|
11
|
+
import { useSelectedEntity, useSelectedObject3d } from '../hooks/useSelection.svelte'
|
|
12
|
+
import { useSettings } from '../hooks/useSettings.svelte'
|
|
13
|
+
import {
|
|
14
|
+
composeEditedPoseForRenderedPose,
|
|
15
|
+
createPose,
|
|
16
|
+
quaternionToPose,
|
|
17
|
+
vector3ToPose,
|
|
18
|
+
} from '../transform'
|
|
19
|
+
|
|
20
|
+
const settings = useSettings()
|
|
21
|
+
const environment = useEnvironment()
|
|
22
|
+
const transformControls = useTransformControls()
|
|
23
|
+
const selectedEntity = useSelectedEntity()
|
|
24
|
+
const selectedObject3d = useSelectedObject3d()
|
|
25
|
+
const sessions = useFrameEditSession()
|
|
26
|
+
|
|
27
|
+
const mode = $derived(settings.current.transformMode)
|
|
28
|
+
const entity = $derived(selectedEntity.current)
|
|
29
|
+
const transformable = useTrait(() => entity, traits.Transformable)
|
|
30
|
+
const networkPose = useTrait(() => entity, traits.Pose)
|
|
31
|
+
const livePose = useTrait(() => entity, traits.LivePose)
|
|
32
|
+
const box = useTrait(() => entity, traits.Box)
|
|
33
|
+
const sphere = useTrait(() => entity, traits.Sphere)
|
|
34
|
+
const capsule = useTrait(() => entity, traits.Capsule)
|
|
35
|
+
const hasScalableGeometry = $derived(
|
|
36
|
+
box.current !== undefined || sphere.current !== undefined || capsule.current !== undefined
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Mesh sets name={entity} on its inner mesh, so useSelectedObject3d resolves
|
|
40
|
+
// to that mesh — not the parent Frame Group we actually want to drive. Walk
|
|
41
|
+
// up to the Group so translate/rotate/scale apply to the whole frame, not
|
|
42
|
+
// the geometry inside it.
|
|
43
|
+
const ref = $derived(selectedObject3d.current?.parent ?? selectedObject3d.current)
|
|
44
|
+
|
|
45
|
+
const activeMode = $derived.by(() => {
|
|
46
|
+
if (mode === 'none' || !transformable.current) return undefined
|
|
47
|
+
// Scale only does anything for primitive geometries the gizmo can size.
|
|
48
|
+
if (mode === 'scale' && !hasScalableGeometry) return undefined
|
|
49
|
+
return mode
|
|
50
|
+
})
|
|
51
|
+
const isSphereScale = $derived(activeMode === 'scale' && sphere.current !== undefined)
|
|
52
|
+
const isCapsuleScale = $derived(activeMode === 'scale' && capsule.current !== undefined)
|
|
53
|
+
|
|
54
|
+
const quaternion = new Quaternion()
|
|
55
|
+
const vector3 = new Vector3()
|
|
56
|
+
const refPose = createPose()
|
|
57
|
+
|
|
58
|
+
let session: FrameEditSession | undefined
|
|
59
|
+
let scaleStart:
|
|
60
|
+
| { type: 'box'; x: number; y: number; z: number }
|
|
61
|
+
| { type: 'sphere'; r: number }
|
|
62
|
+
| { type: 'capsule'; r: number; l: number }
|
|
63
|
+
| undefined
|
|
64
|
+
|
|
65
|
+
const captureScaleStart = () => {
|
|
66
|
+
if (!entity || activeMode !== 'scale') {
|
|
67
|
+
scaleStart = undefined
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const box = entity.get(traits.Box)
|
|
72
|
+
if (box) {
|
|
73
|
+
scaleStart = { type: 'box', ...box }
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const sphere = entity.get(traits.Sphere)
|
|
78
|
+
if (sphere) {
|
|
79
|
+
scaleStart = { type: 'sphere', ...sphere }
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const capsule = entity.get(traits.Capsule)
|
|
84
|
+
if (capsule) {
|
|
85
|
+
scaleStart = { type: 'capsule', ...capsule }
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
scaleStart = undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const onMouseDown = () => {
|
|
93
|
+
if (entity?.has(traits.FramesAPI)) {
|
|
94
|
+
session = sessions.begin([entity])
|
|
95
|
+
}
|
|
96
|
+
captureScaleStart()
|
|
97
|
+
environment.current.viewerMode = 'edit'
|
|
98
|
+
transformControls.setActive(true)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const onChange = () => {
|
|
102
|
+
if (!ref || !entity || !activeMode) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isFrameEntity = entity.has(traits.FramesAPI)
|
|
107
|
+
|
|
108
|
+
if (activeMode === 'translate' || activeMode === 'rotate') {
|
|
109
|
+
if (isFrameEntity) {
|
|
110
|
+
stageFrameTransform()
|
|
111
|
+
} else {
|
|
112
|
+
const pose = entity.get(traits.Pose)
|
|
113
|
+
if (pose) {
|
|
114
|
+
if (activeMode === 'translate') {
|
|
115
|
+
vector3ToPose(ref.getWorldPosition(vector3), pose)
|
|
116
|
+
} else {
|
|
117
|
+
quaternionToPose(ref.getWorldQuaternion(quaternion), pose)
|
|
118
|
+
ref.quaternion.copy(quaternion)
|
|
119
|
+
}
|
|
120
|
+
entity.set(traits.Pose, pose)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// scale → bake the gizmo's scale factor into the geometry trait,
|
|
125
|
+
// then reset the object's scale so subsequent drags start from 1.
|
|
126
|
+
if (!scaleStart) {
|
|
127
|
+
captureScaleStart()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (scaleStart?.type === 'box') {
|
|
131
|
+
const next = {
|
|
132
|
+
x: scaleStart.x * ref.scale.x,
|
|
133
|
+
y: scaleStart.y * ref.scale.y,
|
|
134
|
+
z: scaleStart.z * ref.scale.z,
|
|
135
|
+
}
|
|
136
|
+
if (isFrameEntity) {
|
|
137
|
+
session?.stageGeometry(entity, { type: 'box', ...next })
|
|
138
|
+
} else {
|
|
139
|
+
entity.set(traits.Box, next)
|
|
140
|
+
}
|
|
141
|
+
} else if (scaleStart?.type === 'sphere') {
|
|
142
|
+
const next = { r: scaleStart.r * ref.scale.x }
|
|
143
|
+
if (isFrameEntity) {
|
|
144
|
+
session?.stageGeometry(entity, { type: 'sphere', ...next })
|
|
145
|
+
} else {
|
|
146
|
+
entity.set(traits.Sphere, next)
|
|
147
|
+
}
|
|
148
|
+
} else if (scaleStart?.type === 'capsule') {
|
|
149
|
+
const next = { r: scaleStart.r * ref.scale.x, l: scaleStart.l * ref.scale.y }
|
|
150
|
+
if (isFrameEntity) {
|
|
151
|
+
session?.stageGeometry(entity, { type: 'capsule', ...next })
|
|
152
|
+
} else {
|
|
153
|
+
entity.set(traits.Capsule, next)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ref.scale.setScalar(1)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const onMouseUp = () => {
|
|
162
|
+
session?.commit()
|
|
163
|
+
session = undefined
|
|
164
|
+
scaleStart = undefined
|
|
165
|
+
transformControls.setActive(false)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Pose.svelte renders frame entities through the live blend
|
|
169
|
+
// render = M(live) × M(network)⁻¹ × M(edited)
|
|
170
|
+
// so for the user's drag to render where they pulled the gizmo to, EditedPose
|
|
171
|
+
// must satisfy
|
|
172
|
+
// M(edited) = M(network) × M(live)⁻¹ × M(ref)
|
|
173
|
+
// where M(ref) is the gizmo-driven group's parent-relative matrix in mm.
|
|
174
|
+
// When live ≈ network (no kinematic offset), this collapses to M(edited) =
|
|
175
|
+
// M(ref) — the same as the naive writeback. When they diverge (e.g. an arm
|
|
176
|
+
// whose joints have moved away from its config pose), this composition is
|
|
177
|
+
// what keeps the rendering anchored to the user's pointer instead of
|
|
178
|
+
// shearing through the live × baseline⁻¹ offset.
|
|
179
|
+
const stageFrameTransform = () => {
|
|
180
|
+
if (!ref || !entity) return
|
|
181
|
+
|
|
182
|
+
vector3ToPose(ref.position, refPose)
|
|
183
|
+
quaternionToPose(ref.quaternion, refPose)
|
|
184
|
+
|
|
185
|
+
const live = livePose.current
|
|
186
|
+
const network = networkPose.current
|
|
187
|
+
|
|
188
|
+
if (!live || !network) {
|
|
189
|
+
// No live pose available — Pose.svelte's blend short-circuits to
|
|
190
|
+
// editedPose, so naive writeback is correct.
|
|
191
|
+
if (activeMode === 'translate') {
|
|
192
|
+
session?.stagePose(entity, {
|
|
193
|
+
x: refPose.x,
|
|
194
|
+
y: refPose.y,
|
|
195
|
+
z: refPose.z,
|
|
196
|
+
})
|
|
197
|
+
} else if (activeMode === 'rotate') {
|
|
198
|
+
session?.stagePose(entity, {
|
|
199
|
+
oX: refPose.oX,
|
|
200
|
+
oY: refPose.oY,
|
|
201
|
+
oZ: refPose.oZ,
|
|
202
|
+
theta: refPose.theta,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
session?.stagePose(entity, composeEditedPoseForRenderedPose(network, live, refPose))
|
|
209
|
+
}
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
{#if ref && entity && activeMode}
|
|
213
|
+
{#key entity}
|
|
214
|
+
<TransformControls
|
|
215
|
+
object={ref}
|
|
216
|
+
mode={activeMode}
|
|
217
|
+
translationSnap={settings.current.snapping ? 0.1 : undefined}
|
|
218
|
+
rotationSnap={settings.current.snapping ? Math.PI / 24 : undefined}
|
|
219
|
+
scaleSnap={settings.current.snapping ? 0.1 : undefined}
|
|
220
|
+
showY={!isSphereScale}
|
|
221
|
+
showZ={!isSphereScale && !isCapsuleScale}
|
|
222
|
+
onmouseDown={onMouseDown}
|
|
223
|
+
onobjectChange={onChange}
|
|
224
|
+
onmouseUp={onMouseUp}
|
|
225
|
+
/>
|
|
226
|
+
{/key}
|
|
227
|
+
{/if}
|
|
@@ -14,17 +14,16 @@ Renders a Snapshot protobuf by spawning its transforms and drawings as entities
|
|
|
14
14
|
```
|
|
15
15
|
-->
|
|
16
16
|
<script lang="ts">
|
|
17
|
-
import type { Entity } from 'koota'
|
|
18
|
-
|
|
19
17
|
import { untrack } from 'svelte'
|
|
20
18
|
import { onDestroy } from 'svelte'
|
|
21
19
|
|
|
22
20
|
import type { Snapshot as SnapshotProto } from '../buf/draw/v1/snapshot_pb'
|
|
23
21
|
|
|
24
|
-
import { useWorld } from '../ecs'
|
|
22
|
+
import { traits, useWorld } from '../ecs'
|
|
25
23
|
import { useCameraControls } from '../hooks/useControls.svelte'
|
|
24
|
+
import { useRelationships } from '../hooks/useRelationships.svelte'
|
|
26
25
|
import { useSettings } from '../hooks/useSettings.svelte'
|
|
27
|
-
import { applySceneMetadata, spawnSnapshotEntities } from '../snapshot'
|
|
26
|
+
import { applySceneMetadata, type SnapshotEntity, spawnSnapshotEntities } from '../snapshot'
|
|
28
27
|
|
|
29
28
|
interface Props {
|
|
30
29
|
snapshot: SnapshotProto
|
|
@@ -35,8 +34,9 @@ Renders a Snapshot protobuf by spawning its transforms and drawings as entities
|
|
|
35
34
|
const world = useWorld()
|
|
36
35
|
const settings = useSettings()
|
|
37
36
|
const cameraControls = useCameraControls()
|
|
37
|
+
const relationships = useRelationships()
|
|
38
38
|
|
|
39
|
-
let entities:
|
|
39
|
+
let entities: SnapshotEntity[] = []
|
|
40
40
|
|
|
41
41
|
$effect(() => {
|
|
42
42
|
world.id.toString()
|
|
@@ -44,6 +44,11 @@ Renders a Snapshot protobuf by spawning its transforms and drawings as entities
|
|
|
44
44
|
|
|
45
45
|
untrack(() => {
|
|
46
46
|
entities = spawnSnapshotEntities(world, snapshot)
|
|
47
|
+
for (const spawned of entities) {
|
|
48
|
+
relationships.apply(spawned.entity, spawned.relationships)
|
|
49
|
+
const uuid = spawned.entity.get(traits.UUID)
|
|
50
|
+
if (uuid) relationships.flush(uuid)
|
|
51
|
+
}
|
|
47
52
|
})
|
|
48
53
|
})
|
|
49
54
|
|
|
@@ -78,8 +83,8 @@ Renders a Snapshot protobuf by spawning its transforms and drawings as entities
|
|
|
78
83
|
})
|
|
79
84
|
|
|
80
85
|
onDestroy(() => {
|
|
81
|
-
for (const
|
|
82
|
-
if (world.has(entity)) entity.destroy()
|
|
86
|
+
for (const spawned of entities) {
|
|
87
|
+
if (world.has(spawned.entity)) spawned.entity.destroy()
|
|
83
88
|
}
|
|
84
89
|
})
|
|
85
90
|
</script>
|