@selvajs/ui 6.6.0-beta.12 → 6.6.0-beta.13

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.
@@ -33,9 +33,11 @@
33
33
  * repaint, so the row updates instantly and the geometry lags behind.
34
34
  */
35
35
  onVisibilityChange?: () => void;
36
+ /** The entry under the pointer, or null once it leaves. Drives the viewport preview tint. */
37
+ onHover?: (entry: SceneEntry | null) => void;
36
38
  }
37
39
 
38
- let { outliner, sceneVersion = 0, onVisibilityChange }: Props = $props();
40
+ let { outliner, sceneVersion = 0, onVisibilityChange, onHover }: Props = $props();
39
41
 
40
42
  // A partially hidden layer hides the rest; only a fully hidden one comes back — the same rule
41
43
  // `visibility.toggleLayer` applies to objects, restated over entries because a layer's rows are
@@ -51,6 +53,13 @@
51
53
  onVisibilityChange?.();
52
54
  };
53
55
 
56
+ // A solve or closing the panel removes the row without a mouseleave, which would leave the
57
+ // preview tint stuck on the viewport.
58
+ $effect(() => {
59
+ void sceneVersion;
60
+ return () => onHover?.(null);
61
+ });
62
+
54
63
  // Derived, not destructured: the prop is reassignable, and these must follow it.
55
64
  const hidden = $derived(outliner.visibility.hidden);
56
65
  const selected = $derived(outliner.selection.selected);
@@ -331,6 +340,8 @@
331
340
  class="gap-1.5 pl-5 pr-2 flex h-full cursor-pointer items-center transition-colors
332
341
  {isSelected ? 'bg-primary/10 hover:bg-primary/15' : 'hover:bg-muted'}
333
342
  {isHidden ? 'opacity-40' : ''}"
