@pascal-app/viewer 0.1.13 → 0.2.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.
Files changed (86) hide show
  1. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/ceiling/ceiling-renderer.js +16 -9
  3. package/dist/components/renderers/door/door-renderer.d.ts +5 -0
  4. package/dist/components/renderers/door/door-renderer.d.ts.map +1 -0
  5. package/dist/components/renderers/door/door-renderer.js +11 -0
  6. package/dist/components/renderers/guide/guide-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/guide/guide-renderer.js +4 -2
  8. package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/item/item-renderer.js +92 -7
  10. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  11. package/dist/components/renderers/node-renderer.js +3 -1
  12. package/dist/components/renderers/roof/roof-materials.d.ts +4 -0
  13. package/dist/components/renderers/roof/roof-materials.d.ts.map +1 -0
  14. package/dist/components/renderers/roof/roof-materials.js +16 -0
  15. package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
  16. package/dist/components/renderers/roof/roof-renderer.js +5 -1
  17. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +5 -0
  18. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -0
  19. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +13 -0
  20. package/dist/components/renderers/scan/scan-renderer.d.ts.map +1 -1
  21. package/dist/components/renderers/scan/scan-renderer.js +3 -1
  22. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  23. package/dist/components/renderers/scene-renderer.js +3 -3
  24. package/dist/components/renderers/site/site-renderer.d.ts.map +1 -1
  25. package/dist/components/renderers/site/site-renderer.js +4 -19
  26. package/dist/components/renderers/slab/slab-renderer.js +1 -1
  27. package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -1
  28. package/dist/components/renderers/wall/wall-renderer.js +1 -1
  29. package/dist/components/renderers/window/window-renderer.d.ts.map +1 -1
  30. package/dist/components/renderers/window/window-renderer.js +2 -1
  31. package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -1
  32. package/dist/components/renderers/zone/zone-renderer.js +33 -13
  33. package/dist/components/viewer/ground-occluder.d.ts +2 -0
  34. package/dist/components/viewer/ground-occluder.d.ts.map +1 -0
  35. package/dist/components/viewer/ground-occluder.js +55 -0
  36. package/dist/components/viewer/index.d.ts +1 -0
  37. package/dist/components/viewer/index.d.ts.map +1 -1
  38. package/dist/components/viewer/index.js +59 -6
  39. package/dist/components/viewer/lights.d.ts.map +1 -1
  40. package/dist/components/viewer/lights.js +69 -5
  41. package/dist/components/viewer/perf-monitor.d.ts +2 -0
  42. package/dist/components/viewer/perf-monitor.d.ts.map +1 -0
  43. package/dist/components/viewer/perf-monitor.js +42 -0
  44. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  45. package/dist/components/viewer/post-processing.js +230 -107
  46. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  47. package/dist/components/viewer/selection-manager.js +47 -17
  48. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  49. package/dist/components/viewer/viewer-camera.js +2 -2
  50. package/dist/hooks/use-gltf-ktx2.d.ts +1 -1
  51. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  52. package/dist/hooks/use-gltf-ktx2.js +25 -7
  53. package/dist/hooks/use-node-events.d.ts +9 -1
  54. package/dist/hooks/use-node-events.d.ts.map +1 -1
  55. package/dist/hooks/use-node-events.js +5 -5
  56. package/dist/index.d.ts +4 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +4 -1
  59. package/dist/lib/asset-url.d.ts +1 -1
  60. package/dist/lib/asset-url.d.ts.map +1 -1
  61. package/dist/lib/asset-url.js +2 -1
  62. package/dist/lib/layers.d.ts +5 -0
  63. package/dist/lib/layers.d.ts.map +1 -0
  64. package/dist/lib/layers.js +4 -0
  65. package/dist/store/use-item-light-pool.d.ts +18 -0
  66. package/dist/store/use-item-light-pool.d.ts.map +1 -0
  67. package/dist/store/use-item-light-pool.js +30 -0
  68. package/dist/store/use-viewer.d.ts +52 -7
  69. package/dist/store/use-viewer.d.ts.map +1 -1
  70. package/dist/store/use-viewer.js +79 -17
  71. package/dist/systems/interactive/interactive-system.d.ts +2 -0
  72. package/dist/systems/interactive/interactive-system.d.ts.map +1 -0
  73. package/dist/systems/interactive/interactive-system.js +95 -0
  74. package/dist/systems/item-light/item-light-system.d.ts +2 -0
  75. package/dist/systems/item-light/item-light-system.d.ts.map +1 -0
  76. package/dist/systems/item-light/item-light-system.js +235 -0
  77. package/dist/systems/level/level-system.d.ts.map +1 -1
  78. package/dist/systems/level/level-system.js +19 -8
  79. package/dist/systems/level/level-utils.d.ts +17 -0
  80. package/dist/systems/level/level-utils.d.ts.map +1 -0
  81. package/dist/systems/level/level-utils.js +82 -0
  82. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  83. package/dist/systems/wall/wall-cutout.js +2 -4
  84. package/dist/systems/zone/zone-system.d.ts.map +1 -1
  85. package/dist/systems/zone/zone-system.js +29 -3
  86. package/package.json +6 -5
