@viamrobotics/motion-tools 1.32.0 → 1.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/components/App.svelte +17 -11
  2. package/dist/components/App.svelte.d.ts +14 -7
  3. package/dist/components/Entities/Entities.svelte +18 -25
  4. package/dist/components/Entities/Entities.svelte.d.ts +2 -17
  5. package/dist/components/Entities/Label.svelte +79 -13
  6. package/dist/components/Entities/Label.svelte.d.ts +2 -1
  7. package/dist/components/Entities/Labels.svelte +36 -0
  8. package/dist/components/Entities/Labels.svelte.d.ts +3 -0
  9. package/dist/components/Entities/LineDots.svelte +8 -3
  10. package/dist/components/Entities/labelLayout/applyTeleports.d.ts +9 -0
  11. package/dist/components/Entities/labelLayout/applyTeleports.js +39 -0
  12. package/dist/components/Entities/labelLayout/buildNeighborhood.d.ts +8 -0
  13. package/dist/components/Entities/labelLayout/buildNeighborhood.js +26 -0
  14. package/dist/components/Entities/labelLayout/cameraHash.d.ts +8 -0
  15. package/dist/components/Entities/labelLayout/cameraHash.js +25 -0
  16. package/dist/components/Entities/labelLayout/cost.d.ts +44 -0
  17. package/dist/components/Entities/labelLayout/cost.js +126 -0
  18. package/dist/components/Entities/labelLayout/createLabelLayout.d.ts +27 -0
  19. package/dist/components/Entities/labelLayout/createLabelLayout.js +194 -0
  20. package/dist/components/Entities/labelLayout/geometry.d.ts +20 -0
  21. package/dist/components/Entities/labelLayout/geometry.js +151 -0
  22. package/dist/components/Entities/labelLayout/labelStore.svelte.d.ts +17 -0
  23. package/dist/components/Entities/labelLayout/labelStore.svelte.js +28 -0
  24. package/dist/components/Entities/labelLayout/measure.d.ts +13 -0
  25. package/dist/components/Entities/labelLayout/measure.js +42 -0
  26. package/dist/components/Entities/labelLayout/slots.d.ts +11 -0
  27. package/dist/components/Entities/labelLayout/slots.js +47 -0
  28. package/dist/components/Entities/labelLayout/solve.d.ts +11 -0
  29. package/dist/components/Entities/labelLayout/solve.js +93 -0
  30. package/dist/components/Entities/labelLayout/spatialHash.d.ts +15 -0
  31. package/dist/components/Entities/labelLayout/spatialHash.js +53 -0
  32. package/dist/components/Entities/labelLayout/types.d.ts +105 -0
  33. package/dist/components/Entities/labelLayout/types.js +19 -0
  34. package/dist/components/Entities/labelLayout/writeBack.d.ts +20 -0
  35. package/dist/components/Entities/labelLayout/writeBack.js +51 -0
  36. package/dist/components/Scene.svelte +42 -48
  37. package/dist/components/SceneProviders.svelte +0 -3
  38. package/dist/components/SelectedTransformControls.svelte +65 -47
  39. package/dist/components/overlay/Details.svelte +198 -224
  40. package/dist/components/overlay/Details.svelte.d.ts +1 -1
  41. package/dist/components/overlay/Popover.svelte +6 -4
  42. package/dist/components/overlay/Popover.svelte.d.ts +6 -2
  43. package/dist/components/overlay/dashboard/Button.svelte +7 -2
  44. package/dist/components/overlay/dashboard/Button.svelte.d.ts +2 -1
  45. package/dist/components/overlay/details/AxesHelperDetails.svelte +32 -0
  46. package/dist/components/overlay/details/AxesHelperDetails.svelte.d.ts +7 -0
  47. package/dist/components/overlay/details/ColorDetails.svelte +35 -0
  48. package/dist/components/overlay/details/ColorDetails.svelte.d.ts +7 -0
  49. package/dist/components/overlay/details/GeometryDetails.svelte +104 -0
  50. package/dist/components/overlay/details/GeometryDetails.svelte.d.ts +7 -0
  51. package/dist/components/overlay/details/LineDetails/LineDetails.svelte +196 -0
  52. package/dist/components/overlay/details/LineDetails/LineDetails.svelte.d.ts +7 -0
  53. package/dist/components/overlay/details/LineDetails/linePositions.d.ts +3 -0
  54. package/dist/components/overlay/details/LineDetails/linePositions.js +30 -0
  55. package/dist/components/overlay/details/OpacityDetails.svelte +44 -0
  56. package/dist/components/overlay/details/OpacityDetails.svelte.d.ts +7 -0
  57. package/dist/components/overlay/details/PoseDetails.svelte +189 -0
  58. package/dist/components/overlay/details/PoseDetails.svelte.d.ts +14 -0
  59. package/dist/components/overlay/settings/ConnectionSettings.svelte +42 -0
  60. package/dist/components/overlay/settings/ConnectionSettings.svelte.d.ts +18 -0
  61. package/dist/components/overlay/settings/DebugSettings.svelte +13 -0
  62. package/dist/components/{xr/frame-configure/Controllers.svelte.d.ts → overlay/settings/DebugSettings.svelte.d.ts} +3 -3
  63. package/dist/components/overlay/settings/PointcloudSettings.svelte +61 -0
  64. package/dist/components/overlay/settings/PointcloudSettings.svelte.d.ts +3 -0
  65. package/dist/components/overlay/settings/SceneSettings.svelte +110 -0
  66. package/dist/components/overlay/settings/SceneSettings.svelte.d.ts +18 -0
  67. package/dist/components/overlay/settings/Settings.svelte +27 -312
  68. package/dist/components/overlay/settings/Settings.svelte.d.ts +8 -1
  69. package/dist/components/overlay/settings/Tabs.svelte +5 -3
  70. package/dist/components/overlay/settings/Tabs.svelte.d.ts +3 -3
  71. package/dist/components/overlay/settings/VisionSettings.svelte +31 -0
  72. package/dist/components/overlay/settings/VisionSettings.svelte.d.ts +3 -0
  73. package/dist/components/overlay/settings/WeblabSettings.svelte +27 -0
  74. package/dist/components/overlay/settings/WeblabSettings.svelte.d.ts +18 -0
  75. package/dist/components/overlay/settings/WidgetSettings.svelte +49 -0
  76. package/dist/components/overlay/settings/WidgetSettings.svelte.d.ts +3 -0
  77. package/dist/components/overlay/widgets/FramePov.svelte +1 -12
  78. package/dist/ecs/traits.d.ts +1 -1
  79. package/dist/ecs/traits.js +1 -1
  80. package/dist/hooks/useWorldState.svelte.js +39 -50
  81. package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte +3 -5
  82. package/dist/plugins/XR/DebugPanel.svelte +29 -0
  83. package/dist/plugins/XR/DebugPanel.svelte.d.ts +3 -0
  84. package/dist/plugins/XR/OriginMarker.svelte +341 -0
  85. package/dist/plugins/XR/PendingEditsPanel.svelte +60 -0
  86. package/dist/plugins/XR/PendingEditsPanel.svelte.d.ts +18 -0
  87. package/dist/plugins/XR/WristDisplay.svelte +60 -0
  88. package/dist/plugins/XR/WristDisplay.svelte.d.ts +19 -0
  89. package/dist/{components/xr → plugins/XR}/XR.svelte +69 -23
  90. package/dist/plugins/XR/XRPlugins.svelte +9 -0
  91. package/dist/plugins/XR/XRPlugins.svelte.d.ts +26 -0
  92. package/dist/plugins/XR/XRSettings.svelte +240 -0
  93. package/dist/plugins/XR/XRSettings.svelte.d.ts +3 -0
  94. package/dist/{components/xr → plugins/XR}/XRToast.svelte +6 -9
  95. package/dist/plugins/XR/debug.svelte.d.ts +7 -0
  96. package/dist/plugins/XR/debug.svelte.js +13 -0
  97. package/dist/plugins/XR/frame-configure/Controllers.svelte +413 -0
  98. package/dist/plugins/XR/teleop/Controllers.svelte.d.ts +3 -0
  99. package/dist/{components/xr → plugins/XR}/useAnchors.svelte.d.ts +4 -0
  100. package/dist/{components/xr → plugins/XR}/useAnchors.svelte.js +22 -0
  101. package/dist/plugins/XR/useOrigin.svelte.d.ts +24 -0
  102. package/dist/plugins/XR/useOrigin.svelte.js +50 -0
  103. package/dist/plugins/index.d.ts +2 -0
  104. package/dist/plugins/index.js +2 -0
  105. package/dist/three/OBBHelper.js +1 -0
  106. package/package.json +3 -1
  107. package/dist/components/xr/OriginMarker.svelte +0 -151
  108. package/dist/components/xr/XRControllerSettings.svelte +0 -242
  109. package/dist/components/xr/XRControllerSettings.svelte.d.ts +0 -3
  110. package/dist/components/xr/frame-configure/Controllers.svelte +0 -6
  111. package/dist/components/xr/useOrigin.svelte.d.ts +0 -9
  112. package/dist/components/xr/useOrigin.svelte.js +0 -27
  113. /package/dist/{components/xr → plugins/XR}/ArmTeleop.svelte.d.ts +0 -0
  114. /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte +0 -0
  115. /package/dist/{components/xr → plugins/XR}/BentPlaneGeometry.svelte.d.ts +0 -0
  116. /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte +0 -0
  117. /package/dist/{components/xr → plugins/XR}/CameraFeed.svelte.d.ts +0 -0
  118. /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte +0 -0
  119. /package/dist/{components/xr → plugins/XR}/JointLimitsWidget.svelte.d.ts +0 -0
  120. /package/dist/{components/xr → plugins/XR}/OriginMarker.svelte.d.ts +0 -0
  121. /package/dist/{components/xr → plugins/XR}/PointDistance.svelte +0 -0
  122. /package/dist/{components/xr → plugins/XR}/PointDistance.svelte.d.ts +0 -0
  123. /package/dist/{components/xr → plugins/XR}/XR.svelte.d.ts +0 -0
  124. /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte +0 -0
  125. /package/dist/{components/xr → plugins/XR}/XRConfigPanel.svelte.d.ts +0 -0
  126. /package/dist/{components/xr → plugins/XR}/XRToast.svelte.d.ts +0 -0
  127. /package/dist/{components/xr/teleop → plugins/XR/frame-configure}/Controllers.svelte.d.ts +0 -0
  128. /package/dist/{components/xr → plugins/XR}/math.d.ts +0 -0
  129. /package/dist/{components/xr → plugins/XR}/math.js +0 -0
  130. /package/dist/{components/xr → plugins/XR}/teleop/Controllers.svelte +0 -0
  131. /package/dist/{components/xr → plugins/XR}/toasts.svelte.d.ts +0 -0
  132. /package/dist/{components/xr → plugins/XR}/toasts.svelte.js +0 -0
