@selvajs/ui 6.0.0-beta.5 → 6.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  import type { ActionButton } from '../../types/actionButton';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
5
  import { ChevronUp } from '@lucide/svelte';
6
+ import type { ThreeViewer } from '@selvajs/visualization/render';
6
7
  import Viewer, { type ViewerConfig } from '../viewer/Viewer.svelte';
7
8
  import CalculateButton from '../primitives/CalculateButton.svelte';
8
9
  import SolvingIndicator from '../primitives/SolvingIndicator.svelte';
@@ -39,6 +40,8 @@
39
40
  viewerConfig?: ViewerConfig;
40
41
  /** Branding logo URL shown as a watermark in the viewer's bottom-right corner. */
41
42
  logoUrl?: string;
43
+ /** Hands the live three.js viewer to the host. See `Viewer.svelte`. */
44
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
42
45
  }
43
46
 
44
47
  let {
@@ -60,7 +63,8 @@
60
63
  onListStates,
61
64
  presetLabels,
62
65
  viewerConfig = {},
63
- logoUrl
66
+ logoUrl,
67
+ onViewerReady
64
68
  }: Props = $props();
65
69
 
66
70
  const hasViewer = $derived(
@@ -189,6 +193,7 @@
189
193
  isBlurred={drawerOpen}
190
194
  {drawerOpen}
191
195
  {logoUrl}
196
+ {onViewerReady}
192
197
  viewerConfig={{
193
198
  ...viewerConfig,
194
199
  backgroundColor: schema?.viewerOptions?.backgroundColor,
@@ -352,6 +357,7 @@
352
357
  bind:isFullscreen={isViewerFullscreen}
353
358
  {isSolving}
354
359
  {logoUrl}
360
+ {onViewerReady}
355
361
  viewerConfig={{
356
362
  backgroundColor: schema?.viewerOptions?.backgroundColor
357
363
  }}
@@ -1,6 +1,7 @@
1
1
  import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
2
2
  import type { ActionButton } from '../../types/actionButton';
3
3
  import type { PresetLabels } from '../../types/presetLabels';
4
+ import type { ThreeViewer } from '@selvajs/visualization/render';
4
5
  import { type ViewerConfig } from '../viewer/Viewer.svelte';
5
6
  interface Props {
6
7
  schema: UISchema;
@@ -23,6 +24,8 @@ interface Props {
23
24
  viewerConfig?: ViewerConfig;
24
25
  /** Branding logo URL shown as a watermark in the viewer's bottom-right corner. */
25
26
  logoUrl?: string;
27
+ /** Hands the live three.js viewer to the host. See `Viewer.svelte`. */
28
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
26
29
  }
27
30
  declare const AppLayout: import("svelte").Component<Props, {}, "isViewerFullscreen">;
28
31
  type AppLayout = ReturnType<typeof AppLayout>;
@@ -7,8 +7,9 @@
7
7
  import type { PresetLabels } from '../../types/presetLabels';
8
8
  import { createSolvingIndicator } from '../../compute/solving.svelte';
9
9
  import { createRequestResponseDriver } from '@selvajs/solve/client';
10
- import type { RetainedSolveResult } from '@selvajs/solve/client';
10
+ import type { RetainedSolveResult, SolveSession } from '@selvajs/solve/client';
11
11
  import { meshPolicy } from '@selvajs/visualization/parse';
12
+ import type { ThreeViewer } from '@selvajs/visualization/render';
12
13
  import { useSolveSession } from '../../compute/useSolveSession.svelte';
13
14
  import { useFooterItem } from '../../composables/useFooterItem.svelte';
14
15
  import { hexToOklch } from '../../utils/color';
@@ -62,7 +63,20 @@
62
63
  * A getter, not a snapshot: `onReady` fires once.
63
64
  */
64
65
  getLastResult: () => RetainedSolveResult | null;
66
+ /**
67
+ * The live solve session, for hosts driving solves from their own state —
68
+ * `setValue`/`solve` to push inputs, `subscribe` to react to results.
69
+ *
70
+ * A getter for the same reason as `getLastResult`. Values written here go through the
71
+ * same throttle and memo as the UI's, so it's safe to call at interaction rate.
72
+ */
73
+ getSession: () => SolveSession;
65
74
  }) => void;
75
+ /**
76
+ * Hands the live three.js viewer to the host once it mounts, for apps drawing their own
77
+ * content alongside solve results. See `Viewer.svelte` for the contract.
78
+ */
79
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
66
80
  headerRight?: Snippet;
67
81
  // Replaces the built-in header; takes precedence over `headerRight`.
68
82
  header?: Snippet;
@@ -104,6 +118,7 @@
104
118
  headerRight,
105
119
  header,
106
120
  onReady,
121
+ onViewerReady,
107
122
  externalScopeKey,
108
123
  clientSlot,
109
124
  lang
@@ -152,7 +167,8 @@
152
167
  $effect(() => {
153
168
  onReady?.({
154
169
  loadValues: (incoming) => session.loadValues(incoming),
155
- getLastResult: () => session.lastResult
170
+ getLastResult: () => session.lastResult,
171
+ getSession: () => session
156
172
  });
157
173
  });
158
174
 
@@ -240,6 +256,7 @@
240
256
  bind:isViewerFullscreen
241
257
  values={session.values}
242
258
  logoUrl={logo}
259
+ {onViewerReady}
243
260
  {panelActions}
244
261
  {showSaveButton}
245
262
  {showLoadButton}
@@ -2,7 +2,8 @@ import type { UISchema, ParameterPreset } from '@selvajs/schemas';
2
2
  import type { ActionButton } from '../../types/actionButton';
3
3
  import type { SolveFn } from '@selvajs/solve/shared';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
- import type { RetainedSolveResult } from '@selvajs/solve/client';
5
+ import type { RetainedSolveResult, SolveSession } from '@selvajs/solve/client';
6
+ import type { ThreeViewer } from '@selvajs/visualization/render';
6
7
  import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
7
8
  import type { Locale } from '../../i18n/messages';
8
9
  import type { Snippet } from 'svelte';
@@ -47,7 +48,20 @@ interface Props {
47
48
  * A getter, not a snapshot: `onReady` fires once.
48
49
  */
49
50
  getLastResult: () => RetainedSolveResult | null;
51
+ /**
52
+ * The live solve session, for hosts driving solves from their own state —
53
+ * `setValue`/`solve` to push inputs, `subscribe` to react to results.
54
+ *
55
+ * A getter for the same reason as `getLastResult`. Values written here go through the
56
+ * same throttle and memo as the UI's, so it's safe to call at interaction rate.
57
+ */
58
+ getSession: () => SolveSession;
50
59
  }) => void;
60
+ /**
61
+ * Hands the live three.js viewer to the host once it mounts, for apps drawing their own
62
+ * content alongside solve results. See `Viewer.svelte` for the contract.
63
+ */
64
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
51
65
  headerRight?: Snippet;
52
66
  header?: Snippet;
53
67
  externalScopeKey?: string;
@@ -11,7 +11,8 @@
11
11
  type CameraProjection,
12
12
  type MeasureTool,
13
13
  type ViewPreset,
14
- type Grid
14
+ type Grid,
15
+ type ThreeViewer
15
16
  } from '@selvajs/visualization/render';
