@pascal-app/viewer 0.1.12 → 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 (90) 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 +93 -7
  10. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  11. package/dist/components/renderers/node-renderer.js +4 -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 +5 -20
  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 +5 -0
  30. package/dist/components/renderers/window/window-renderer.d.ts.map +1 -0
  31. package/dist/components/renderers/window/window-renderer.js +11 -0
  32. package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -1
  33. package/dist/components/renderers/zone/zone-renderer.js +33 -13
  34. package/dist/components/viewer/ground-occluder.d.ts +2 -0
  35. package/dist/components/viewer/ground-occluder.d.ts.map +1 -0
  36. package/dist/components/viewer/ground-occluder.js +55 -0
  37. package/dist/components/viewer/index.d.ts +1 -0
  38. package/dist/components/viewer/index.d.ts.map +1 -1
  39. package/dist/components/viewer/index.js +59 -6
  40. package/dist/components/viewer/lights.d.ts.map +1 -1
  41. package/dist/components/viewer/lights.js +69 -5
  42. package/dist/components/viewer/perf-monitor.d.ts +2 -0
  43. package/dist/components/viewer/perf-monitor.d.ts.map +1 -0
  44. package/dist/components/viewer/perf-monitor.js +42 -0
  45. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  46. package/dist/components/viewer/post-processing.js +229 -106
  47. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  48. package/dist/components/viewer/selection-manager.js +62 -18
  49. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  50. package/dist/components/viewer/viewer-camera.js +2 -2
  51. package/dist/hooks/use-gltf-ktx2.d.ts +1 -1
  52. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  53. package/dist/hooks/use-gltf-ktx2.js +25 -7
  54. package/dist/hooks/use-node-events.d.ts +13 -1
  55. package/dist/hooks/use-node-events.d.ts.map +1 -1
  56. package/dist/hooks/use-node-events.js +34 -7
  57. package/dist/index.d.ts +4 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -1
  60. package/dist/lib/asset-url.d.ts +1 -1
  61. package/dist/lib/asset-url.d.ts.map +1 -1
  62. package/dist/lib/asset-url.js +2 -1
  63. package/dist/lib/layers.d.ts +5 -0
  64. package/dist/lib/layers.d.ts.map +1 -0
  65. package/dist/lib/layers.js +4 -0
  66. package/dist/store/use-item-light-pool.d.ts +18 -0
  67. package/dist/store/use-item-light-pool.d.ts.map +1 -0
  68. package/dist/store/use-item-light-pool.js +30 -0
  69. package/dist/store/use-viewer.d.ts +54 -7
  70. package/dist/store/use-viewer.d.ts.map +1 -1
  71. package/dist/store/use-viewer.js +81 -17
  72. package/dist/systems/interactive/interactive-system.d.ts +2 -0
  73. package/dist/systems/interactive/interactive-system.d.ts.map +1 -0
  74. package/dist/systems/interactive/interactive-system.js +95 -0
  75. package/dist/systems/item-light/item-light-system.d.ts +2 -0
  76. package/dist/systems/item-light/item-light-system.d.ts.map +1 -0
  77. package/dist/systems/item-light/item-light-system.js +235 -0
  78. package/dist/systems/level/level-system.d.ts.map +1 -1
  79. package/dist/systems/level/level-system.js +19 -8
  80. package/dist/systems/level/level-utils.d.ts +17 -0
  81. package/dist/systems/level/level-utils.d.ts.map +1 -0
  82. package/dist/systems/level/level-utils.js +82 -0
  83. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  84. package/dist/systems/wall/wall-cutout.js +2 -4
  85. package/dist/systems/zone/zone-system.d.ts.map +1 -1
  86. package/dist/systems/zone/zone-system.js +29 -3
  87. package/package.json +10 -7
  88. package/dist/hooks/use-grid-events.d.ts +0 -12
  89. package/dist/hooks/use-grid-events.d.ts.map +0 -1
  90. package/dist/hooks/use-grid-events.js +0 -33