@@ -7,7 +7,7 @@
7
7
  import { useXR } from '@threlte/xr'
8
8
  import { provideToast, ToastContainer } from '@viamrobotics/prime-core'
9
9
  import { primeTheme } from '@viamrobotics/tweakpane-config'
10
- import { onMount, type Snippet } from 'svelte'
10
+ import { type Component, onMount, type Snippet } from 'svelte'
11
11
  import { ThemeUtils } from 'svelte-tweakpane-ui'
12
12
 
13
13
  import type { FragmentInfo } from '../hooks/usePartConfig.svelte'
@@ -17,7 +17,6 @@
17
17
  import Details from './overlay/Details.svelte'
18
18
  import TreeContainer from './overlay/left-pane/TreeContainer.svelte'
19
19
  import Settings from './overlay/settings/Settings.svelte'
20
- import XR from './xr/XR.svelte'
21
20
  import { provideWorld, traits, useQuery } from '../ecs'
22
21
  import { type CameraPose, provideCameraControls } from '../hooks/useControls.svelte'
23
22
  import { provideEnvironment } from '../hooks/useEnvironment.svelte'
@@ -51,7 +50,20 @@
51
50
  localConfigProps?: LocalConfigProps
52
51
 
53
52
  /**
54
- * Snippet for THREE objects
53
+ * Allows adding additional tabs to the settings panel
54
+ */
55
+ settingsTabs?: {
56
+ label: string
57
+ component: Component
58
+ }[]
59
+
60
+ /**
61
+ * Allows setting the initial camera pose
62
+ */
63
+ cameraPose?: CameraPose
64
+
65
+ /**
66
+ * Snippet for Three.js objects
55
67
  */