@@ -1,4 +1,4 @@
1
- export declare const ASSETS_CDN_URL = "https://editor.pascal.app";
1
+ export declare const ASSETS_CDN_URL: any;
2
2
  /**
3
3
  * Resolves an asset URL to the appropriate format:
4
4
  * - If URL starts with http:// or https://, return as-is (external URL)
@@ -1 +1 @@
1
- {"version":3,"file":"asset-url.d.ts","sourceRoot":"","sources":["../../src/lib/asset-url.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,8BAA8B,CAAA;AAEzD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB5F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAiB3E"}
1
+ {"version":3,"file":"asset-url.d.ts","sourceRoot":"","sources":["../../src/lib/asset-url.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,KAAwE,CAAA;AAEnG;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB5F;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAiB3E"}
@@ -1,5 +1,6 @@
1
1
  import { loadAssetUrl } from '@pascal-app/core';
2
- export const ASSETS_CDN_URL = 'https://editor.pascal.app';
2
+ // @ts-expect-error
3
+ export const ASSETS_CDN_URL = process.env.NEXT_PUBLIC_ASSETS_CDN_URL || 'https://editor.pascal.app';
3
4
  /**
4
5
  * Resolves an asset URL to the appropriate format:
5
6
  * - If URL starts with http:// or https://, return as-is (external URL)
@@ -0,0 +1,5 @@
1
+ /** Default Three.js layer for main scene geometry. */
2
+ export declare const SCENE_LAYER = 0;
3
+ /** Layer used for zone rendering (floor fills and wall borders). */
4
+ export declare const ZONE_LAYER = 2;
5
+ //# sourceMappingURL=layers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/lib/layers.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B,oEAAoE;AACpE,eAAO,MAAM,UAAU,IAAI,CAAA"}
@@ -0,0 +1,4 @@
1
+ /** Default Three.js layer for main scene geometry. */
2
+ export const SCENE_LAYER = 0;
3
+ /** Layer used for zone rendering (floor fills and wall borders). */
4
+ export const ZONE_LAYER = 2;
@@ -0,0 +1,18 @@
1
+ import type { AnyNodeId, Interactive, LightEffect } from '@pascal-app/core';
2
+ export type LightRegistration = {
3
+ nodeId: AnyNodeId;
4
+ effect: LightEffect;
5
+ toggleIndex: number;
6
+ sliderIndex: number;
7
+ sliderMin: number;
8
+ sliderMax: number;
9
+ hasSlider: boolean;
10
+ };
11
+ type ItemLightPoolStore = {
12
+ registrations: Map<string, LightRegistration>;
13
+ register: (key: string, nodeId: AnyNodeId, effect: LightEffect, interactive: Interactive) => void;
14
+ unregister: (key: string) => void;
15
+ };
16
+ export declare const useItemLightPool: import("zustand").UseBoundStore<import("zustand").StoreApi<ItemLightPoolStore>>;
17
+ export {};
18
+ //# sourceMappingURL=use-item-light-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-item-light-pool.d.ts","sourceRoot":"","sources":["../../src/store/use-item-light-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAiB,MAAM,kBAAkB,CAAA;AAG1F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,SAAS,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,KAAK,IAAI,CAAA;IACjG,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC,CAAA;AAED,eAAO,MAAM,gBAAgB,iFAiC1B,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { create } from 'zustand';
2
+ export const useItemLightPool = create((set) => ({
3
+ registrations: new Map(),
4
+ register: (key, nodeId, effect, interactive) => {
5
+ const toggleIndex = interactive.controls.findIndex((c) => c.kind === 'toggle');
6
+ const sliderIndex = interactive.controls.findIndex((c) => c.kind === 'slider');
7
+ const sliderControl = sliderIndex >= 0 ? interactive.controls[sliderIndex] : null;
8
+ const registration = {
9
+ nodeId,
10
+ effect,
11
+ toggleIndex,
12
+ sliderIndex,
13
+ hasSlider: sliderControl !== null,
14
+ sliderMin: sliderControl?.min ?? 0,
15
+ sliderMax: sliderControl?.max ?? 1,
16
+ };
17
+ set((s) => {
18
+ const next = new Map(s.registrations);
19
+ next.set(key, registration);
20
+ return { registrations: next };
21
+ });
22
+ },
23
+ unregister: (key) => {
24
+ set((s) => {
25
+ const next = new Map(s.registrations);
26
+ next.delete(key);
27
+ return { registrations: next };
28
+ });
29
+ },
30
+ }));
@@ -1,10 +1,10 @@
1
- import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from "@pascal-app/core";
2
- import type { Object3D } from "three";
1
+ import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core';
2
+ import type { Object3D } from 'three';
3
3
  type SelectionPath = {
4
- buildingId: BuildingNode["id"] | null;
5
- levelId: LevelNode["id"] | null;
6
- zoneId: ZoneNode["id"] | null;
7
- selectedIds: BaseNode["id"][];
4
+ buildingId: BuildingNode['id'] | null;
5
+ levelId: LevelNode['id'] | null;
6
+ zoneId: ZoneNode['id'] | null;
7
+ selectedIds: BaseNode['id'][];
8
8
  };