@@ -1,4 +1,5 @@
1
1
  import { emitter, } from '@pascal-app/core';
2
+ import useViewer from '../store/use-viewer';
2
3
  export function useNodeEvents(node, type) {
3
4
  const emit = (suffix, e) => {
4
5
  const eventKey = `${type}:${suffix}`;
@@ -15,24 +16,50 @@ export function useNodeEvents(node, type) {
15
16
  };
16
17
  return {
17
18
  onPointerDown: (e) => {
19
+ if (useViewer.getState().cameraDragging)
20
+ return;
18
21
  if (e.button !== 0)
19
22
  return;
20
23
  emit('pointerdown', e);
21
24
  },
22
25
  onPointerUp: (e) => {
26
+ if (useViewer.getState().cameraDragging)
27
+ return;
23
28
  if (e.button !== 0)
24
29
  return;
25
30
  emit('pointerup', e);
31
+ // Synthesize a click event on pointer up to be more forgiving than R3F's default onClick
32
+ // which often fails if the mouse moves even 1 pixel.
33
+ emit('click', e);
26
34
  },
27
35
  onClick: (e) => {
28
- if (e.button !== 0)
36
+ // Disable default R3F click since we synthesize it on pointerup
37
+ // This prevents double-clicks from firing twice.
38
+ },
39
+ onPointerEnter: (e) => {
40
+ if (useViewer.getState().cameraDragging)
29
41
  return;
30
- emit('click', e);
42
+ emit('enter', e);
43
+ },
44
+ onPointerLeave: (e) => {
45
+ if (useViewer.getState().cameraDragging)
46
+ return;
47
+ emit('leave', e);
48
+ },
49
+ onPointerMove: (e) => {
50
+ if (useViewer.getState().cameraDragging)
51
+ return;
52
+ emit('move', e);
53
+ },
54
+ onDoubleClick: (e) => {
55
+ if (useViewer.getState().cameraDragging)
56
+ return;
57
+ emit('double-click', e);
58
+ },
59
+ onContextMenu: (e) => {
60
+ if (useViewer.getState().cameraDragging)
61
+ return;
62
+ emit('context-menu', e);
31
63
  },
32
- onPointerEnter: (e) => emit('enter', e),
33
- onPointerLeave: (e) => emit('leave', e),
34
- onPointerMove: (e) => emit('move', e),
35
- onDoubleClick: (e) => emit('double-click', e),
36
- onContextMenu: (e) => emit('context-menu', e),
37
64
  };
38
65
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { default as Viewer } from './components/viewer';
2
- export { default as useViewer } from './store/use-viewer';
3
2
  export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url';
3
+ export { SCENE_LAYER, ZONE_LAYER } from './lib/layers';
4
+ export { default as useViewer } from './store/use-viewer';
5
+ export { InteractiveSystem } from './systems/interactive/interactive-system';
6
+ export { snapLevelsToTruePositions } from './systems/level/level-utils';
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  export { default as Viewer } from './components/viewer';
2
- export { default as useViewer } from './store/use-viewer';
3
2
  export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url';
3
+ export { SCENE_LAYER, ZONE_LAYER } from './lib/layers';
4
+ export { default as useViewer } from './store/use-viewer';
5
+ export { InteractiveSystem } from './systems/interactive/interactive-system';
6
+ export { snapLevelsToTruePositions } from './systems/level/level-utils';
@@ -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,12 +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;
45
+ cameraDragging: boolean;
46
+ setCameraDragging: (dragging: boolean) => void;
32
47
  };
33
- 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
+ }>;
34
81
  export default useViewer;
35
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;CAC3D,CAAA;AAED,QAAA,MAAM,SAAS,0EAqDZ,CAAC;AAEJ,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
@@ -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,5 +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 }),
98
+ cameraDragging: false,
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
+ }),
45
109
  }));
46
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"}