@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.
Files changed (53) hide show
  1. package/dist/FrameConfigUpdater.svelte.d.ts +0 -1
  2. package/dist/FrameConfigUpdater.svelte.js +6 -24
  3. package/dist/buf/draw/v1/metadata_pb.d.ts +39 -0
  4. package/dist/buf/draw/v1/metadata_pb.js +55 -0
  5. package/dist/buf/draw/v1/service_connect.d.ts +34 -1
  6. package/dist/buf/draw/v1/service_connect.js +34 -1
  7. package/dist/buf/draw/v1/service_pb.d.ts +136 -0
  8. package/dist/buf/draw/v1/service_pb.js +201 -0
  9. package/dist/components/Entities/Arrows/ArrowGroups.svelte +1 -0
  10. package/dist/components/Entities/Arrows/Arrows.svelte +1 -1
  11. package/dist/components/Entities/Points.svelte +23 -23
  12. package/dist/components/Entities/Pose.svelte +18 -13
  13. package/dist/components/Entities/hooks/useEntityEvents.svelte.js +18 -1
  14. package/dist/components/FileDrop/FileDrop.svelte +8 -1
  15. package/dist/components/FileDrop/useFileDrop.svelte.js +16 -2
  16. package/dist/components/PCD.svelte +9 -1
  17. package/dist/components/PCD.svelte.d.ts +2 -0
  18. package/dist/components/PointerMissBox.svelte +1 -1
  19. package/dist/components/Scene.svelte +2 -0
  20. package/dist/components/SceneProviders.svelte +4 -0
  21. package/dist/components/SelectedTransformControls.svelte +227 -0
  22. package/dist/components/SelectedTransformControls.svelte.d.ts +3 -0
  23. package/dist/components/Snapshot.svelte +12 -7
  24. package/dist/components/StaticGeometries.svelte +3 -56
  25. package/dist/components/overlay/AddRelationship.svelte +25 -3
  26. package/dist/components/overlay/Details.svelte +290 -229
  27. package/dist/components/overlay/dashboard/Button.svelte +4 -2
  28. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  29. package/dist/components/overlay/dashboard/Dashboard.svelte +43 -33
  30. package/dist/draw.d.ts +22 -9
  31. package/dist/draw.js +71 -41
  32. package/dist/ecs/relations.js +1 -1
  33. package/dist/ecs/traits.d.ts +17 -0
  34. package/dist/ecs/traits.js +9 -0
  35. package/dist/editing/FrameEditSession.d.ts +37 -0
  36. package/dist/editing/FrameEditSession.js +178 -0
  37. package/dist/hooks/useDrawService.svelte.d.ts +2 -0
  38. package/dist/hooks/useDrawService.svelte.js +139 -20
  39. package/dist/hooks/useFrameEditSession.svelte.d.ts +15 -0
  40. package/dist/hooks/useFrameEditSession.svelte.js +36 -0
  41. package/dist/hooks/useFrames.svelte.js +37 -2
  42. package/dist/hooks/usePartConfig.svelte.js +10 -0
  43. package/dist/hooks/useRelationships.svelte.d.ts +12 -0
  44. package/dist/hooks/useRelationships.svelte.js +78 -0
  45. package/dist/hooks/useSettings.svelte.d.ts +1 -2
  46. package/dist/hooks/useSettings.svelte.js +1 -2
  47. package/dist/hooks/useWorldState.svelte.js +10 -4
  48. package/dist/metadata.d.ts +7 -3
  49. package/dist/metadata.js +26 -2
  50. package/dist/snapshot.d.ts +6 -1
  51. package/dist/snapshot.js +10 -5
  52. package/dist/transform.js +13 -0
  53. 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 transparancy is very costly for the GPU, so we turn it on conservatively
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
- if (opacity.current !== undefined && opacity.current < 1) {
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 = colors !== undefined
74
+ material.vertexColors = vertexColors !== undefined
83
75
 
84
- if (colors && positions) {
85
- const hasAlphaChannel = positions.array.length / colors.array.length === 0.75
76
+ const hasUniformOpacity = opacity.current !== undefined && opacity.current < 1
77
+ material.opacity = hasUniformOpacity ? opacity.current! : 1
86
78
 
87
- let transparent = false
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 = colors.array.length; i < l; i += 4) {
90
- if (colors.array[i] < 1) {
91
- transparent = true
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 { matrixToPose, poseToMatrix } from '../../transform'
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
- const resolvedPose = $derived.by(() => {
29
- if (pose.current === undefined || partConfig.hasPendingSave) {
30
- return editedPose.current
31
- }
28
+ $effect.pre(() => {
29
+ if (pose.current === undefined) return
32
30
 
33
- if (!entityPose.current || !editedPose.current) {
34
- return
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
- const poseNetwork = poseToMatrix(entityPose.current)
38
- const poseUsePose = poseToMatrix(pose.current)
39
- const poseLocalEditedPose = poseToMatrix(editedPose.current)
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
- const poseNetworkInverse = poseNetwork.invert()
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
- if (event.dataTransfer === null)
56
+ const { files } = dataTransfer;
57
+ if (files.length === 0) {
58
+ dropState = 'inactive';
45
59
  return;
46
- const { files } = event.dataTransfer;
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 = $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
  }
@@ -3,6 +3,8 @@ interface Props {
3
3
  data: Uint8Array;
4
4
  name?: string;
5
5
  renderOrder?: number;
6
+ depthTest?: boolean;
7
+ depthWrite?: boolean;
6
8
  interactionLayers?: InteractionLayerValue[];
7
9
  oncreate?: (positions: Float32Array, colors: Uint8Array | undefined) => void;
8
10
  }
@@ -23,7 +23,7 @@
23
23
  onpointerdown={() => {
24
24
  cameraDown.copy(camera.current.position)
25
25
  }}
26
- onpointerup={() => {
26
+ onclick={() => {
27
27
  if (transformControls.active) {
28
28
  return
29
29
  }
@@ -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}
@@ -0,0 +1,3 @@
1
+ declare const SelectedTransformControls: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SelectedTransformControls = ReturnType<typeof SelectedTransformControls>;
3
+ export default SelectedTransformControls;
@@ -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: Entity[] = []
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 entity of entities) {
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>