343
+ onmouseenter={() => onHover?.(entry)}
344
+ onmouseleave={() => onHover?.(null)}
334
345
  onmousedown={(e) => {
335
346
  e.stopPropagation();
336
347
  if (e.shiftKey) e.preventDefault();
@@ -1,4 +1,4 @@
1
- import { type SceneOutliner } from '@selvajs/visualization/scene';
1
+ import { type SceneEntry, type SceneOutliner } from '@selvajs/visualization/scene';
2
2
  interface Props {
3
3
  /**
4
4
  * Owned by `Viewer.svelte`, never built here: this panel unmounts when it closes, and hidden
@@ -13,6 +13,8 @@ interface Props {
13
13
  * repaint, so the row updates instantly and the geometry lags behind.
14
14
  */
15
15
  onVisibilityChange?: () => void;
16
+ /** The entry under the pointer, or null once it leaves. Drives the viewport preview tint. */
17
+ onHover?: (entry: SceneEntry | null) => void;
16
18
  }
17
19
  declare const SceneManager: import("svelte").Component<Props, {}, "">;
18
20
  type SceneManager = ReturnType<typeof SceneManager>;
@@ -129,6 +129,8 @@
129
129
  let updateShadowBounds: (() => void) | null = null;
130
130
  let fitToView: (() => void) | null = null;
131
131
  let clearSelection: (() => void) | null = null;
132
+ let selectEntries: ThreeViewer['selectEntries'] | null = null;
133
+ let setHover: ThreeViewer['setHover'] | null = null;
132
134
  let viewerInitialized = false;
133
135
  let sceneVersion = $state(0);
134
136
  let hideButton = $state(false);
@@ -203,8 +205,8 @@
203
205
  events: {
204
206
  // Fires before onMeshMetadataClicked, so picking an object without metadata closes the
205
207
  // previous object's panel instead of leaving it describing the wrong thing.
206
- onObjectSelected: closeMetadata,
207
- onBackgroundClicked: closeMetadata,
208
+ onObjectSelected: dropPanelSelection,
209
+ onBackgroundClicked: dropPanelSelection,
208
210
  onMeshMetadataClicked: config.enableMeshClick
209
211
  ? (metadata: Record<string, unknown>) => {
210
212
  if (hasUsefulMetadata(metadata)) {
@@ -236,6 +238,8 @@
236
238
  updateShadowBounds = init.updateShadowBounds;
237
239
  fitToView = init.fitToView;
238
240
  clearSelection = init.clearSelection;
241
+ selectEntries = init.selectEntries;
242
+ setHover = init.setHover;
239
243
  projection = init.cameraController.getProjection();
240
244
 
241
245
  // Untracked so the host reading `meshes` or config inside its setup can't re-run onMount's teardown.
@@ -389,6 +393,24 @@
389
393
  selectedMeshName = null;
390
394
  }
391
395
 
396
+ // A click on the canvas replaces whatever the panel selected: one selection at a time.
397
+ function dropPanelSelection() {
398
+ closeMetadata();
399
+ selectedObjects.clear();
400
+ }
401
+
402
+ // Mirrors the panel's selection into the viewport. The size check skips walking the entries for
403
+ // the common empty case, which also keeps `has()` from subscribing to every key.
404
+ $effect(() => {
405
+ if (selectedObjects.size === 0) {
406
+ selectEntries?.([]);
407
+ return;
408
+ }
409
+ const chosen = outliner?.entries().filter((entry) => selectedObjects.has(entry.key)) ?? [];
410
+ closeMetadata();
411
+ selectEntries?.(chosen);
412
+ });
413
+
392
414
  function dismissMetadata() {
393
415
  closeMetadata();
394
416
  clearSelection?.();
@@ -655,7 +677,12 @@
655
677
  {#if sceneManagerOpen && scene && outliner}
656
678
  <Resizable.Handle withHandle />
657
679
  <Resizable.Pane id="scene-manager" order={2} defaultSize={15} minSize={8} maxSize={30}>
658
- <SceneManager {outliner} {sceneVersion} onVisibilityChange={() => invalidate?.()} />
680
+ <SceneManager
681
+ {outliner}
682
+ {sceneVersion}
683
+ onVisibilityChange={() => invalidate?.()}
684
+ onHover={(entry) => setHover?.(entry?.object ?? null, entry?.memberIndex ?? null)}
685
+ />
659
686
  </Resizable.Pane>
660
687
  {/if}
661
688
  </Resizable.PaneGroup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "6.6.0-beta.12",
3
+ "version": "6.6.0-beta.13",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "author": "VektorNode",
@@ -64,9 +64,9 @@
64
64
  "tailwind-variants": "^3.3.1",
65
65
  "three": "^0.186.1",
66
66
  "@selvajs/compute": "^5.0.0-beta.8",
67
- "@selvajs/solve": "^2.0.0-beta.10",
68
- "@selvajs/visualization": "^1.6.0-beta.6",
69
- "@selvajs/schemas": "^5.3.0-beta.1"
67
+ "@selvajs/schemas": "^5.3.0-beta.1",
68
+ "@selvajs/visualization": "^1.6.0-beta.7",
69
+ "@selvajs/solve": "^2.0.0-beta.10"
70
70
  },
71
71
  "peerDependenciesMeta": {
72
72
  "three": {
@@ -97,7 +97,7 @@
97
97
  "@selvajs/compute": "5.0.0-beta.8",
98
98
  "@selvajs/config": "0.0.4",
99
99
  "@selvajs/schemas": "5.3.0-beta.1",
100
- "@selvajs/visualization": "1.6.0-beta.6",
100
+ "@selvajs/visualization": "1.6.0-beta.7",
101
101
  "@selvajs/solve": "2.0.0-beta.10"
102
102
  },
103
103
  "scripts": {
@@ -33,9 +33,11 @@
33
33
  * repaint, so the row updates instantly and the geometry lags behind.
34
34
  */
35
35
  onVisibilityChange?: () => void;
36
+ /** The entry under the pointer, or null once it leaves. Drives the viewport preview tint. */
37
+ onHover?: (entry: SceneEntry | null) => void;
36
38
  }
37
39
 
38
- let { outliner, sceneVersion = 0, onVisibilityChange }: Props = $props();
40
+ let { outliner, sceneVersion = 0, onVisibilityChange, onHover }: Props = $props();
39
41
 
40
42
  // A partially hidden layer hides the rest; only a fully hidden one comes back — the same rule
41
43
  // `visibility.toggleLayer` applies to objects, restated over entries because a layer's rows are
@@ -51,6 +53,13 @@
51
53
  onVisibilityChange?.();
52
54
  };
53
55
 
56
+ // A solve or closing the panel removes the row without a mouseleave, which would leave the
57
+ // preview tint stuck on the viewport.
58
+ $effect(() => {
59
+ void sceneVersion;
60
+ return () => onHover?.(null);
61
+ });
62
+
54
63
  // Derived, not destructured: the prop is reassignable, and these must follow it.
55
64
  const hidden = $derived(outliner.visibility.hidden);
56
65
  const selected = $derived(outliner.selection.selected);
@@ -331,6 +340,8 @@
331
340
  class="gap-1.5 pl-5 pr-2 flex h-full cursor-pointer items-center transition-colors
332
341
  {isSelected ? 'bg-primary/10 hover:bg-primary/15' : 'hover:bg-muted'}
333
342
  {isHidden ? 'opacity-40' : ''}"
343
+ onmouseenter={() => onHover?.(entry)}
344
+ onmouseleave={() => onHover?.(null)}
334
345
  onmousedown={(e) => {
335
346
  e.stopPropagation();
336
347
  if (e.shiftKey) e.preventDefault();
@@ -129,6 +129,8 @@
129
129
  let updateShadowBounds: (() => void) | null = null;
130
130
  let fitToView: (() => void) | null = null;
131
131
  let clearSelection: (() => void) | null = null;
132
+ let selectEntries: ThreeViewer['selectEntries'] | null = null;
133
+ let setHover: ThreeViewer['setHover'] | null = null;
132
134
  let viewerInitialized = false;
133
135
  let sceneVersion = $state(0);
134
136
  let hideButton = $state(false);
@@ -203,8 +205,8 @@
203
205
  events: {
204
206
  // Fires before onMeshMetadataClicked, so picking an object without metadata closes the
205
207
  // previous object's panel instead of leaving it describing the wrong thing.
206
- onObjectSelected: closeMetadata,
207
- onBackgroundClicked: closeMetadata,
208
+ onObjectSelected: dropPanelSelection,
209
+ onBackgroundClicked: dropPanelSelection,
208
210
  onMeshMetadataClicked: config.enableMeshClick
209
211
  ? (metadata: Record<string, unknown>) => {
210
212
  if (hasUsefulMetadata(metadata)) {
@@ -236,6 +238,8 @@
236
238
  updateShadowBounds = init.updateShadowBounds;
237
239
  fitToView = init.fitToView;
238
240
  clearSelection = init.clearSelection;
241
+ selectEntries = init.selectEntries;
242
+ setHover = init.setHover;
239
243
  projection = init.cameraController.getProjection();
240
244
 
241
245
  // Untracked so the host reading `meshes` or config inside its setup can't re-run onMount's teardown.
@@ -389,6 +393,24 @@
389
393
  selectedMeshName = null;
390
394
  }
391
395
 
396
+ // A click on the canvas replaces whatever the panel selected: one selection at a time.
397
+ function dropPanelSelection() {
398
+ closeMetadata();
399
+ selectedObjects.clear();
400
+ }
401
+
402
+ // Mirrors the panel's selection into the viewport. The size check skips walking the entries for
403
+ // the common empty case, which also keeps `has()` from subscribing to every key.
404
+ $effect(() => {
405
+ if (selectedObjects.size === 0) {
406
+ selectEntries?.([]);
407
+ return;
408
+ }
409
+ const chosen = outliner?.entries().filter((entry) => selectedObjects.has(entry.key)) ?? [];
410
+ closeMetadata();
411
+ selectEntries?.(chosen);
412
+ });
413
+
392
414
  function dismissMetadata() {
393
415
  closeMetadata();
394
416
  clearSelection?.();
@@ -655,7 +677,12 @@
655
677
  {#if sceneManagerOpen && scene && outliner}
656
678
  <Resizable.Handle withHandle />
657
679
  <Resizable.Pane id="scene-manager" order={2} defaultSize={15} minSize={8} maxSize={30}>
658
- <SceneManager {outliner} {sceneVersion} onVisibilityChange={() => invalidate?.()} />
680
+ <SceneManager
681
+ {outliner}
682
+ {sceneVersion}
683
+ onVisibilityChange={() => invalidate?.()}
684
+ onHover={(entry) => setHover?.(entry?.object ?? null, entry?.memberIndex ?? null)}
685
+ />
659
686
  </Resizable.Pane>
660
687
  {/if}
661
688
  </Resizable.PaneGroup>