9
9
  type Outliner = {
10
10
  selectedObjects: Object3D[];
@@ -16,6 +16,8 @@ type ViewerState = {
16
16
  setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void;
17
17
  cameraMode: 'perspective' | 'orthographic';
18
18
  setCameraMode: (mode: 'perspective' | 'orthographic') => void;
19
+ theme: 'light' | 'dark';
20
+ setTheme: (theme: 'light' | 'dark') => void;
19
21
  levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
20
22
  setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
21
23
  wallMode: 'up' | 'cutaway' | 'down';
@@ -24,14 +26,57 @@ type ViewerState = {
24
26
  setShowScans: (show: boolean) => void;
25
27
  showGuides: boolean;
26
28
  setShowGuides: (show: boolean) => void;
29
+ showGrid: boolean;
30
+ setShowGrid: (show: boolean) => void;
31
+ projectId: string | null;
32
+ setProjectId: (id: string | null) => void;
33
+ projectPreferences: Record<string, {
34
+ showScans?: boolean;
35
+ showGuides?: boolean;
36
+ showGrid?: boolean;
37
+ }>;
27
38
  setSelection: (updates: Partial<SelectionPath>) => void;
28
39
  resetSelection: () => void;
29
40
  outliner: Outliner;
30
41
  exportScene: (() => Promise<void>) | null;
31
42
  setExportScene: (fn: (() => Promise<void>) | null) => void;
43
+ debugColors: boolean;
44
+ setDebugColors: (enabled: boolean) => void;
32
45
  cameraDragging: boolean;
33
46
  setCameraDragging: (dragging: boolean) => void;
34
47
  };
35
- declare const useViewer: import("zustand").UseBoundStore<import("zustand").StoreApi<ViewerState>>;
48
+ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ViewerState>, "setState" | "persist"> & {
49
+ setState(partial: ViewerState | Partial<ViewerState> | ((state: ViewerState) => ViewerState | Partial<ViewerState>), replace?: false | undefined): unknown;
50
+ setState(state: ViewerState | ((state: ViewerState) => ViewerState), replace: true): unknown;
51
+ persist: {
52
+ setOptions: (options: Partial<import("zustand/middleware").PersistOptions<ViewerState, {
53
+ cameraMode: "perspective" | "orthographic";
54
+ theme: "light" | "dark";
55
+ levelMode: "stacked" | "exploded" | "solo" | "manual";
56
+ wallMode: "up" | "cutaway" | "down";
57
+ projectPreferences: Record<string, {
58
+ showScans?: boolean;
59
+ showGuides?: boolean;
60
+ showGrid?: boolean;
61
+ }>;
62
+ }, unknown>>) => void;
63
+ clearStorage: () => void;
64
+ rehydrate: () => Promise<void> | void;
65
+ hasHydrated: () => boolean;
66
+ onHydrate: (fn: (state: ViewerState) => void) => () => void;
67
+ onFinishHydration: (fn: (state: ViewerState) => void) => () => void;
68
+ getOptions: () => Partial<import("zustand/middleware").PersistOptions<ViewerState, {
69
+ cameraMode: "perspective" | "orthographic";
70
+ theme: "light" | "dark";
71
+ levelMode: "stacked" | "exploded" | "solo" | "manual";
72
+ wallMode: "up" | "cutaway" | "down";
73
+ projectPreferences: Record<string, {
74
+ showScans?: boolean;
75
+ showGuides?: boolean;
76
+ showGrid?: boolean;
77
+ }>;
78
+ }, unknown>>;
79
+ };
80
+ }>;
36
81
  export default useViewer;
37
82
  //# sourceMappingURL=use-viewer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;CAC/B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,YAAY,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAEjE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAA;IAC1C,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,cAAc,KAAK,IAAI,CAAA;IAE7D,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;IACrD,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAExE,QAAQ,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,KAAK,IAAI,CAAA;IAEtD,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAErC,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAGtC,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IACvD,cAAc,EAAE,MAAM,IAAI,CAAA;IAE1B,QAAQ,EAAE,QAAQ,CAAA;IAGlB,WAAW,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IACzC,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAE1D,cAAc,EAAE,OAAO,CAAA;IACvB,iBAAiB,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;CAC/C,CAAA;AAED,QAAA,MAAM,SAAS,0EAwDZ,CAAC;AAEJ,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC5F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAKrC,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACrC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAA;CAC9B,CAAA;AAED,KAAK,QAAQ,GAAG;IACd,eAAe,EAAE,QAAQ,EAAE,CAAA;IAC3B,cAAc,EAAE,QAAQ,EAAE,CAAA;CAC3B,CAAA;AAED,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,YAAY,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAEjE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAA;IAC1C,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,cAAc,KAAK,IAAI,CAAA;IAE7D,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAA;IAE3C,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;IACrD,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAExE,QAAQ,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,KAAK,IAAI,CAAA;IAEtD,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAErC,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAEtC,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAEpC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACzC,kBAAkB,EAAE,MAAM,CACxB,MAAM,EACN;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAClE,CAAA;IAGD,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IACvD,cAAc,EAAE,MAAM,IAAI,CAAA;IAE1B,QAAQ,EAAE,QAAQ,CAAA;IAGlB,WAAW,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IACzC,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAE1D,WAAW,EAAE,OAAO,CAAA;IACpB,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAE1C,cAAc,EAAE,OAAO,CAAA;IACvB,iBAAiB,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;CAC/C,CAAA;AAED,QAAA,MAAM,SAAS;;;;;;;;;;4BApBG,OAAO;6BAAe,OAAO;2BAAa,OAAO;;;;;;;;;;;;;;4BAAjD,OAAO;6BAAe,OAAO;2BAAa,OAAO;;;;EAiJlE,CAAA;AAED,eAAe,SAAS,CAAA"}
@@ -1,33 +1,84 @@
1
- "use client";
2
- import { create } from "zustand";
3
- const useViewer = create()((set, get) => ({
1
+ 'use client';
2
+ import { create } from 'zustand';
3
+ import { persist } from 'zustand/middleware';
4
+ const useViewer = create()(persist((set) => ({
4
5
  selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
5
6
  hoveredId: null,
6
7
  setHoveredId: (id) => set({ hoveredId: id }),
7
- cameraMode: "perspective",
8
+ cameraMode: 'perspective',
8
9
  setCameraMode: (mode) => set({ cameraMode: mode }),
9
- levelMode: "stacked",
10
+ theme: 'light',
11
+ setTheme: (theme) => set({ theme }),
12
+ levelMode: 'stacked',
10
13
  setLevelMode: (mode) => set({ levelMode: mode }),
11
- wallMode: 'cutaway',
14
+ wallMode: 'up',
12
15
  setWallMode: (mode) => set({ wallMode: mode }),
13
16
  showScans: true,
14
- setShowScans: (show) => set({ showScans: show }),
17
+ setShowScans: (show) => set((state) => {
18
+ const projectPreferences = { ...(state.projectPreferences || {}) };
19
+ if (state.projectId) {
20
+ projectPreferences[state.projectId] = {
21
+ ...(projectPreferences[state.projectId] || {}),
22
+ showScans: show,
23
+ };
24
+ }
25
+ return { showScans: show, projectPreferences };
26
+ }),
15
27
  showGuides: true,
16
- setShowGuides: (show) => set({ showGuides: show }),
28
+ setShowGuides: (show) => set((state) => {
29
+ const projectPreferences = { ...(state.projectPreferences || {}) };
30
+ if (state.projectId) {
31
+ projectPreferences[state.projectId] = {
32
+ ...(projectPreferences[state.projectId] || {}),
33
+ showGuides: show,
34
+ };
35
+ }
36
+ return { showGuides: show, projectPreferences };
37
+ }),
38
+ showGrid: true,
39
+ setShowGrid: (show) => set((state) => {
40
+ const projectPreferences = { ...(state.projectPreferences || {}) };
41
+ if (state.projectId) {
42
+ projectPreferences[state.projectId] = {
43
+ ...(projectPreferences[state.projectId] || {}),
44
+ showGrid: show,
45
+ };
46
+ }
47
+ return { showGrid: show, projectPreferences };
48
+ }),
49
+ projectId: null,
50
+ setProjectId: (id) => set((state) => {
51
+ if (!id)
52
+ return { projectId: id };
53
+ const prefs = state.projectPreferences?.[id] || {};
54
+ return {
55
+ projectId: id,
56
+ showScans: prefs.showScans ?? true,
57
+ showGuides: prefs.showGuides ?? true,
58
+ showGrid: prefs.showGrid ?? true,
59
+ };
60
+ }),
61
+ projectPreferences: {},
17
62
  setSelection: (updates) => set((state) => {
18
63
  const newSelection = { ...state.selection, ...updates };
19
- // Hierarchy Guard: If we change a high-level parent, reset the children
64
+ // Hierarchy Guard: If we change a high-level parent, reset the children unless explicitly provided
20
65
  if (updates.buildingId !== undefined) {
21
- newSelection.levelId = null;
22
- newSelection.zoneId = null;
23
- newSelection.selectedIds = [];
66
+ if (updates.levelId === undefined)
67
+ newSelection.levelId = null;
68
+ if (updates.zoneId === undefined)
69
+ newSelection.zoneId = null;
70
+ if (updates.selectedIds === undefined)
71
+ newSelection.selectedIds = [];
24
72
  }
25
- else if (updates.levelId !== undefined) {
26
- newSelection.zoneId = null;
27
- newSelection.selectedIds = [];
73
+ if (updates.levelId !== undefined) {
74
+ if (updates.zoneId === undefined)
75
+ newSelection.zoneId = null;
76
+ if (updates.selectedIds === undefined)
77
+ newSelection.selectedIds = [];
28
78
  }
29
- else if (updates.zoneId !== undefined) {
30
- newSelection.selectedIds = [];
79
+ if (updates.zoneId !== undefined) {
80
+ if (updates.selectedIds === undefined)
81
+ newSelection.selectedIds = [];
31
82
  }
32
83
  return { selection: newSelection };
33
84
  }),
@@ -42,7 +93,18 @@ const useViewer = create()((set, get) => ({
42
93
  outliner: { selectedObjects: [], hoveredObjects: [] },
43
94
  exportScene: null,
44
95
  setExportScene: (fn) => set({ exportScene: fn }),
96
+ debugColors: false,
97
+ setDebugColors: (enabled) => set({ debugColors: enabled }),
45
98
  cameraDragging: false,
46
99
  setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
100
+ }), {
101
+ name: 'viewer-preferences',
102
+ partialize: (state) => ({
103
+ cameraMode: state.cameraMode,
104
+ theme: state.theme,
105
+ levelMode: state.levelMode,
106
+ wallMode: state.wallMode,
107
+ projectPreferences: state.projectPreferences,
108
+ }),
47
109
  }));
48
110
  export default useViewer;
@@ -0,0 +1,2 @@
1
+ export declare const InteractiveSystem: () => import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=interactive-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"AAwBA,eAAO,MAAM,iBAAiB,+CAgB7B,CAAA"}
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { pointInPolygon, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
4
+ import { Html } from '@react-three/drei';
5
+ import { createPortal, useFrame } from '@react-three/fiber';
6
+ import { useState } from 'react';
7
+ import { Vector3 } from 'three';
8
+ import { useShallow } from 'zustand/react/shallow';
9
+ import useViewer from '../../store/use-viewer';
10
+ const _tempVec = new Vector3();
11
+ // ---- Parent: one overlay per interactive item ----
12
+ export const InteractiveSystem = () => {
13
+ const interactiveNodeIds = useScene(useShallow((state) => Object.values(state.nodes)
14
+ .filter((n) => n.type === 'item' && n.asset.interactive != null)
15
+ .map((n) => n.id)));
16
+ return (_jsx(_Fragment, { children: interactiveNodeIds.map((id) => (_jsx(ItemControlsOverlay, { nodeId: id }, id))) }));
17
+ };
18
+ // ---- Child: polls sceneRegistry then portals controls into the item group ----
19
+ const ItemControlsOverlay = ({ nodeId }) => {
20
+ const node = useScene((state) => state.nodes[nodeId]);
21
+ const [itemObj, setItemObj] = useState(null);
22
+ useFrame(() => {
23
+ if (itemObj)
24
+ return;
25
+ const obj = sceneRegistry.nodes.get(nodeId);
26
+ if (obj)
27
+ setItemObj(obj);
28
+ });
29
+ const controlValues = useInteractive(useShallow((state) => state.items[nodeId]?.controlValues));
30
+ const setControlValue = useInteractive((state) => state.setControlValue);
31
+ const zoneId = useViewer((s) => s.selection.zoneId);
32
+ const zonePolygon = useScene((s) => {
33
+ if (!zoneId)
34
+ return null;
35
+ const z = s.nodes[zoneId];
36
+ return z?.polygon ?? null;
37
+ });
38
+ if (!(itemObj && controlValues && node?.asset.interactive))
39
+ return null;
40
+ const { controls } = node.asset.interactive;
41
+ const [, height] = node.asset.dimensions;
42
+ let opacity = 0;
43
+ let pointerEvents = 'none';
44
+ if (zoneId && zonePolygon?.length) {
45
+ itemObj.getWorldPosition(_tempVec);
46
+ const inside = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
47
+ opacity = inside ? 1 : 0.1;
48
+ pointerEvents = inside ? 'auto' : 'none';
49
+ }
50
+ return createPortal(_jsx(Html, { center: true, distanceFactor: 8, occlude: true, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ gap: 6,
54
+ background: 'rgba(0,0,0,0.75)',
55
+ backdropFilter: 'blur(8px)',
56
+ borderRadius: 8,
57
+ padding: '8px 12px',
58
+ minWidth: 120,
59
+ pointerEvents,
60
+ userSelect: 'none',
61
+ opacity,
62
+ transition: 'opacity 0.3s ease',
63
+ }, children: controls.map((control, i) => (_jsx(ControlWidget, { control: control, onChange: (v) => setControlValue(nodeId, i, v), value: controlValues[i] ?? false }, i))) }) }), itemObj);
64
+ };
65
+ // ---- Control widgets ----
66
+ const ControlWidget = ({ control, value, onChange, }) => {
67
+ const labelStyle = {
68
+ color: 'white',
69
+ fontSize: 11,
70
+ fontFamily: 'monospace',
71
+ display: 'flex',
72
+ flexDirection: 'column',
73
+ gap: 2,
74
+ };
75
+ if (control.kind === 'toggle') {
76
+ return (_jsx("button", { onClick: () => onChange(!value), style: {
77
+ background: value ? '#4ade80' : '#374151',
78
+ color: 'white',
79
+ border: 'none',
80
+ borderRadius: 4,
81
+ padding: '4px 8px',
82
+ cursor: 'pointer',
83
+ fontSize: 12,
84
+ fontFamily: 'monospace',
85
+ transition: 'background 0.2s',
86
+ }, children: control.label ?? (value ? 'On' : 'Off') }));
87
+ }
88
+ if (control.kind === 'slider') {
89
+ return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, control.unit ? ` ${control.unit}` : ''] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: control.step, type: "range", value: value })] }));
90
+ }
91
+ if (control.kind === 'temperature') {
92
+ return (_jsxs("label", { style: labelStyle, children: [_jsxs("span", { children: [control.label, ": ", value, "\u00B0", control.unit] }), _jsx("input", { max: control.max, min: control.min, onChange: (e) => onChange(Number(e.target.value)), onPointerDown: (e) => e.stopPropagation(), step: 1, type: "range", value: value })] }));
93
+ }
94
+ return null;
95
+ };
@@ -0,0 +1,2 @@
1
+ export declare function ItemLightSystem(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=item-light-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item-light-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-light/item-light-system.tsx"],"names":[],"mappings":"AAwFA,wBAAgB,eAAe,4CA+M9B"}
@@ -0,0 +1,235 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { sceneRegistry, useInteractive, useScene } from '@pascal-app/core';
3
+ import { useFrame } from '@react-three/fiber';
4
+ import { useRef } from 'react';
5
+ import { MathUtils, Vector3 } from 'three';
6
+ import { useItemLightPool } from '../../store/use-item-light-pool';
7
+ import useViewer from '../../store/use-viewer';
8
+ const POOL_SIZE = 12;
9
+ // How often (in seconds) to re-evaluate which items have lights assigned (fallback timer)
10
+ const REASSIGN_INTERVAL = 0.2;
11
+ // Hysteresis: a currently-assigned slot keeps its key unless an unassigned
12
+ // candidate beats it by at least this much (prevents flickering at the boundary)
13
+ const HYSTERESIS = 0.15;
14
+ // Camera movement thresholds that trigger an early re-evaluation
15
+ const CAM_MOVE_DIST = 0.5; // units
16
+ const CAM_ROT_DOT = 0.995; // cos(~5.7°)
17
+ // Module-level temp vectors reused every frame (avoids GC pressure)
18
+ const _dir = new Vector3();
19
+ const _camPos = new Vector3();
20
+ const _camFwd = new Vector3();
21
+ const _itemPos = new Vector3();
22
+ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveState) {
23
+ // Skip lights that are toggled off — they contribute no illumination
24
+ if (reg.toggleIndex >= 0) {
25
+ const values = interactiveState.items[reg.nodeId]?.controlValues;
26
+ const isOn = Boolean(values?.[reg.toggleIndex]);
27
+ if (!isOn)
28
+ return Number.POSITIVE_INFINITY;
29
+ }
30
+ const { nodeId, effect } = reg;
31
+ const obj = sceneRegistry.nodes.get(nodeId);
32
+ if (!obj)
33
+ return Number.POSITIVE_INFINITY;
34
+ obj.getWorldPosition(_itemPos);
35
+ _itemPos.x += effect.offset[0];
36
+ _itemPos.y += effect.offset[1];
37
+ _itemPos.z += effect.offset[2];
38
+ _dir.copy(_itemPos).sub(_camPos).normalize();
39
+ const dot = _camFwd.dot(_dir); // 1 = ahead, -1 = behind
40
+ // Angular component (0 = dead ahead, 2 = directly behind)
41
+ const angular = 1 - dot;
42
+ // Normalised distance component (assumes scenes < 200 units)
43
+ const dist = _camPos.distanceTo(_itemPos) / 200;
44
+ // ── Level factor ──────────────────────────────────────────────────────────
45
+ const node = nodes[nodeId];
46
+ const itemLevelId = node?.parentId ?? null;
47
+ let levelPenalty = 0;
48
+ if (selectedLevelId) {
49
+ if (itemLevelId !== selectedLevelId) {
50
+ // In solo mode items on other levels are invisible — deprioritize strongly
51
+ levelPenalty = levelMode === 'solo' ? 100 : 0.8;
52
+ }
53
+ }
54
+ else if (itemLevelId) {
55
+ // No level selected — lightly prefer items on level index 0
56
+ const levelNode = nodes[itemLevelId];
57
+ const levelIndex = levelNode?.level ?? 0;
58
+ if (levelIndex !== 0)
59
+ levelPenalty = 0.3;
60
+ }
61
+ return angular * 0.7 + dist * 0.3 + levelPenalty;
62
+ }
63
+ export function ItemLightSystem() {
64
+ const lightRefs = useRef(Array.from({ length: POOL_SIZE }, () => null));
65
+ const slots = useRef(Array.from({ length: POOL_SIZE }, () => ({ key: null, pendingKey: null, isFadingOut: false })));
66
+ const reassignTimer = useRef(0);
67
+ // Track camera state at last reassignment to detect meaningful movement
68
+ const prevReassignCamPos = useRef(new Vector3());
69
+ const prevReassignCamFwd = useRef(new Vector3(0, 0, -1));
70
+ useFrame(({ camera }, delta) => {
71
+ const dt = Math.min(delta, 0.1);
72
+ const { registrations } = useItemLightPool.getState();
73
+ const interactiveState = useInteractive.getState();
74
+ // ── 1. Throttled priority reassignment ──────────────────────────────────
75
+ camera.getWorldPosition(_camPos);
76
+ camera.getWorldDirection(_camFwd);
77
+ const camMoved = _camPos.distanceTo(prevReassignCamPos.current) > CAM_MOVE_DIST ||
78
+ _camFwd.dot(prevReassignCamFwd.current) < CAM_ROT_DOT;
79
+ reassignTimer.current -= delta;
80
+ const shouldReassign = reassignTimer.current <= 0 || camMoved;
81
+ if (shouldReassign) {
82
+ reassignTimer.current = REASSIGN_INTERVAL;
83
+ prevReassignCamPos.current.copy(_camPos);
84
+ prevReassignCamFwd.current.copy(_camFwd);
85
+ // Read level/scene state once for the whole tick
86
+ const nodes = useScene.getState().nodes;
87
+ const viewerState = useViewer.getState();
88
+ const selectedLevelId = viewerState.selection.levelId;
89
+ const levelMode = viewerState.levelMode;
90
+ // Score every registration
91
+ const scored = [];
92
+ for (const [key, reg] of registrations) {
93
+ scored.push({
94
+ key,
95
+ score: scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveState),
96
+ });
97
+ }
98
+ scored.sort((a, b) => a.score - b.score);
99
+ // Build the desired assignment (top POOL_SIZE keys)
100
+ const desired = scored.slice(0, POOL_SIZE).map((s) => s.key);
101
+ // Build a map of currently-assigned keys → slot index for hysteresis
102
+ const currentlyAssigned = new Map();
103
+ for (let i = 0; i < POOL_SIZE; i++) {
104
+ const s = slots.current[i];
105
+ if (!s)
106
+ continue;
107
+ const k = s.key ?? s.pendingKey;
108
+ if (k)
109
+ currentlyAssigned.set(k, i);
110
+ }
111
+ // Assign desired keys to slots — prefer keeping existing assignments
112
+ const usedSlots = new Set();
113
+ const assignedKeys = new Set();
114
+ // Pass 1: keep existing slots where the key is still in desired
115
+ for (const key of desired) {
116
+ const existingSlot = currentlyAssigned.get(key);
117
+ if (existingSlot !== undefined && !usedSlots.has(existingSlot)) {
118
+ usedSlots.add(existingSlot);
119
+ assignedKeys.add(key);
120
+ }
121
+ }
122
+ // Pass 2: assign remaining desired keys to free slots
123
+ let freeSlot = 0;
124
+ for (const key of desired) {
125
+ if (assignedKeys.has(key))
126
+ continue;
127
+ while (freeSlot < POOL_SIZE && usedSlots.has(freeSlot))
128
+ freeSlot++;
129
+ if (freeSlot >= POOL_SIZE)
130
+ break;
131
+ // Hysteresis: only evict the current occupant if the new key scores
132
+ // meaningfully better than it
133
+ const freeSlotData = slots.current[freeSlot];
134
+ const currentKey = freeSlotData ? (freeSlotData.key ?? freeSlotData.pendingKey) : null;
135
+ if (currentKey && !desired.includes(currentKey)) {
136
+ const currentScore = scored.find((s) => s.key === currentKey)?.score ?? Number.POSITIVE_INFINITY;
137
+ const newScore = scored.find((s) => s.key === key)?.score ?? 0;
138
+ if (currentScore - newScore < HYSTERESIS) {
139
+ freeSlot++;
140
+ continue;
141
+ }
142
+ }
143
+ usedSlots.add(freeSlot);
144
+ assignedKeys.add(key);
145
+ const slot = slots.current[freeSlot];
146
+ if (slot && slot.key !== key) {
147
+ slot.pendingKey = key;
148
+ slot.isFadingOut = slot.key !== null;
149
+ if (!slot.isFadingOut) {
150
+ // Slot was idle — skip fade-out, assign immediately
151
+ slot.key = key;
152
+ slot.pendingKey = null;
153
+ const light = lightRefs.current[freeSlot];
154
+ const reg = registrations.get(key);
155
+ if (light && reg) {
156
+ light.color.set(reg.effect.color);
157
+ light.distance = reg.effect.distance ?? 0;
158
+ }
159
+ }
160
+ }
161
+ freeSlot++;
162
+ }
163
+ // Clear slots whose key is no longer in desired and not pending
164
+ for (let i = 0; i < POOL_SIZE; i++) {
165
+ if (!usedSlots.has(i)) {
166
+ const slot = slots.current[i];
167
+ if (slot?.key && !desired.includes(slot.key)) {
168
+ slot.pendingKey = null;
169
+ slot.isFadingOut = true;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ // ── 2. Per-frame light updates ───────────────────────────────────────────
175
+ for (let i = 0; i < POOL_SIZE; i++) {
176
+ const light = lightRefs.current[i];
177
+ if (!light)
178
+ continue;
179
+ const slot = slots.current[i];
180
+ if (!slot)
181
+ continue;
182
+ // Fade-out phase: lerp intensity → 0, then complete the transition
183
+ if (slot.isFadingOut) {
184
+ light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
185
+ if (light.intensity < 0.01) {
186
+ light.intensity = 0;
187
+ slot.isFadingOut = false;
188
+ slot.key = slot.pendingKey;
189
+ slot.pendingKey = null;
190
+ if (slot.key) {
191
+ const reg = registrations.get(slot.key);
192
+ if (reg) {
193
+ light.color.set(reg.effect.color);
194
+ light.distance = reg.effect.distance ?? 0;
195
+ }
196
+ }
197
+ }
198
+ continue;
199
+ }
200
+ if (!slot.key) {
201
+ // Idle slot — keep dark
202
+ light.intensity = 0;
203
+ continue;
204
+ }
205
+ const reg = registrations.get(slot.key);
206
+ if (!reg) {
207
+ slot.key = null;
208
+ light.intensity = 0;
209
+ continue;
210
+ }
211
+ // Snap world position each frame
212
+ const obj = sceneRegistry.nodes.get(reg.nodeId);
213
+ if (obj) {
214
+ obj.getWorldPosition(_itemPos);
215
+ const [ox, oy, oz] = reg.effect.offset;
216
+ light.position.set(_itemPos.x + ox, _itemPos.y + oy, _itemPos.z + oz);
217
+ }
218
+ // Compute target intensity
219
+ const values = interactiveState.items[reg.nodeId]?.controlValues;
220
+ const isOn = reg.toggleIndex >= 0 ? Boolean(values?.[reg.toggleIndex]) : true;
221
+ let t = 1;
222
+ if (reg.hasSlider) {
223
+ const raw = values?.[reg.sliderIndex] ?? reg.sliderMin;
224
+ t = (raw - reg.sliderMin) / (reg.sliderMax - reg.sliderMin);
225
+ }
226
+ const targetIntensity = isOn
227
+ ? MathUtils.lerp(reg.effect.intensityRange[0], reg.effect.intensityRange[1], t)
228
+ : reg.effect.intensityRange[0];
229
+ light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
230
+ }
231
+ });
232
+ return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, ref: (el) => {
233
+ lightRefs.current[i] = el;
234
+ } }, i))) }));
235
+ }