56
68
  children?: Snippet
57
69
 
@@ -64,11 +76,6 @@
64
76
  * Snippet to inject items into the details panel
65
77
  */
66
78
  details?: Snippet<[{ entity: Entity }]>
67
-
68
- /**
69
- * Allows setting the initial camera pose
70
- */
71
- cameraPose?: CameraPose
72
79
  }
73
80
 
74
81
  let {
@@ -76,6 +83,7 @@
76
83
  inputBindingsEnabled = true,
77
84
  localConfigProps,
78
85
  cameraPose,
86
+ settingsTabs,
79
87
  children: appChildren,
80
88
  dashboard,
81
89
  details,
@@ -124,8 +132,6 @@
124
132
  {@render appChildren?.()}
125
133
  </Scene>
126
134
 
127
- <XR {@attach domPortal(root)} />
128
-
129
135
  {#if settings.current.renderSubEntityHoverDetail}
130
136
  <HoveredEntities />
131
137
  {/if}
@@ -166,7 +172,7 @@
166
172
 
167
173
  <PortalTarget id="dom" />
168
174
 
169
- <Settings />
175
+ <Settings {settingsTabs} />
170
176
  <Logs />
171
177
  <AddFrames />
172
178
  </div>
@@ -1,6 +1,6 @@
1
1
  import type { Struct } from '@viamrobotics/sdk';
2
2
  import type { Entity } from 'koota';
3
- import { type Snippet } from 'svelte';
3
+ import { type Component, type Snippet } from 'svelte';
4
4
  import type { FragmentInfo } from '../hooks/usePartConfig.svelte';
5
5
  import { type CameraPose } from '../hooks/useControls.svelte';
6
6
  interface LocalConfigProps {
@@ -14,7 +14,18 @@ interface Props {
14
14
  inputBindingsEnabled?: boolean;
15
15
  localConfigProps?: LocalConfigProps;
16
16
  /**
17
- * Snippet for THREE objects
17
+ * Allows adding additional tabs to the settings panel
18
+ */
19
+ settingsTabs?: {
20
+ label: string;
21
+ component: Component;
22
+ }[];
23
+ /**
24
+ * Allows setting the initial camera pose
25
+ */
26
+ cameraPose?: CameraPose;
27
+ /**
28
+ * Snippet for Three.js objects
18
29
  */
19
30
  children?: Snippet;
20
31
  /**
@@ -27,11 +38,7 @@ interface Props {
27
38
  details?: Snippet<[{
28
39
  entity: Entity;
29
40
  }]>;
30
- /**
31
- * Allows setting the initial camera pose
32
- */
33
- cameraPose?: CameraPose;
34
41
  }
35
- declare const App: import("svelte").Component<Props, {}, "">;
42
+ declare const App: Component<Props, {}, "">;
36
43
  type App = ReturnType<typeof App>;
37
44
  export default App;
@@ -2,12 +2,13 @@
2
2
  import { Not, Or } from 'koota'
3
3
 
4
4
  import { traits, useQuery } from '../../ecs'
5
+ import { useSettings } from '../../hooks/useSettings.svelte'
5
6
 
6
7
  import Arrows from './Arrows/ArrowGroups.svelte'
7
8
  import Frame from './Frame.svelte'
8
9
  import Geometry from './Geometry.svelte'
9
10
  import GLTF from './GLTF.svelte'
10
- import Label from './Label.svelte'
11
+ import Labels from './Labels.svelte'
11
12
  import Line from './Line.svelte'
12
13
  import Points from './Points.svelte'
13
14
  import Pose from './Pose.svelte'
@@ -60,56 +61,48 @@
60
61
  const points = useQuery(traits.Points)
61
62
  const lines = useQuery(traits.LinePositions)
62
63
  const gltfs = useQuery(traits.GLTF)
64
+
65
+ const settings = useSettings()
66
+
67
+ const enableLabels = $derived(settings.current.enableLabels)
63
68
  </script>
64
69
 
65
70
  {#each machineFramesEntities.current as entity (entity)}
66
71
  <Pose {entity}>
67
- <Frame {entity}>
68
- <Label text={entity.get(traits.Name)} />
69
- </Frame>
72
+ <Frame {entity} />
70
73
  </Pose>
71
74
  {/each}
72
75
 
73
76
  {#each resourceGeometriesEntities.current as entity (entity)}
74
- <Geometry {entity}>
75
- <Label text={entity.get(traits.Name)} />
76
- </Geometry>
77
+ <Geometry {entity} />
77
78
  {/each}
78
79
 
79
80
  {#each worldStateEntities.current as entity (entity)}
80
- <Frame {entity}>
81
- <Label text={entity.get(traits.Name)} />
82
- </Frame>
81
+ <Frame {entity} />
83
82
  {/each}
84
83
 
85
84
  {#each drawServiceEntities.current as entity (entity)}
86
- <Frame {entity}>
87
- <Label text={entity.get(traits.Name)} />
88
- </Frame>
85
+ <Frame {entity} />
89
86
  {/each}
90
87
 
91
88
  {#each meshEntities.current as entity (entity)}
92
- <Frame {entity}>
93
- <Label text={entity.get(traits.Name)} />
94
- </Frame>
89
+ <Frame {entity} />
95
90
  {/each}
96
91
 
97
92
  {#each points.current as entity (entity)}
98
- <Points {entity}>
99
- <Label text={entity.get(traits.Name)} />
100
- </Points>
93
+ <Points {entity} />
101
94
  {/each}
102
95
 
103
96
  {#each lines.current as entity (entity)}
104
- <Line {entity}>
105
- <Label text={entity.get(traits.Name)} />
106
- </Line>
97
+ <Line {entity} />
107
98
  {/each}
108
99
 
109
100
  {#each gltfs.current as entity (entity)}
110
- <GLTF {entity}>
111
- <Label text={entity.get(traits.Name)} />
112
- </GLTF>
101
+ <GLTF {entity} />
113
102
  {/each}
114
103
 
115
104
  <Arrows />
105
+
106
+ {#if enableLabels}
107
+ <Labels />
108
+ {/if}
@@ -1,18 +1,3 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const Entities: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type Entities = InstanceType<typeof Entities>;
1
+ declare const Entities: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Entities = ReturnType<typeof Entities>;
18
3
  export default Entities;
@@ -1,25 +1,91 @@
1
1
  <script lang="ts">
2
+ import type { Entity } from 'koota'
3
+
4
+ import { useThrelte } from '@threlte/core'
2
5
  import { HTML } from '@threlte/extras'
6
+ import { untrack } from 'svelte'
7
+ import { Group } from 'three'
8
+
9
+ import { traits, useTag, useTrait } from '../../ecs'
3
10
 
4
- import { useSettings } from '../../hooks/useSettings.svelte'
11
+ import { labels } from './labelLayout/labelStore.svelte'
5
12
 
6
13
  interface Props {
7
- text?: string
14
+ entity: Entity
8
15
  }
9
16
 
10
- let { text }: Props = $props()
17
+ let { entity }: Props = $props()
18
+
19
+ const { invalidate } = useThrelte()
20
+
21
+ const matrix = useTrait(() => entity, traits.WorldMatrix)
22
+ const name = useTrait(() => entity, traits.Name)
23
+ const color = useTrait(() => entity, traits.Color)
24
+ const selected = useTag(() => entity, traits.Selected)
25
+
26
+ let element = $state.raw<HTMLElement>()
27
+
28
+ $effect(() => {
29
+ const el = element
30
+
31
+ if (!el) return
32
+
33
+ return untrack(() => {
34
+ labels.add(el)
35
+ return () => labels.remove(el)
36
+ })
37
+ })
38
+
39
+ // Re-measure when the label text changes (its width drives slot geometry).
40
+ $effect(() => {
41
+ if (name.current) {
42
+ untrack(() => labels.touch())
43
+ }
44
+ })
11
45
 
12
- const settings = useSettings()
46
+ let ref = $state<Group>()
13
47
 
14
- const labels = $derived(settings.current.enableLabels)
48
+ $effect(() => {
49
+ if (matrix.current && ref) {
50
+ ref.matrix.copy(matrix.current)
51
+ ref.updateMatrixWorld()
52
+ invalidate()
53
+ }
54
+ })
15
55
  </script>
16
56
 
17
- {#if labels && text}
18
- <HTML
19
- center
20
- zIndexRange={[3, 0]}
21
- class="border-gray-7 border bg-white px-2 py-1 text-xs"
57
+ <HTML
58
+ center
59
+ zIndexRange={[3, 0]}
60
+ matrixAutoUpdate={false}
61
+ bind:ref
62
+ >
63
+ <div
64
+ class="label relative h-0 w-0"
65
+ bind:this={element}
22
66
  >
23
- {text}
24
- </HTML>
25
- {/if}
67
+ <svg class="link pointer-events-none absolute top-0 left-0 overflow-visible">
68
+ <line class="stroke-gray-9 stroke-1" />
69
+ </svg>
70
+ <div
71
+ class="dot border-gray-9 pointer-events-none absolute -top-1 -left-0 z-1 h-2 w-2 -translate-1/2 rounded-full border"
72
+ ></div>
73
+ <button
74
+ class={[
75
+ 'border-gray-9 text absolute z-2 border px-2 py-1 text-xs text-nowrap',
76
+ {
77
+ 'bg-gray-9 text-white': selected.current,
78
+ 'bg-white': !selected.current,
79
+ },
80
+ ]}
81
+ style={color.current
82
+ ? `border-color-left: rgb(${color.current.r}, ${color.current.g}, ${color.current.b})`
83
+ : undefined}
84
+ onclick={() => {
85
+ entity.add(traits.Selected)
86
+ }}
87
+ >
88
+ {name.current}
89
+ </button>
90
+ </div>
91
+ </HTML>
@@ -1,5 +1,6 @@
1
+ import type { Entity } from 'koota';
1
2
  interface Props {
2
- text?: string;
3
+ entity: Entity;
3
4
  }
4
5
  declare const Label: import("svelte").Component<Props, {}, "">;
5
6
  type Label = ReturnType<typeof Label>;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { useTask, useThrelte } from '@threlte/core'
3
+
4
+ import { traits, useQuery } from '../../ecs'
5
+
6
+ import Label from './Label.svelte'
7
+ import { createLabelLayout } from './labelLayout/createLabelLayout'
8
+ import { labels } from './labelLayout/labelStore.svelte'
9
+
10
+ const { camera, invalidate, size } = useThrelte()
11
+
12
+ const entities = useQuery(traits.Name)
13
+
14
+ const layout = createLabelLayout({ camera, size, invalidate, labels })
15
+
16
+ // Wake the on-demand render loop when labels are added/removed or their text
17
+ // changes, so the engine re-solves even while the camera is still. Reading
18
+ // `version` registers the reactive dependency.
19
+ $effect(() => {
20
+ if (labels.version >= 0) invalidate()
21
+ })
22
+
23
+ // `autoInvalidate: false` — the engine drives its own invalidation (camera
24
+ // motion, the version effect above, and while animating), so the task can run
25
+ // without pinning the on-demand Canvas to render every frame.
26
+ useTask(
27
+ (delta) => {
28
+ layout.frame(delta)
29
+ },
30
+ { autoInvalidate: false }
31
+ )
32
+ </script>
33
+
34
+ {#each entities.current as entity (entity)}
35
+ <Label {entity} />
36
+ {/each}
@@ -0,0 +1,3 @@
1
+ declare const Labels: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Labels = ReturnType<typeof Labels>;
3
+ export default Labels;
@@ -41,9 +41,15 @@
41
41
 
42
42
  $effect(() => {
43
43
  if (!positions) return
44
+ // Track the IDs `addInstance` returns rather than assuming they're a
45
+ // sequential 0..N-1 range — when positions changes (e.g. a line gizmo
46
+ // being placed), cleanup-by-index would target slots that were never
47
+ // allocated for this effect run and throw "Invalid instanceId".
48
+ const instances: number[] = []
44
49
  for (let i = 0, l = positions.length; i < l; i += 3) {
45
50
  const dotIndex = i / 3
46
51
  const instance = mesh.addInstance(geometryID)
52
+ instances.push(instance)
47
53
  matrix.makeTranslation(positions[i + 0], positions[i + 1], positions[i + 2])
48
54
  matrix.scale(vec3.setScalar(scale))
49
55
  mesh.setMatrixAt(instance, matrix)
@@ -55,9 +61,8 @@
55
61
  }
56
62
 
57
63
  return () => {
58
- if (!positions) return
59
- for (let i = 0, l = positions.length / 3; i < l; i += 1) {
60
- mesh.deleteInstance(i)
64
+ for (const instance of instances) {
65
+ mesh.deleteInstance(instance)
61
66
  }
62
67
  }
63
68
  })
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Post-solve teleport handling: decides which nodes snap to their solved target
3
+ * this frame versus ease toward it. A big camera jump (large median anchor
4
+ * displacement) or a brand-new node (no prior anchor) places labels in their
5
+ * final spot rather than gliding across the screen; everything else eases.
6
+ * Always rolls each node's prevAx/prevAy forward for the next solve's comparison.
7
+ */
8
+ import type { LabelNode, SolverConfig } from './types';
9
+ export declare const applyTeleports: (nodes: LabelNode[], width: number, height: number, config: SolverConfig) => void;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Post-solve teleport handling: decides which nodes snap to their solved target
3
+ * this frame versus ease toward it. A big camera jump (large median anchor
4
+ * displacement) or a brand-new node (no prior anchor) places labels in their
5
+ * final spot rather than gliding across the screen; everything else eases.
6
+ * Always rolls each node's prevAx/prevAy forward for the next solve's comparison.
7
+ */
8
+ /** The radius of a node's outermost slot (0 if it has none). */
9
+ const maxSlotRadius = (node) => {
10
+ const last = node.slots.at(-1);
11
+ return last ? last.radius : 0;
12
+ };
13
+ export const applyTeleports = (nodes, width, height, config) => {
14
+ const diag = Math.hypot(width, height);
15
+ const displacements = [];
16
+ for (const node of nodes) {
17
+ if (!Number.isNaN(node.prevAx)) {
18
+ displacements.push(Math.hypot(node.ax - node.prevAx, node.ay - node.prevAy));
19
+ }
20
+ }
21
+ let snapAll = displacements.length === 0;
22
+ if (!snapAll) {
23
+ displacements.sort((a, b) => a - b);
24
+ const median = displacements[displacements.length >> 1];
25
+ if (median > config.teleportFrac * diag)
26
+ snapAll = true;
27
+ }
28
+ for (const node of nodes) {
29
+ const ownJump = !Number.isNaN(node.prevAx) &&
30
+ Math.hypot(node.ax - node.prevAx, node.ay - node.prevAy) > maxSlotRadius(node) * 3;
31
+ if (snapAll || ownJump) {
32
+ node.cx = node.tx;
33
+ node.cy = node.ty;
34
+ node.settled = true;
35
+ }
36
+ node.prevAx = node.ax;
37
+ node.prevAy = node.ay;
38
+ }
39
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Builds each node's pruned, symmetric interaction set for one solve. The cell
3
+ * size is chosen so any two labels whose boxes could possibly interact share or
4
+ * border a cell, so the 3x3 query around each node finds every candidate.
5
+ */
6
+ import type { SpatialHash } from './spatialHash';
7
+ import type { LabelNode, SolverConfig } from './types';
8
+ export declare const buildNeighborhood: (grid: SpatialHash, nodes: LabelNode[], config: SolverConfig) => void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Builds each node's pruned, symmetric interaction set for one solve. The cell
3
+ * size is chosen so any two labels whose boxes could possibly interact share or
4
+ * border a cell, so the 3x3 query around each node finds every candidate.
5
+ */
6
+ export const buildNeighborhood = (grid, nodes, config) => {
7
+ const maxRingMult = Math.max(...config.ringRadiiCrowded);
8
+ // Cell size so any two labels whose boxes could interact share/border a cell.
9
+ let cell = 1;
10
+ for (const node of nodes) {
11
+ const halfDiag = Math.hypot(node.w / 2, node.h / 2);
12
+ const support = Math.max(node.w, node.h) / 2;
13
+ const outer = (support + node.dotR + config.dotPadding) * maxRingMult;
14
+ cell = Math.max(cell, 2 * (halfDiag + outer));
15
+ }
16
+ grid.build(nodes, cell);
17
+ // Symmetric neighbourhoods so the solver's incremental bookkeeping stays exact.
18
+ for (const node of nodes)
19
+ node.neighbors = grid.queryNeighbors(node, config.maxNeighbors);
20
+ for (const a of nodes) {
21
+ for (const b of a.neighbors) {
22
+ if (!b.neighbors.includes(a))
23
+ b.neighbors.push(a);
24
+ }
25
+ }
26
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A cheap, exact change-signal for the camera. Hashing the view + projection
3
+ * matrices (as raw float bits) plus the viewport size catches every pan, orbit,
4
+ * dolly, zoom, resize, and perspective/orthographic swap with no epsilon to tune.
5
+ * Computed every frame; the layout only re-solves when the hash changes.
6
+ */
7
+ import type { Camera } from 'three';
8
+ export declare function cameraMatrixHash(camera: Camera, width: number, height: number): number;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A cheap, exact change-signal for the camera. Hashing the view + projection
3
+ * matrices (as raw float bits) plus the viewport size catches every pan, orbit,
4
+ * dolly, zoom, resize, and perspective/orthographic swap with no epsilon to tune.
5
+ * Computed every frame; the layout only re-solves when the hash changes.
6
+ */
7
+ const f32 = new Float32Array(1);
8
+ const i32 = new Int32Array(f32.buffer);
9
+ function bits(value) {
10
+ f32[0] = value;
11
+ return i32[0];
12
+ }
13
+ export function cameraMatrixHash(camera, width, height) {
14
+ camera.updateMatrixWorld();
15
+ const view = camera.matrixWorldInverse.elements;
16
+ const proj = camera.projectionMatrix.elements;
17
+ let h = 2166136261 >>> 0;
18
+ for (let i = 0; i < 16; i++)
19
+ h = Math.imul(h ^ bits(view[i]), 16777619) >>> 0;
20
+ for (let i = 0; i < 16; i++)
21
+ h = Math.imul(h ^ bits(proj[i]), 16777619) >>> 0;
22
+ h = Math.imul(h ^ bits(width), 16777619) >>> 0;
23
+ h = Math.imul(h ^ bits(height), 16777619) >>> 0;
24
+ return h >>> 0;
25
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cost function for a candidate label placement. Lower is better.
3
+ *
4
+ * The hierarchy `lineBox >> boxDot > boxBox > lineLine >> stick > spread > len`
5
+ * is near-lexicographic: a single leader passing under another label always
6
+ * outranks fixing every overlap a node could have against its <=24 neighbors,
7
+ * so the optimizer eliminates crossings first (the user's top priority), then
8
+ * dot coverage, then box overlaps, then tidies the radial fan.
9
+ */
10
+ import type { LabelNode, SolverConfig } from './types';
11
+ export declare const W: {
12
+ /** DOMINANT — a leader passing under ANOTHER label's box. Requirement #1. */
13
+ lineBox: number;
14
+ /** Our box covering another node's dot. Worse than a box overlap. Requirement #4. */
15
+ boxDot: number;
16
+ /** Two label boxes overlapping. Requirement #2. */
17
+ boxBox: number;
18
+ /** Two leaders crossing — thin lines, mild. Supports the radial fan. */
19
+ lineLine: number;
20
+ /** Sticky bonus for staying on the previous slot (anti flip-flop). Requirement #6. */
21
+ stick: number;
22
+ /** Penalty when a slot's angle nearly coincides with a neighbor's leader angle. */
23
+ spread: number;
24
+ /** Outward-fan preference: cheaper to point away from the local cluster. Requirement #3. */
25
+ radial: number;
26
+ /** Leader length — keep labels close to their dot. Small. */
27
+ len: number;
28
+ };
29
+ /**
30
+ * The geometric "bad" terms only (leader-under-box, box-over-dot, box-box,
31
+ * leader-leader, angular spread) for placing `node` at slot `si` against its
32
+ * committed neighbors. This is the local-search objective AND its termination
33
+ * guard — exactly 0 when the node has no crossings/overlaps. The solver both
34
+ * selects and accepts moves on this value so it can never lock a node at a
35
+ * placement whose conflict another available slot would reduce.
36
+ */
37
+ export declare function evalConflict(node: LabelNode, si: number, neighbors: LabelNode[], config: SolverConfig): number;
38
+ /**
39
+ * Conflict-independent "tidiness" of a slot: short leaders, pointing away from
40
+ * the local cluster centroid (radial fan-out), with a bonus for staying put.
41
+ * Used only as a tie-break among slots of equal conflict, never to override a
42
+ * conflict reduction.
43
+ */
44
+ export declare function placementBias(node: LabelNode, si: number): number;