@selvajs/ui 6.1.2 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ConfirmDialog.svelte +59 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +13 -0
- package/dist/components/compute/ComputeApp.svelte +10 -0
- package/dist/components/compute/ComputeApp.svelte.d.ts +3 -0
- package/dist/components/compute/ParameterPresetManager.svelte +3 -3
- package/dist/components/layout/AppShell.svelte +19 -2
- package/dist/components/layout/SideNav.svelte +16 -2
- package/dist/components/layout/SideNav.svelte.d.ts +5 -0
- package/dist/components/preview/inputs/FileInput.svelte +3 -2
- package/dist/components/primitives/Callout.svelte +73 -0
- package/dist/components/primitives/Callout.svelte.d.ts +40 -0
- package/dist/components/primitives/index.d.ts +2 -0
- package/dist/components/primitives/index.js +2 -0
- package/dist/components/viewer/MeshMetadataDialog.svelte +1 -1
- package/dist/components/viewer/SceneManager.svelte +274 -103
- package/dist/components/viewer/SceneManager.svelte.d.ts +6 -0
- package/dist/components/viewer/Viewer.svelte +104 -32
- package/dist/i18n/messages.d.ts +4 -2
- package/dist/i18n/messages.js +8 -4
- package/package.json +14 -14
- package/src/lib/components/ConfirmDialog.svelte +59 -0
- package/src/lib/components/compute/ComputeApp.svelte +10 -0
- package/src/lib/components/compute/ParameterPresetManager.svelte +3 -3
- package/src/lib/components/layout/AppShell.svelte +19 -2
- package/src/lib/components/layout/SideNav.svelte +16 -2
- package/src/lib/components/preview/inputs/FileInput.svelte +3 -2
- package/src/lib/components/primitives/Callout.svelte +73 -0
- package/src/lib/components/primitives/index.ts +2 -0
- package/src/lib/components/viewer/MeshMetadataDialog.svelte +1 -1
- package/src/lib/components/viewer/SceneManager.svelte +274 -103
- package/src/lib/components/viewer/Viewer.svelte +104 -32
- package/src/lib/i18n/messages.ts +12 -6
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
initThree,
|
|
6
6
|
updateScene,
|
|
7
7
|
LOOKS,
|
|
8
|
+
LOOK_PRESETS,
|
|
8
9
|
type Look,
|
|
9
10
|
type ThreeInitializerOptions,
|
|
10
11
|
type CameraController,
|
|
@@ -118,11 +119,13 @@
|
|
|
118
119
|
let cameraController: CameraController | null = null;
|
|
119
120
|
let measureTool: MeasureTool | null = null;
|
|
120
121
|
let grid: Grid | null = null;
|
|
121
|
-
let applyEdges:
|
|
122
|
-
let clearEdges:
|
|
122
|
+
let applyEdges: ThreeViewer['applyEdges'] | null = null;
|
|
123
|
+
let clearEdges: ThreeViewer['clearEdges'] | null = null;
|
|
123
124
|
let invalidate: (() => void) | null = null;
|
|
125
|
+
let captureImage: ThreeViewer['captureImage'] | null = null;
|
|
124
126
|
let setLook: ((look: Look) => void) | null = null;
|
|
125
127
|
let updateGridScale: (() => void) | null = null;
|
|
128
|
+
let updateShadowBounds: (() => void) | null = null;
|
|
126
129
|
let fitToView: (() => void) | null = null;
|
|
127
130
|
let viewerInitialized = false;
|
|
128
131
|
let sceneVersion = $state(0);
|
|
@@ -144,10 +147,15 @@
|
|
|
144
147
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
145
148
|
let selectedMeshName: string | null = $state(null);
|
|
146
149
|
|
|
147
|
-
// Derived from LOOKS so adding a look in @selvajs/visualization shows up here with no edit
|
|
150
|
+
// Derived from LOOKS so adding a look in @selvajs/visualization shows up here with no edit; the
|
|
151
|
+
// map only overrides the names that don't survive capitalisation ('xray' → 'X-Ray').
|
|
152
|
+
const LOOK_LABELS: Partial<Record<Look, string>> = {
|
|
153
|
+
xray: 'X-Ray',
|
|
154
|
+
lineart: 'Line Art'
|
|
155
|
+
};
|
|
148
156
|
const STYLE_OPTIONS: { look: Look; label: string }[] = LOOKS.map((look) => ({
|
|
149
157
|
look,
|
|
150
|
-
label: look.charAt(0).toUpperCase() + look.slice(1)
|
|
158
|
+
label: LOOK_LABELS[look] ?? look.charAt(0).toUpperCase() + look.slice(1)
|
|
151
159
|
}));
|
|
152
160
|
|
|
153
161
|
const VIEW_PRESETS: { preset: ViewPreset; label: () => string }[] = [
|
|
@@ -175,12 +183,11 @@
|
|
|
175
183
|
onMount(() => {
|
|
176
184
|
if (!canvas) return;
|
|
177
185
|
|
|
178
|
-
// Only what differs from the library defaults.
|
|
179
|
-
//
|
|
186
|
+
// Only what differs from the library defaults. Sunlight, shadows and AO are left ON (the
|
|
187
|
+
// library default) and their strength comes from the look — with IBL alone every face of a
|
|
188
|
+
// box lights nearly equally and the model reads as a flat white silhouette.
|
|
180
189
|
const opts: ThreeInitializerOptions = {
|
|
181
190
|
look: renderStyle,
|
|
182
|
-
lighting: { enableSunlight: false },
|
|
183
|
-
render: { enableShadows: false },
|
|
184
191
|
environment: { backgroundColor: config.backgroundColor },
|
|
185
192
|
grid: { enabled: config.showToolsMenu && config.showGridToggle },
|
|
186
193
|
measure: { enabled: config.showToolsMenu },
|
|
@@ -210,8 +217,10 @@
|
|
|
210
217
|
applyEdges = init.applyEdges;
|
|
211
218
|
clearEdges = init.clearEdges;
|
|
212
219
|
invalidate = init.invalidate;
|
|
220
|
+
captureImage = init.captureImage;
|
|
213
221
|
setLook = init.setLook;
|
|
214
222
|
updateGridScale = init.updateGridScale;
|
|
223
|
+
updateShadowBounds = init.updateShadowBounds;
|
|
215
224
|
fitToView = init.fitToView;
|
|
216
225
|
projection = init.cameraController.getProjection();
|
|
217
226
|
|
|
@@ -253,22 +262,84 @@
|
|
|
253
262
|
invalidate?.();
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
// Edges the lineart look switched on, so leaving it can put the user's own choice back rather
|
|
266
|
+
// than stranding them with overlays they never asked for.
|
|
267
|
+
let edgesBeforeLineDrawing: boolean | null = null;
|
|
268
|
+
|
|
256
269
|
function setRenderStyle(look: Look) {
|
|
257
270
|
if (!setLook || look === renderStyle) return;
|
|
271
|
+
const wasLineDrawing = LOOK_PRESETS[renderStyle].requiresEdges === true;
|
|
258
272
|
renderStyle = look;
|
|
259
273
|
setLook(look);
|
|
274
|
+
|
|
275
|
+
// A look never drives the overlay itself; honouring `requiresEdges` is the host's call. Without
|
|
276
|
+
// edges lineart is blank white shapes, so entering it forces them on.
|
|
277
|
+
if (LOOK_PRESETS[look].requiresEdges) {
|
|
278
|
+
if (!wasLineDrawing) edgesBeforeLineDrawing = edgesVisible;
|
|
279
|
+
if (!edgesVisible) {
|
|
280
|
+
edgesVisible = true;
|
|
281
|
+
} else if (!wasLineDrawing && scene) {
|
|
282
|
+
// Already-attached overlays carry the old fade setting, and addEdges skips a mesh that
|
|
283
|
+
// has one — so they have to go before the un-faded pass can replace them.
|
|
284
|
+
clearEdges?.(scene);
|
|
285
|
+
}
|
|
286
|
+
applyEdgeState();
|
|
287
|
+
} else if (wasLineDrawing && edgesBeforeLineDrawing !== null) {
|
|
288
|
+
edgesVisible = edgesBeforeLineDrawing;
|
|
289
|
+
// Rebuild either way: staying on means swapping the un-faded overlays back for faded ones.
|
|
290
|
+
if (edgesVisible && scene) clearEdges?.(scene);
|
|
291
|
+
applyEdgeState();
|
|
292
|
+
edgesBeforeLineDrawing = null;
|
|
293
|
+
} else if (edgesVisible && scene) {
|
|
294
|
+
// Neither look is a line drawing, but an overlay's colour is derived from its mesh's
|
|
295
|
+
// material — which `setLook` just repainted. addEdges skips meshes that already have an
|
|
296
|
+
// overlay, so without a rebuild the edges keep the previous look's colour.
|
|
297
|
+
clearEdges?.(scene);
|
|
298
|
+
applyEdgeState();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Meshes past the overlay budget get the screen-space fallback instead, and that pass thresholds
|
|
303
|
+
// a depth/normal discontinuity to decide what is an edge — so a shallow crease sitting near the
|
|
304
|
+
// cutoff flips on and off as the camera turns a degree. Tolerable as a hint over a shaded model;
|
|
305
|
+
// in a line drawing it is the picture itself flickering. Raised far enough that a building model
|
|
306
|
+
// gets real overlays throughout: they cost draw calls, but they are stable under orbit.
|
|
307
|
+
const LINE_ART_MAX_OVERLAYS = 20_000;
|
|
308
|
+
|
|
309
|
+
// Edge colour is normally derived per mesh from that mesh's own material, darkened. lineart
|
|
310
|
+
// repaints every material near-white, so a derived edge lands at rgb(62,62,62) or lighter — and
|
|
311
|
+
// worse, `setLook` and `applyEdges` are independent, so whether the derivation sees the white
|
|
312
|
+
// override or the model's original colours depends on which ran last. That is the real source of
|
|
313
|
+
// the "edges look different every time" behaviour. Forcing a colour makes it deterministic.
|
|
314
|
+
const LINE_ART_EDGE_COLOR = 0x1a1d21;
|
|
315
|
+
|
|
316
|
+
// The density fade sets opacity per overlay, so a finely-detailed mesh fades as a whole and its
|
|
317
|
+
// long silhouette edges go with it — in a line drawing that erases the only thing on screen, and
|
|
318
|
+
// it recomputes per frame, so edges pop while orbiting. Shaded looks keep it: there it just
|
|
319
|
+
// softens outlines on a model you can still see.
|
|
320
|
+
function edgeOverrides() {
|
|
321
|
+
return LOOK_PRESETS[renderStyle].requiresEdges
|
|
322
|
+
? {
|
|
323
|
+
distanceFade: false,
|
|
324
|
+
maxOverlays: LINE_ART_MAX_OVERLAYS,
|
|
325
|
+
color: LINE_ART_EDGE_COLOR
|
|
326
|
+
}
|
|
327
|
+
: undefined;
|
|
260
328
|
}
|
|
261
329
|
|
|
262
330
|
// `applyEdges` is idempotent per mesh, so the repeated calls after each solve add no duplicate
|
|
263
331
|
// overlays; `clearEdges` is its inverse.
|
|
264
332
|
function applyEdgeState() {
|
|
265
333
|
if (!scene) return;
|
|
266
|
-
if (edgesVisible) applyEdges?.(scene);
|
|
334
|
+
if (edgesVisible) applyEdges?.(scene, edgeOverrides());
|
|
267
335
|
else clearEdges?.(scene);
|
|
268
336
|
}
|
|
269
337
|
|
|
270
338
|
function toggleEdges() {
|
|
271
339
|
edgesVisible = !edgesVisible;
|
|
340
|
+
// Toggling by hand inside lineart overrides what that look forced, so the pre-look value is
|
|
341
|
+
// stale — leaving the look must not undo the user's more recent explicit choice.
|
|
342
|
+
if (LOOK_PRESETS[renderStyle].requiresEdges) edgesBeforeLineDrawing = null;
|
|
272
343
|
applyEdgeState();
|
|
273
344
|
}
|
|
274
345
|
|
|
@@ -278,10 +349,16 @@
|
|
|
278
349
|
// Untracked because toggleEdges() already handles the toggle directly — reading
|
|
279
350
|
// `edgesVisible` tracked here would re-trigger a full solve.
|
|
280
351
|
untrack(() => {
|
|
352
|
+
// The new meshes carry the materials the parser built, so a look that overrides them
|
|
353
|
+
// (arctic, x-ray) has to be re-applied or the solve silently reverts to shaded.
|
|
354
|
+
setLook?.(renderStyle);
|
|
281
355
|
// updateScene discarded the previous solve's overlays along with its content.
|
|
282
|
-
if (edgesVisible) applyEdges?.(scene
|
|
356
|
+
if (edgesVisible) applyEdges?.(scene!, edgeOverrides());
|
|
283
357
|
// Rescale the grid so cells and fade match the new content's extent.
|
|
284
358
|
updateGridScale?.();
|
|
359
|
+
// The shadow frustum is sized to scene content, so it has to follow the new geometry —
|
|
360
|
+
// left at the old extent, shadows go blocky or fall outside the map entirely.
|
|
361
|
+
updateShadowBounds?.();
|
|
285
362
|
// The rebuild un-hid everything; the outliner keys hidden state on Grasshopper
|
|
286
363
|
// identity, not on the instances just discarded, so it can re-hide it.
|
|
287
364
|
outliner?.applyTo();
|
|
@@ -311,8 +388,7 @@
|
|
|
311
388
|
const EXCLUDED_KEYS = new Set([
|
|
312
389
|
'name',
|
|
313
390
|
'layer',
|
|
314
|
-
'
|
|
315
|
-
'sourceComponentId',
|
|
391
|
+
'id',
|
|
316
392
|
'vertexCount',
|
|
317
393
|
'faceCount',
|
|
318
394
|
'vertexOffset',
|
|
@@ -322,23 +398,19 @@
|
|
|
322
398
|
return usefulEntries.length > 0;
|
|
323
399
|
}
|
|
324
400
|
|
|
325
|
-
function downloadScreenshot() {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
document.body.removeChild(link);
|
|
339
|
-
URL.revokeObjectURL(url);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
401
|
+
async function downloadScreenshot() {
|
|
402
|
+
const blob = await captureImage?.();
|
|
403
|
+
if (!blob) return;
|
|
404
|
+
|
|
405
|
+
const url = URL.createObjectURL(blob);
|
|
406
|
+
const link = document.createElement('a');
|
|
407
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
408
|
+
link.href = url;
|
|
409
|
+
link.download = `viewer-${timestamp}.png`;
|
|
410
|
+
document.body.appendChild(link);
|
|
411
|
+
link.click();
|
|
412
|
+
document.body.removeChild(link);
|
|
413
|
+
URL.revokeObjectURL(url);
|
|
342
414
|
}
|
|
343
415
|
</script>
|
|
344
416
|
|
|
@@ -422,10 +494,10 @@
|
|
|
422
494
|
<DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
|
|
423
495
|
{#if projection === 'perspective'}
|
|
424
496
|
<Square class="h-4 w-4" />
|
|
425
|
-
{t.
|
|
497
|
+
{t.switchToOrthographic}
|
|
426
498
|
{:else}
|
|
427
499
|
<Box class="h-4 w-4" />
|
|
428
|
-
{t.
|
|
500
|
+
{t.switchToPerspective}
|
|
429
501
|
{/if}
|
|
430
502
|
</DropdownMenu.Item>
|
|
431
503
|
|
|
@@ -569,7 +641,7 @@
|
|
|
569
641
|
{#if sceneManagerOpen && scene && outliner}
|
|
570
642
|
<Resizable.Handle withHandle />
|
|
571
643
|
<Resizable.Pane id="scene-manager" order={2} defaultSize={15} minSize={8} maxSize={30}>
|
|
572
|
-
<SceneManager {outliner} {sceneVersion} />
|
|
644
|
+
<SceneManager {outliner} {sceneVersion} onVisibilityChange={() => invalidate?.()} />
|
|
573
645
|
</Resizable.Pane>
|
|
574
646
|
{/if}
|
|
575
647
|
</Resizable.PaneGroup>
|
package/src/lib/i18n/messages.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type Locale = 'en' | 'de';
|
|
|
11
11
|
export interface ViewerMessages {
|
|
12
12
|
// Tools menu
|
|
13
13
|
toolsMenu: string;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
switchToOrthographic: string;
|
|
15
|
+
switchToPerspective: string;
|
|
16
16
|
fitToView: string;
|
|
17
17
|
views: string;
|
|
18
18
|
measure: string;
|
|
@@ -38,6 +38,8 @@ export interface ViewerMessages {
|
|
|
38
38
|
clearSearch: string;
|
|
39
39
|
expandLayer: string;
|
|
40
40
|
collapseLayer: string;
|
|
41
|
+
expandAll: string;
|
|
42
|
+
collapseAll: string;
|
|
41
43
|
showLayer: string;
|
|
42
44
|
hideLayer: string;
|
|
43
45
|
showObject: string;
|
|
@@ -73,8 +75,8 @@ export interface ViewerMessages {
|
|
|
73
75
|
|
|
74
76
|
const en: ViewerMessages = {
|
|
75
77
|
toolsMenu: 'Viewer tools',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
switchToOrthographic: 'Orthographic camera',
|
|
79
|
+
switchToPerspective: 'Perspective camera',
|
|
78
80
|
fitToView: 'Fit to view',
|
|
79
81
|
views: 'Views',
|
|
80
82
|
measure: 'Measure',
|
|
@@ -98,6 +100,8 @@ const en: ViewerMessages = {
|
|
|
98
100
|
clearSearch: 'Clear search',
|
|
99
101
|
expandLayer: 'Expand layer',
|
|
100
102
|
collapseLayer: 'Collapse layer',
|
|
103
|
+
expandAll: 'Expand all',
|
|
104
|
+
collapseAll: 'Collapse all',
|
|
101
105
|
showLayer: 'Show layer',
|
|
102
106
|
hideLayer: 'Hide layer',
|
|
103
107
|
showObject: 'Show object',
|
|
@@ -128,8 +132,8 @@ const en: ViewerMessages = {
|
|
|
128
132
|
|
|
129
133
|
const de: ViewerMessages = {
|
|
130
134
|
toolsMenu: 'Viewer-Werkzeuge',
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
switchToOrthographic: 'Orthografische Kamera',
|
|
136
|
+
switchToPerspective: 'Perspektivische Kamera',
|
|
133
137
|
fitToView: 'Ansicht anpassen',
|
|
134
138
|
views: 'Ansichten',
|
|
135
139
|
measure: 'Messen',
|
|
@@ -153,6 +157,8 @@ const de: ViewerMessages = {
|
|
|
153
157
|
clearSearch: 'Suche löschen',
|
|
154
158
|
expandLayer: 'Ebene aufklappen',
|
|
155
159
|
collapseLayer: 'Ebene zuklappen',
|
|
160
|
+
expandAll: 'Alle aufklappen',
|
|
161
|
+
collapseAll: 'Alle zuklappen',
|
|
156
162
|
showLayer: 'Ebene einblenden',
|
|
157
163
|
hideLayer: 'Ebene ausblenden',
|
|
158
164
|
showObject: 'Objekt einblenden',
|