16
17
  import {
17
18
  Maximize,
@@ -49,7 +50,7 @@
49
50
  showGridToggle?: boolean;
50
51
  /**
51
52
  * Expose the "Display" submenu (render style picker + edges toggle) in the tools menu.
52
- * Defaults on. Starts on the 'technical' style with edges shown.
53
+ * Defaults on. Starts on the 'technical' style with edges hidden.
53
54
  */
54
55
  showDisplayMenu?: boolean;
55
56
  enableMeshClick?: boolean;
@@ -68,6 +69,16 @@
68
69
  * bottom-right corner. Omitted/empty renders nothing.
69
70
  */
70
71
  logoUrl?: string;
72
+ /**
73
+ * Hands the live three.js viewer to the host once the canvas is up, for apps drawing their
74
+ * own content — `addUserGeometry` (survives solves), `tools.register` (claim clicks before
75
+ * selection), `labelLayer`, `cameraController`.
76
+ *
77
+ * Return a cleanup function to tear down what you added; it runs before the viewer disposes.
78
+ * Anything added outside a solve needs `viewer.invalidate()` to repaint — the render loop is
79
+ * on-demand.
80
+ */
81
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
71
82
  /**
72
83
  * UI language for the viewer's own chrome (tools menu, panels, dialogs).
73
84
  * When set, the viewer provides it to its subtree. When omitted, the viewer
@@ -96,6 +107,7 @@
96
107
  drawerOpen = false,
97
108
  viewerConfig = {},
98
109
  logoUrl,
110
+ onViewerReady,
99
111
  lang
100
112
  }: Props = $props();
101
113
 
@@ -141,10 +153,10 @@
141
153
  let projection: CameraProjection = $state('perspective');
142
154
  let measureActive = $state(false);
143
155
  let gridVisible = $state(false);
144
- // Render style + edge overlays. 'technical' is the default look; edges (crease lines) start on so
145
- // the technical look reads as a CAD shaded view — both are user-switchable via the Display submenu.
156
+ // Render style + edge overlays. 'technical' is the default look; edges (crease lines) start off
157
+ // both are user-switchable via the Display submenu.
146
158
  let renderStyle: Look = $state('technical');
147
- let edgesVisible = $state(true);
159
+ let edgesVisible = $state(false);
148
160
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
149
161
  let selectedMeshName: string | null = $state(null);
150
162
 
@@ -225,7 +237,13 @@
225
237
 
226
238
  const renderer = init.renderer;
227
239
 
240
+ // Untracked: the host's setup runs once against this canvas, and reading `meshes` or config
241
+ // inside it must not re-run onMount's teardown.
242
+ const hostCleanup = untrack(() => onViewerReady?.(init));
243
+
228
244
  return () => {
245
+ // Before dispose, so the host can still remove its own objects from a live scene.
246
+ hostCleanup?.();
229
247
  init.dispose();
230
248
  // `{#key definitionKey}` recreates the canvas + WebGLRenderer + GL context
231
249
  // on every definition switch; browsers cap live contexts (~16). Explicitly
@@ -352,7 +370,12 @@
352
370
  : 'overflow-hidden rounded-[0.625rem]'}"
353
371
  >
354
372
  <Resizable.PaneGroup direction="horizontal" class="h-full w-full">
355
- <Resizable.Pane defaultSize={100} minSize={40}>
373
+ <!-- `defaultSize` must sum to 100 across the live panes. Panes register a frame before the
374
+ group recomputes its layout, so a sum of 115 renders one frame at the raw flex-grow ratio
375
+ and is then renormalized — and if the recompute short-circuits on an equal layout, the
376
+ scene pane keeps a sliver of its intended width. Hence 85 + 15, and the explicit
377
+ id/order so a conditionally-rendered pane keeps its slot. -->
378
+ <Resizable.Pane id="viewport" order={1} defaultSize={sceneManagerOpen ? 85 : 100} minSize={40}>
356
379
  <div class="relative h-full w-full" style="touch-action: none;">
357
380
  <canvas class="block h-full w-full" bind:this={canvas}></canvas>
358
381
 
@@ -565,7 +588,7 @@
565
588
  <!-- Scene Manager Pane -->
566
589
  {#if sceneManagerOpen && scene && outliner}
567
590
  <Resizable.Handle withHandle />
568
- <Resizable.Pane defaultSize={15} minSize={8} maxSize={30}>
591
+ <Resizable.Pane id="scene-manager" order={2} defaultSize={15} minSize={8} maxSize={30}>
569
592
  <SceneManager {outliner} {sceneVersion} />
570
593
  </Resizable.Pane>
571
594
  {/if}
@@ -1,3 +1,4 @@
1
+ import { type ThreeViewer } from '@selvajs/visualization/render';
1
2
  import type { Locale } from '../../i18n/messages';
2
3
  export interface ViewerConfig {
3
4
  showScreenshotButton?: boolean;
@@ -8,7 +9,7 @@ export interface ViewerConfig {
8
9
  showGridToggle?: boolean;
9
10
  /**
10
11
  * Expose the "Display" submenu (render style picker + edges toggle) in the tools menu.
11
- * Defaults on. Starts on the 'technical' style with edges shown.
12
+ * Defaults on. Starts on the 'technical' style with edges hidden.
12
13
  */
13
14
  showDisplayMenu?: boolean;
14
15
  enableMeshClick?: boolean;
@@ -26,6 +27,16 @@ interface Props {
26
27
  * bottom-right corner. Omitted/empty renders nothing.
27
28
  */
28
29
  logoUrl?: string;
30
+ /**
31
+ * Hands the live three.js viewer to the host once the canvas is up, for apps drawing their
32
+ * own content — `addUserGeometry` (survives solves), `tools.register` (claim clicks before
33
+ * selection), `labelLayer`, `cameraController`.
34
+ *
35
+ * Return a cleanup function to tear down what you added; it runs before the viewer disposes.
36
+ * Anything added outside a solve needs `viewer.invalidate()` to repaint — the render loop is
37
+ * on-demand.
38
+ */
39
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
29
40
  /**
30
41
  * UI language for the viewer's own chrome (tools menu, panels, dialogs).
31
42
  * When set, the viewer provides it to its subtree. When omitted, the viewer
package/dist/public.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { default as AppLayout } from './components/compute/AppLayout.svelte';
2
2
  export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
3
3
  export { default as Viewer, type ViewerConfig } from './components/viewer/Viewer.svelte';
4
+ export type { ThreeViewer, PointerTool, ToolRegistry, ToolRegistration, LabelLayer, LabelHandle, CameraController, ViewPreset } from '@selvajs/visualization/render';
5
+ export { appSource, isHostOwned, isOwnedBy, pointerToNdc } from '@selvajs/visualization/render';
4
6
  export type { Locale, ViewerMessages } from './i18n/messages';
5
7
  export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
6
8
  export { setLocaleContext, getLocaleContext, type LocaleContext } from './i18n/localeContext.svelte';
package/dist/public.js CHANGED
@@ -26,6 +26,7 @@ export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
26
26
  // an optional `viewerConfig`. Pass `lang` to localize its chrome, or provide a
27
27
  // locale context (setLocaleContext) at the host root to drive it app-wide.
28
28
  export { default as Viewer } from './components/viewer/Viewer.svelte';
29
+ export { appSource, isHostOwned, isOwnedBy, pointerToNdc } from '@selvajs/visualization/render';
29
30
  export { VIEWER_MESSAGES, DEFAULT_LOCALE, messagesFor } from './i18n/messages';
30
31
  export { setLocaleContext, getLocaleContext } from './i18n/localeContext.svelte';
31
32
  // Full-screen states a host app renders
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "6.0.0-beta.5",
3
+ "version": "6.0.0-beta.6",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "author": "VektorNode",
@@ -62,7 +62,7 @@
62
62
  "@selvajs/compute": "^4.0.0-beta.5",
63
63
  "@selvajs/schemas": "^5.0.0-beta.2",
64
64
  "@selvajs/solve": "^1.0.0-beta.9",
65
- "@selvajs/visualization": "^1.0.0-beta.2"
65
+ "@selvajs/visualization": "^1.0.0-beta.3"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "three": {
@@ -90,10 +90,10 @@
90
90
  "svelte": "5.56.8",
91
91
  "tailwind-variants": "^3.3.0",
92
92
  "vitest": "^4.1.10",
93
- "@selvajs/compute": "4.0.0-beta.5",
94
93
  "@selvajs/config": "0.0.3",
95
94
  "@selvajs/schemas": "5.0.0-beta.2",
96
- "@selvajs/visualization": "1.0.0-beta.2",
95
+ "@selvajs/compute": "4.0.0-beta.5",
96
+ "@selvajs/visualization": "1.0.0-beta.3",
97
97
  "@selvajs/solve": "1.0.0-beta.9"
98
98
  },
99
99
  "scripts": {
@@ -3,6 +3,7 @@
3
3
  import type { ActionButton } from '../../types/actionButton';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
5
  import { ChevronUp } from '@lucide/svelte';
6
+ import type { ThreeViewer } from '@selvajs/visualization/render';
6
7
  import Viewer, { type ViewerConfig } from '../viewer/Viewer.svelte';
7
8
  import CalculateButton from '../primitives/CalculateButton.svelte';
8
9
  import SolvingIndicator from '../primitives/SolvingIndicator.svelte';
@@ -39,6 +40,8 @@
39
40
  viewerConfig?: ViewerConfig;
40
41
  /** Branding logo URL shown as a watermark in the viewer's bottom-right corner. */
41
42
  logoUrl?: string;
43
+ /** Hands the live three.js viewer to the host. See `Viewer.svelte`. */
44
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
42
45
  }
43
46
 
44
47
  let {
@@ -60,7 +63,8 @@
60
63
  onListStates,
61
64
  presetLabels,
62
65
  viewerConfig = {},
63
- logoUrl
66
+ logoUrl,
67
+ onViewerReady
64
68
  }: Props = $props();
65
69
 
66
70
  const hasViewer = $derived(
@@ -189,6 +193,7 @@
189
193
  isBlurred={drawerOpen}
190
194
  {drawerOpen}
191
195
  {logoUrl}
196
+ {onViewerReady}
192
197
  viewerConfig={{
193
198
  ...viewerConfig,
194
199
  backgroundColor: schema?.viewerOptions?.backgroundColor,
@@ -352,6 +357,7 @@
352
357
  bind:isFullscreen={isViewerFullscreen}
353
358
  {isSolving}
354
359
  {logoUrl}
360
+ {onViewerReady}
355
361
  viewerConfig={{
356
362
  backgroundColor: schema?.viewerOptions?.backgroundColor
357
363
  }}
@@ -7,8 +7,9 @@
7
7
  import type { PresetLabels } from '../../types/presetLabels';
8
8
  import { createSolvingIndicator } from '../../compute/solving.svelte';
9
9
  import { createRequestResponseDriver } from '@selvajs/solve/client';
10
- import type { RetainedSolveResult } from '@selvajs/solve/client';
10
+ import type { RetainedSolveResult, SolveSession } from '@selvajs/solve/client';
11
11
  import { meshPolicy } from '@selvajs/visualization/parse';
12
+ import type { ThreeViewer } from '@selvajs/visualization/render';
12
13
  import { useSolveSession } from '../../compute/useSolveSession.svelte';
13
14
  import { useFooterItem } from '../../composables/useFooterItem.svelte';
14
15
  import { hexToOklch } from '../../utils/color';
@@ -62,7 +63,20 @@
62
63
  * A getter, not a snapshot: `onReady` fires once.
63
64
  */
64
65
  getLastResult: () => RetainedSolveResult | null;
66
+ /**
67
+ * The live solve session, for hosts driving solves from their own state —
68
+ * `setValue`/`solve` to push inputs, `subscribe` to react to results.
69
+ *
70
+ * A getter for the same reason as `getLastResult`. Values written here go through the
71
+ * same throttle and memo as the UI's, so it's safe to call at interaction rate.
72
+ */
73
+ getSession: () => SolveSession;
65
74
  }) => void;
75
+ /**
76
+ * Hands the live three.js viewer to the host once it mounts, for apps drawing their own
77
+ * content alongside solve results. See `Viewer.svelte` for the contract.
78
+ */
79
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
66
80
  headerRight?: Snippet;
67
81
  // Replaces the built-in header; takes precedence over `headerRight`.
68
82
  header?: Snippet;
@@ -104,6 +118,7 @@
104
118
  headerRight,
105
119
  header,
106
120
  onReady,
121
+ onViewerReady,
107
122
  externalScopeKey,
108
123
  clientSlot,
109
124
  lang
@@ -152,7 +167,8 @@
152
167
  $effect(() => {
153
168
  onReady?.({
154
169
  loadValues: (incoming) => session.loadValues(incoming),
155
- getLastResult: () => session.lastResult
170
+ getLastResult: () => session.lastResult,
171
+ getSession: () => session
156
172
  });
157
173
  });
158
174
 
@@ -240,6 +256,7 @@
240
256
  bind:isViewerFullscreen
241
257
  values={session.values}
242
258
  logoUrl={logo}
259
+ {onViewerReady}
243
260
  {panelActions}
244
261
  {showSaveButton}
245
262
  {showLoadButton}
@@ -11,7 +11,8 @@
11
11
  type CameraProjection,
12
12
  type MeasureTool,
13
13
  type ViewPreset,
14
- type Grid
14
+ type Grid,
15
+ type ThreeViewer
15
16
  } from '@selvajs/visualization/render';
16
17
  import {
17
18
  Maximize,
@@ -49,7 +50,7 @@
49
50
  showGridToggle?: boolean;
50
51
  /**
51
52
  * Expose the "Display" submenu (render style picker + edges toggle) in the tools menu.
52
- * Defaults on. Starts on the 'technical' style with edges shown.
53
+ * Defaults on. Starts on the 'technical' style with edges hidden.
53
54
  */
54
55
  showDisplayMenu?: boolean;
55
56
  enableMeshClick?: boolean;
@@ -68,6 +69,16 @@
68
69
  * bottom-right corner. Omitted/empty renders nothing.
69
70
  */
70
71
  logoUrl?: string;
72
+ /**
73
+ * Hands the live three.js viewer to the host once the canvas is up, for apps drawing their
74
+ * own content — `addUserGeometry` (survives solves), `tools.register` (claim clicks before
75
+ * selection), `labelLayer`, `cameraController`.
76
+ *
77
+ * Return a cleanup function to tear down what you added; it runs before the viewer disposes.
78
+ * Anything added outside a solve needs `viewer.invalidate()` to repaint — the render loop is
79
+ * on-demand.
80
+ */
81
+ onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
71
82
  /**
72
83
  * UI language for the viewer's own chrome (tools menu, panels, dialogs).
73
84
  * When set, the viewer provides it to its subtree. When omitted, the viewer
@@ -96,6 +107,7 @@
96
107
  drawerOpen = false,
97
108
  viewerConfig = {},
98
109
  logoUrl,
110
+ onViewerReady,
99
111
  lang
100
112
  }: Props = $props();
101
113
 
@@ -141,10 +153,10 @@
141
153
  let projection: CameraProjection = $state('perspective');
142
154
  let measureActive = $state(false);
143
155
  let gridVisible = $state(false);
144
- // Render style + edge overlays. 'technical' is the default look; edges (crease lines) start on so
145
- // the technical look reads as a CAD shaded view — both are user-switchable via the Display submenu.
156
+ // Render style + edge overlays. 'technical' is the default look; edges (crease lines) start off
157
+ // both are user-switchable via the Display submenu.
146
158
  let renderStyle: Look = $state('technical');
147
- let edgesVisible = $state(true);
159
+ let edgesVisible = $state(false);
148
160
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
149
161
  let selectedMeshName: string | null = $state(null);
150
162
 
@@ -225,7 +237,13 @@
225
237
 
226
238
  const renderer = init.renderer;
227
239
 
240
+ // Untracked: the host's setup runs once against this canvas, and reading `meshes` or config
241
+ // inside it must not re-run onMount's teardown.
242
+ const hostCleanup = untrack(() => onViewerReady?.(init));
243
+
228
244
  return () => {
245
+ // Before dispose, so the host can still remove its own objects from a live scene.
246
+ hostCleanup?.();
229
247
  init.dispose();
230
248
  // `{#key definitionKey}` recreates the canvas + WebGLRenderer + GL context
231
249
  // on every definition switch; browsers cap live contexts (~16). Explicitly
@@ -352,7 +370,12 @@
352
370
  : 'overflow-hidden rounded-[0.625rem]'}"
353
371
  >
354
372
  <Resizable.PaneGroup direction="horizontal" class="h-full w-full">
355
- <Resizable.Pane defaultSize={100} minSize={40}>
373
+ <!-- `defaultSize` must sum to 100 across the live panes. Panes register a frame before the
374
+ group recomputes its layout, so a sum of 115 renders one frame at the raw flex-grow ratio
375
+ and is then renormalized — and if the recompute short-circuits on an equal layout, the
376
+ scene pane keeps a sliver of its intended width. Hence 85 + 15, and the explicit
377
+ id/order so a conditionally-rendered pane keeps its slot. -->
378
+ <Resizable.Pane id="viewport" order={1} defaultSize={sceneManagerOpen ? 85 : 100} minSize={40}>
356
379
  <div class="relative h-full w-full" style="touch-action: none;">
357
380
  <canvas class="block h-full w-full" bind:this={canvas}></canvas>
358
381
 
@@ -565,7 +588,7 @@
565
588
  <!-- Scene Manager Pane -->
566
589
  {#if sceneManagerOpen && scene && outliner}
567
590
  <Resizable.Handle withHandle />
568
- <Resizable.Pane defaultSize={15} minSize={8} maxSize={30}>
591
+ <Resizable.Pane id="scene-manager" order={2} defaultSize={15} minSize={8} maxSize={30}>
569
592
  <SceneManager {outliner} {sceneVersion} />
570
593
  </Resizable.Pane>
571
594
  {/if}
package/src/lib/public.ts CHANGED
@@ -29,6 +29,24 @@ export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
29
29
  // locale context (setLocaleContext) at the host root to drive it app-wide.
30
30
  export { default as Viewer, type ViewerConfig } from './components/viewer/Viewer.svelte';
31
31
 
32
+ // Viewer app seam. `onViewerReady` (on <Viewer> and <ComputeApp>) hands over the
33
+ // live three.js viewer, so a host can draw its own content into the same scene as
34
+ // the solve results — a point cloud, draft lines, annotations — and register
35
+ // pointer tools that claim clicks ahead of object selection. Re-exported from
36
+ // @selvajs/visualization/render so hosts can annotate without depending on it
37
+ // directly; `three` stays a peer dep of that package either way.
38
+ export type {
39
+ ThreeViewer,
40
+ PointerTool,
41
+ ToolRegistry,
42
+ ToolRegistration,
43
+ LabelLayer,
44
+ LabelHandle,
45
+ CameraController,
46
+ ViewPreset
47
+ } from '@selvajs/visualization/render';
48
+ export { appSource, isHostOwned, isOwnedBy, pointerToNdc } from '@selvajs/visualization/render';
49
+
32
50
  // Viewer localization. The library renders English + German chrome; switch at
33
51
  // runtime by passing `lang` to <Viewer> or by setting a reactive locale context
34
52
  // once at the host root (e.g. feed in an app-wide Paraglide locale). Does not