@selvajs/ui 6.6.0-beta.14 → 6.6.0-beta.15

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.
@@ -347,7 +347,8 @@
347
347
  {onViewerReady}
348
348
  viewerConfig={{
349
349
  ...viewerConfig,
350
- backgroundColor: schema?.viewerOptions?.backgroundColor,
350
+ backgroundColor: schema?.viewerOptions?.backgroundColor ?? viewerConfig.backgroundColor,
351
+ look: schema?.viewerOptions?.look ?? viewerConfig.look,
351
352
  showSceneManager: false
352
353
  }}
353
354
  />
@@ -490,7 +491,9 @@
490
491
  {onViewerReady}
491
492
  viewerConfig={{
492
493
  ...viewerConfig,
493
- backgroundColor: schema?.viewerOptions?.backgroundColor
494
+ backgroundColor:
495
+ schema?.viewerOptions?.backgroundColor ?? viewerConfig.backgroundColor,
496
+ look: schema?.viewerOptions?.look ?? viewerConfig.look
494
497
  }}
495
498
  />
496
499
  </Resizable.Pane>
@@ -23,7 +23,7 @@
23
23
 
24
24
  <ToggleGroup.Root
25
25
  type="single"
26
- variant="outline"
26
+ variant="accent"
27
27
  size={hasIcons ? 'stacked' : 'default'}
28
28
  class="w-full"
29
29
  aria-labelledby={labelId}
@@ -45,6 +45,7 @@
45
45
  bind:this={tiles[index]}
46
46
  type="button"
47
47
  role="radio"
48
+ title={option.name}
48
49
  aria-checked={selected}
49
50
  tabindex={index === focusIndex ? 0 : -1}
50
51
  {disabled}
@@ -65,7 +66,7 @@
65
66
  <span class="text-lg font-medium" aria-hidden="true">{option.name.charAt(0)}</span>
66
67
  {/if}
67
68
  </span>
68
- <span class="px-0.5 text-xs leading-tight truncate" class:font-medium={selected}>
69
+ <span class="px-0.5 text-xs leading-tight line-clamp-2" class:font-medium={selected}>
69
70
  {option.name}
70
71
  </span>
71
72
  </button>
@@ -6,7 +6,10 @@
6
6
  variants: {
7
7
  variant: {
8
8
  default: 'bg-transparent',
9
- outline: 'border-input hover:bg-muted border bg-transparent'
9
+ outline: 'border-input hover:bg-muted border bg-transparent',
10
+ // Filled accent for the active item; a bare bg-muted is too faint on a form.
11
+ accent:
12
+ 'border-input hover:bg-muted border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset'
10
13
  },
11
14
  size: {
12
15
  default:
@@ -3,6 +3,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
3
3
  variant: {
4
4
  default: "bg-transparent";
5
5
  outline: "border-input hover:bg-muted border bg-transparent";
6
+ accent: "border-input hover:bg-muted border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset";
6
7
  };
7
8
  size: {
8
9
  default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
@@ -14,6 +15,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
14
15
  variant: {
15
16
  default: "bg-transparent";
16
17
  outline: "border-input hover:bg-muted border bg-transparent";
18
+ accent: "border-input hover:bg-muted border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset";
17
19
  };
18
20
  size: {
19
21
  default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
@@ -25,6 +27,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
25
27
  variant: {
26
28
  default: "bg-transparent";
27
29
  outline: "border-input hover:bg-muted border bg-transparent";
30
+ accent: "border-input hover:bg-muted border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset";
28
31
  };
29
32
  size: {
30
33
  default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
@@ -23,7 +23,7 @@
23
23
  data-size={ctx.size || size}
24
24
  data-spacing={ctx.spacing}
25
25
  class={cn(
26
- 'group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 shrink-0 group-data-[spacing=0]/toggle-group:rounded-none focus:z-10 focus-visible:z-10 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
26
+ 'group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 shrink-0 group-data-[spacing=0]/toggle-group:rounded-none focus:z-10 focus-visible:z-10 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=accent]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=accent]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=accent]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=accent]:first:border-t group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
27
27
  toggleVariants({
28
28
  variant: ctx.variant || variant,
29
29
  size: ctx.size || size
@@ -52,6 +52,8 @@
52
52
  showDisplayMenu?: boolean;
53
53
  enableMeshClick?: boolean;
54
54
  backgroundColor?: string;
55
+ /** The look to open in. Default `technical`. */
56
+ look?: Look;
55
57
  /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
56
58
  showEdges?: boolean;
57
59
  }
@@ -91,6 +93,7 @@
91
93
  showDisplayMenu: true,
92
94
  enableMeshClick: true,
93
95
  backgroundColor: '#E6E6E6',
96
+ look: 'technical',
94
97
  showEdges: false
95
98
  };
96
99
 
@@ -124,7 +127,8 @@
124
127
  let clearEdges: ThreeViewer['clearEdges'] | null = null;
125
128
  let invalidate: (() => void) | null = null;
126
129
  let captureImage: ThreeViewer['captureImage'] | null = null;
127
- let setLook: ((look: Look) => void) | null = null;
130
+ let setLook: ThreeViewer['setLook'] | null = null;
131
+ let syncEmissiveLights: (() => void) | null = null;
128
132
  let updateGridScale: (() => void) | null = null;
129
133
  let updateShadowBounds: (() => void) | null = null;
130
134
  let fitToView: (() => void) | null = null;
@@ -145,9 +149,16 @@
145
149
  let projection: CameraProjection = $state('perspective');
146
150
  let measureActive = $state(false);
147
151
  let gridVisible = $state(false);
148
- let renderStyle: Look = $state('technical');
152
+ // A saved schema can name a look this build doesn't have, and an unknown look throws on mount.
153
+ const openingLook = untrack(() => {
154
+ const look = viewerConfig.look;
155
+ return look && LOOKS.includes(look) ? look : defaultViewerConfig.look;
156
+ });
157
+ const opensAsLineDrawing = LOOK_PRESETS[openingLook].requiresEdges === true;
158
+ const configuredEdges = untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges;
159
+ let renderStyle: Look = $state(openingLook);
149
160
  // Seeded once, then owned by the menu toggle.
150
- let edgesVisible = $state(untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges);
161
+ let edgesVisible = $state(opensAsLineDrawing || configuredEdges);
151
162
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
152
163
  let selectedMeshName: string | null = $state(null);
153
164
 
@@ -160,7 +171,8 @@
160
171
  arctic: t.lookArctic,
161
172
  xray: t.lookXray,
162
173
  lineart: t.lookLineart,
163
- wireframe: t.lookWireframe
174
+ wireframe: t.lookWireframe,
175
+ night: t.lookNight
164
176
  });
165
177
  const styleOptions = $derived<{ look: Look; label: string }[]>(
166
178
  LOOKS.map((look) => ({
@@ -234,6 +246,7 @@
234
246
  invalidate = init.invalidate;
235
247
  captureImage = init.captureImage;
236
248
  setLook = init.setLook;
249
+ syncEmissiveLights = init.syncEmissiveLights;
237
250
  updateGridScale = init.updateGridScale;
238
251
  updateShadowBounds = init.updateShadowBounds;
239
252
  fitToView = init.fitToView;
@@ -279,7 +292,7 @@
279
292
 
280
293
  // Edges the lineart look switched on, so leaving it can put the user's own choice back rather
281
294
  // than stranding them with overlays they never asked for.
282
- let edgesBeforeLineDrawing: boolean | null = null;
295
+ let edgesBeforeLineDrawing: boolean | null = opensAsLineDrawing ? configuredEdges : null;
283
296
 
284
297
  function setRenderStyle(look: Look) {
285
298
  if (!setLook || look === renderStyle) return;
@@ -366,7 +379,8 @@
366
379
  untrack(() => {
367
380
  // The new meshes carry the materials the parser built, so a look that overrides them
368
381
  // (arctic, x-ray) has to be re-applied or the solve silently reverts to shaded.
369
- setLook?.(renderStyle);
382
+ // Emissive lights wait for `applyTo` below, or a hidden lamp would count as lit.
383
+ setLook?.(renderStyle, { deferEmissiveLights: true });
370
384
  // updateScene discarded the previous solve's overlays along with its content.
371
385
  if (edgesVisible) applyEdges?.(scene!, edgeOverrides());
372
386
  // Rescale the grid so cells and fade match the new content's extent.
@@ -377,6 +391,7 @@
377
391
  // The rebuild un-hid everything; the outliner keys hidden state on Grasshopper
378
392
  // identity, not on the instances just discarded, so it can re-hide it.
379
393
  outliner?.applyTo();
394
+ syncEmissiveLights?.();
380
395
  sceneVersion++;
381
396
  // Repaint now rather than on the render loop's safety interval.
382
397
  invalidate?.();
@@ -680,7 +695,7 @@
680
695
  <SceneManager
681
696
  {outliner}
682
697
  {sceneVersion}
683
- onVisibilityChange={() => invalidate?.()}
698
+ onVisibilityChange={() => syncEmissiveLights?.()}
684
699
  onHover={(entry) => setHover?.(entry?.object ?? null, entry?.memberIndex ?? null)}
685
700
  />
686
701
  </Resizable.Pane>
@@ -1,4 +1,4 @@
1
- import { type ThreeViewer } from '@selvajs/visualization/render';
1
+ import { type Look, type ThreeViewer } from '@selvajs/visualization/render';
2
2
  import type { Locale } from '../../i18n/messages';
3
3
  export interface ViewerConfig {
4
4
  showScreenshotButton?: boolean;
@@ -9,6 +9,8 @@ export interface ViewerConfig {
9
9
  showDisplayMenu?: boolean;
10
10
  enableMeshClick?: boolean;
11
11
  backgroundColor?: string;
12
+ /** The look to open in. Default `technical`. */
13
+ look?: Look;
12
14
  /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
13
15
  showEdges?: boolean;
14
16
  }
@@ -23,6 +23,7 @@ export interface ViewerMessages {
23
23
  lookTechnical: string;
24
24
  lookStudio: string;
25
25
  lookShowcase: string;
26
+ lookNight: string;
26
27
  lookArctic: string;
27
28
  lookXray: string;
28
29
  lookLineart: string;
@@ -32,6 +32,7 @@ const en = {
32
32
  lookTechnical: 'Technical',
33
33
  lookStudio: 'Studio',
34
34
  lookShowcase: 'Showcase',
35
+ lookNight: 'Night',
35
36
  lookArctic: 'Arctic',
36
37
  lookXray: 'X-Ray',
37
38
  lookLineart: 'Line Art',
@@ -206,6 +207,7 @@ const de = {
206
207
  lookTechnical: 'Technisch',
207
208
  lookStudio: 'Studio',
208
209
  lookShowcase: 'Präsentation',
210
+ lookNight: 'Nacht',
209
211
  lookArctic: 'Arktis',
210
212
  lookXray: 'Röntgen',
211
213
  lookLineart: 'Strichzeichnung',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "6.6.0-beta.14",
3
+ "version": "6.6.0-beta.15",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "author": "VektorNode",
@@ -63,10 +63,10 @@
63
63
  "svelte": "^5",
64
64
  "tailwind-variants": "^3.3.1",
65
65
  "three": "^0.186.1",
66
- "@selvajs/compute": "^5.0.0-beta.8",
67
- "@selvajs/schemas": "^5.3.0-beta.1",
68
- "@selvajs/solve": "^2.0.0-beta.10",
69
- "@selvajs/visualization": "^1.6.0-beta.8"
66
+ "@selvajs/compute": "^5.0.0-beta.9",
67
+ "@selvajs/visualization": "^1.6.0-beta.9",
68
+ "@selvajs/schemas": "^5.3.0-beta.2",
69
+ "@selvajs/solve": "^2.0.0-beta.12"
70
70
  },
71
71
  "peerDependenciesMeta": {
72
72
  "three": {
@@ -94,11 +94,11 @@
94
94
  "svelte": "5.57.2",
95
95
  "tailwind-variants": "^3.3.1",
96
96
  "vitest": "^5.0.5",
97
- "@selvajs/compute": "5.0.0-beta.8",
98
- "@selvajs/schemas": "5.3.0-beta.1",
99
- "@selvajs/visualization": "1.6.0-beta.8",
97
+ "@selvajs/compute": "5.0.0-beta.9",
98
+ "@selvajs/visualization": "1.6.0-beta.9",
99
+ "@selvajs/solve": "2.0.0-beta.12",
100
100
  "@selvajs/config": "0.0.4",
101
- "@selvajs/solve": "2.0.0-beta.10"
101
+ "@selvajs/schemas": "5.3.0-beta.2"
102
102
  },
103
103
  "scripts": {
104
104
  "predev": "node ../../scripts/sync-shared-assets.js",
@@ -347,7 +347,8 @@
347
347
  {onViewerReady}
348
348
  viewerConfig={{
349
349
  ...viewerConfig,
350
- backgroundColor: schema?.viewerOptions?.backgroundColor,
350
+ backgroundColor: schema?.viewerOptions?.backgroundColor ?? viewerConfig.backgroundColor,
351
+ look: schema?.viewerOptions?.look ?? viewerConfig.look,
351
352
  showSceneManager: false
352
353
  }}
353
354
  />
@@ -490,7 +491,9 @@
490
491
  {onViewerReady}
491
492
  viewerConfig={{
492
493
  ...viewerConfig,
493
- backgroundColor: schema?.viewerOptions?.backgroundColor
494
+ backgroundColor:
495
+ schema?.viewerOptions?.backgroundColor ?? viewerConfig.backgroundColor,
496
+ look: schema?.viewerOptions?.look ?? viewerConfig.look
494
497
  }}
495
498
  />
496
499
  </Resizable.Pane>
@@ -23,7 +23,7 @@
23
23
 
24
24
  <ToggleGroup.Root
25
25
  type="single"
26
- variant="outline"
26
+ variant="accent"
27
27
  size={hasIcons ? 'stacked' : 'default'}
28
28
  class="w-full"
29
29
  aria-labelledby={labelId}
@@ -45,6 +45,7 @@
45
45
  bind:this={tiles[index]}
46
46
  type="button"
47
47
  role="radio"
48
+ title={option.name}
48
49
  aria-checked={selected}
49
50
  tabindex={index === focusIndex ? 0 : -1}
50
51
  {disabled}
@@ -65,7 +66,7 @@
65
66
  <span class="text-lg font-medium" aria-hidden="true">{option.name.charAt(0)}</span>
66
67
  {/if}
67
68
  </span>
68
- <span class="px-0.5 text-xs leading-tight truncate" class:font-medium={selected}>
69
+ <span class="px-0.5 text-xs leading-tight line-clamp-2" class:font-medium={selected}>
69
70
  {option.name}
70
71
  </span>
71
72
  </button>
@@ -6,7 +6,10 @@
6
6
  variants: {
7
7
  variant: {
8
8
  default: 'bg-transparent',
9
- outline: 'border-input hover:bg-muted border bg-transparent'
9
+ outline: 'border-input hover:bg-muted border bg-transparent',
10
+ // Filled accent for the active item; a bare bg-muted is too faint on a form.
11
+ accent:
12
+ 'border-input hover:bg-muted border bg-transparent data-[state=on]:bg-primary/15 data-[state=on]:ring-1 data-[state=on]:ring-primary data-[state=on]:ring-inset'
10
13
  },
11
14
  size: {
12
15
  default:
@@ -23,7 +23,7 @@
23
23
  data-size={ctx.size || size}
24
24
  data-spacing={ctx.spacing}
25
25
  class={cn(
26
- 'group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 shrink-0 group-data-[spacing=0]/toggle-group:rounded-none focus:z-10 focus-visible:z-10 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
26
+ 'group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 shrink-0 group-data-[spacing=0]/toggle-group:rounded-none focus:z-10 focus-visible:z-10 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=accent]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=accent]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=accent]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=accent]:first:border-t group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
27
27
  toggleVariants({
28
28
  variant: ctx.variant || variant,
29
29
  size: ctx.size || size
@@ -52,6 +52,8 @@
52
52
  showDisplayMenu?: boolean;
53
53
  enableMeshClick?: boolean;
54
54
  backgroundColor?: string;
55
+ /** The look to open in. Default `technical`. */
56
+ look?: Look;
55
57
  /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
56
58
  showEdges?: boolean;
57
59
  }
@@ -91,6 +93,7 @@
91
93
  showDisplayMenu: true,
92
94
  enableMeshClick: true,
93
95
  backgroundColor: '#E6E6E6',
96
+ look: 'technical',
94
97
  showEdges: false
95
98
  };
96
99
 
@@ -124,7 +127,8 @@
124
127
  let clearEdges: ThreeViewer['clearEdges'] | null = null;
125
128
  let invalidate: (() => void) | null = null;
126
129
  let captureImage: ThreeViewer['captureImage'] | null = null;
127
- let setLook: ((look: Look) => void) | null = null;
130
+ let setLook: ThreeViewer['setLook'] | null = null;
131
+ let syncEmissiveLights: (() => void) | null = null;
128
132
  let updateGridScale: (() => void) | null = null;
129
133
  let updateShadowBounds: (() => void) | null = null;
130
134
  let fitToView: (() => void) | null = null;
@@ -145,9 +149,16 @@
145
149
  let projection: CameraProjection = $state('perspective');
146
150
  let measureActive = $state(false);
147
151
  let gridVisible = $state(false);
148
- let renderStyle: Look = $state('technical');
152
+ // A saved schema can name a look this build doesn't have, and an unknown look throws on mount.
153
+ const openingLook = untrack(() => {
154
+ const look = viewerConfig.look;
155
+ return look && LOOKS.includes(look) ? look : defaultViewerConfig.look;
156
+ });
157
+ const opensAsLineDrawing = LOOK_PRESETS[openingLook].requiresEdges === true;
158
+ const configuredEdges = untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges;
159
+ let renderStyle: Look = $state(openingLook);
149
160
  // Seeded once, then owned by the menu toggle.
150
- let edgesVisible = $state(untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges);
161
+ let edgesVisible = $state(opensAsLineDrawing || configuredEdges);
151
162
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
152
163
  let selectedMeshName: string | null = $state(null);
153
164
 
@@ -160,7 +171,8 @@
160
171
  arctic: t.lookArctic,
161
172
  xray: t.lookXray,
162
173
  lineart: t.lookLineart,
163
- wireframe: t.lookWireframe
174
+ wireframe: t.lookWireframe,
175
+ night: t.lookNight
164
176
  });
165
177
  const styleOptions = $derived<{ look: Look; label: string }[]>(
166
178
  LOOKS.map((look) => ({
@@ -234,6 +246,7 @@
234
246
  invalidate = init.invalidate;
235
247
  captureImage = init.captureImage;
236
248
  setLook = init.setLook;
249
+ syncEmissiveLights = init.syncEmissiveLights;
237
250
  updateGridScale = init.updateGridScale;
238
251
  updateShadowBounds = init.updateShadowBounds;
239
252
  fitToView = init.fitToView;
@@ -279,7 +292,7 @@
279
292
 
280
293
  // Edges the lineart look switched on, so leaving it can put the user's own choice back rather
281
294
  // than stranding them with overlays they never asked for.
282
- let edgesBeforeLineDrawing: boolean | null = null;
295
+ let edgesBeforeLineDrawing: boolean | null = opensAsLineDrawing ? configuredEdges : null;
283
296
 
284
297
  function setRenderStyle(look: Look) {
285
298
  if (!setLook || look === renderStyle) return;
@@ -366,7 +379,8 @@
366
379
  untrack(() => {
367
380
  // The new meshes carry the materials the parser built, so a look that overrides them
368
381
  // (arctic, x-ray) has to be re-applied or the solve silently reverts to shaded.
369
- setLook?.(renderStyle);
382
+ // Emissive lights wait for `applyTo` below, or a hidden lamp would count as lit.
383
+ setLook?.(renderStyle, { deferEmissiveLights: true });
370
384
  // updateScene discarded the previous solve's overlays along with its content.
371
385
  if (edgesVisible) applyEdges?.(scene!, edgeOverrides());
372
386
  // Rescale the grid so cells and fade match the new content's extent.
@@ -377,6 +391,7 @@
377
391
  // The rebuild un-hid everything; the outliner keys hidden state on Grasshopper
378
392
  // identity, not on the instances just discarded, so it can re-hide it.
379
393
  outliner?.applyTo();
394
+ syncEmissiveLights?.();
380
395
  sceneVersion++;
381
396
  // Repaint now rather than on the render loop's safety interval.
382
397
  invalidate?.();
@@ -680,7 +695,7 @@
680
695
  <SceneManager
681
696
  {outliner}
682
697
  {sceneVersion}
683
- onVisibilityChange={() => invalidate?.()}
698
+ onVisibilityChange={() => syncEmissiveLights?.()}
684
699
  onHover={(entry) => setHover?.(entry?.object ?? null, entry?.memberIndex ?? null)}
685
700
  />
686
701
  </Resizable.Pane>
@@ -40,6 +40,7 @@ export interface ViewerMessages {
40
40
  lookTechnical: string;
41
41
  lookStudio: string;
42
42
  lookShowcase: string;
43
+ lookNight: string;
43
44
  lookArctic: string;
44
45
  lookXray: string;
45
46
  lookLineart: string;
@@ -268,6 +269,7 @@ const en: ViewerMessages = {
268
269
  lookTechnical: 'Technical',
269
270
  lookStudio: 'Studio',
270
271
  lookShowcase: 'Showcase',
272
+ lookNight: 'Night',
271
273
  lookArctic: 'Arctic',
272
274
  lookXray: 'X-Ray',
273
275
  lookLineart: 'Line Art',
@@ -461,6 +463,7 @@ const de: ViewerMessages = {
461
463
  lookTechnical: 'Technisch',
462
464
  lookStudio: 'Studio',
463
465
  lookShowcase: 'Präsentation',
466
+ lookNight: 'Nacht',
464
467
  lookArctic: 'Arktis',
465
468
  lookXray: 'Röntgen',
466
469
  lookLineart: 'Strichzeichnung',