@pascal-app/viewer 0.9.1 → 0.9.2

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 (176) hide show
  1. package/dist/components/error-boundary.d.ts +6 -0
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +6 -0
  4. package/dist/components/renderers/node-renderer.d.ts +5 -1
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +5 -2
  7. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  9. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  10. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  11. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  12. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  13. package/dist/components/viewer/frame-limiter.js +39 -4
  14. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  15. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-interactive.js +394 -0
  17. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  19. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  20. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-replace-instances.js +61 -0
  23. package/dist/components/viewer/glb-scene.d.ts +54 -0
  24. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  25. package/dist/components/viewer/glb-scene.js +920 -0
  26. package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
  29. package/dist/components/viewer/index.d.ts +19 -2
  30. package/dist/components/viewer/index.d.ts.map +1 -1
  31. package/dist/components/viewer/index.js +104 -12
  32. package/dist/components/viewer/lights.d.ts.map +1 -1
  33. package/dist/components/viewer/lights.js +36 -5
  34. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  35. package/dist/components/viewer/perf-monitor.js +17 -2
  36. package/dist/components/viewer/post-processing.d.ts +7 -1
  37. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  38. package/dist/components/viewer/post-processing.js +179 -24
  39. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  40. package/dist/components/viewer/registered-systems.js +6 -4
  41. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  42. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  43. package/dist/components/viewer/scene-bvh.js +3 -0
  44. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  46. package/dist/components/viewer/scene-bvh.test.js +13 -0
  47. package/dist/components/viewer/scene-environment.d.ts +2 -0
  48. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  49. package/dist/components/viewer/scene-environment.js +79 -0
  50. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  51. package/dist/components/viewer/selection-manager.js +18 -10
  52. package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
  53. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  54. package/dist/components/viewer/walkthrough-controls.js +20 -0
  55. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  56. package/dist/hooks/use-gltf-ktx2.js +3 -22
  57. package/dist/hooks/use-node-events.d.ts +1 -1
  58. package/dist/hooks/use-node-events.d.ts.map +1 -1
  59. package/dist/hooks/use-node-events.js +22 -14
  60. package/dist/index.d.ts +19 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +22 -8
  63. package/dist/lib/backdrop.d.ts +33 -0
  64. package/dist/lib/backdrop.d.ts.map +1 -0
  65. package/dist/lib/backdrop.js +92 -0
  66. package/dist/lib/box-uv.d.ts +13 -0
  67. package/dist/lib/box-uv.d.ts.map +1 -0
  68. package/dist/lib/box-uv.js +32 -0
  69. package/dist/lib/csg-utils.d.ts +3 -9
  70. package/dist/lib/csg-utils.d.ts.map +1 -1
  71. package/dist/lib/csg-utils.js +65 -5
  72. package/dist/lib/csg-utils.test.d.ts +2 -0
  73. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  74. package/dist/lib/csg-utils.test.js +45 -0
  75. package/dist/lib/edge-style.d.ts +1 -0
  76. package/dist/lib/edge-style.d.ts.map +1 -1
  77. package/dist/lib/edge-style.js +26 -6
  78. package/dist/lib/ink-edges.d.ts +1 -1
  79. package/dist/lib/ink-edges.d.ts.map +1 -1
  80. package/dist/lib/ink-edges.js +13 -7
  81. package/dist/lib/ktx2-loader.d.ts +43 -0
  82. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  83. package/dist/lib/ktx2-loader.js +136 -0
  84. package/dist/lib/layers.d.ts +10 -0
  85. package/dist/lib/layers.d.ts.map +1 -1
  86. package/dist/lib/layers.js +10 -0
  87. package/dist/lib/materials.d.ts +14 -1
  88. package/dist/lib/materials.d.ts.map +1 -1
  89. package/dist/lib/materials.js +103 -26
  90. package/dist/lib/scene-themes.d.ts +1 -0
  91. package/dist/lib/scene-themes.d.ts.map +1 -1
  92. package/dist/lib/scene-themes.js +25 -16
  93. package/dist/lib/shadow-only.d.ts +4 -0
  94. package/dist/lib/shadow-only.d.ts.map +1 -0
  95. package/dist/lib/shadow-only.js +36 -0
  96. package/dist/lib/tsl-compat.d.ts +12 -0
  97. package/dist/lib/tsl-compat.d.ts.map +1 -0
  98. package/dist/lib/tsl-compat.js +13 -0
  99. package/dist/store/use-viewer.d.ts +60 -18
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +175 -3
  102. package/dist/store/use-viewer.test.d.ts +2 -0
  103. package/dist/store/use-viewer.test.d.ts.map +1 -0
  104. package/dist/store/use-viewer.test.js +42 -0
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +4 -1
  107. package/dist/systems/door/door-animation.test.d.ts +2 -0
  108. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  109. package/dist/systems/door/door-animation.test.js +51 -0
  110. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  111. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  112. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  113. package/dist/systems/door/door-system.d.ts +24 -0
  114. package/dist/systems/door/door-system.d.ts.map +1 -1
  115. package/dist/systems/door/door-system.js +494 -161
  116. package/dist/systems/fence/fence-system.d.ts +17 -0
  117. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  118. package/dist/systems/fence/fence-system.js +221 -53
  119. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  120. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  121. package/dist/systems/geometry/geometry-system.js +95 -26
  122. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  123. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  124. package/dist/systems/geometry/geometry-system.test.js +30 -0
  125. package/dist/systems/interactive/control-widget.d.ts +10 -0
  126. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  127. package/dist/systems/interactive/control-widget.js +35 -0
  128. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  129. package/dist/systems/interactive/interactive-system.js +55 -56
  130. package/dist/systems/item/item-system.d.ts.map +1 -1
  131. package/dist/systems/item/item-system.js +20 -6
  132. package/dist/systems/level/level-system.d.ts.map +1 -1
  133. package/dist/systems/level/level-system.js +28 -4
  134. package/dist/systems/level/level-utils.d.ts +0 -3
  135. package/dist/systems/level/level-utils.d.ts.map +1 -1
  136. package/dist/systems/level/level-utils.js +2 -41
  137. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  138. package/dist/systems/roof/roof-materials.js +29 -8
  139. package/dist/systems/roof/roof-system.d.ts +21 -8
  140. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  141. package/dist/systems/roof/roof-system.js +1079 -221
  142. package/dist/systems/slab/slab-system.d.ts +5 -3
  143. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  144. package/dist/systems/slab/slab-system.js +41 -23
  145. package/dist/systems/slab/slab-system.test.js +3 -2
  146. package/dist/systems/stair/stair-system.js +6 -3
  147. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  148. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  149. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  150. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  151. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  152. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -17
  155. package/dist/systems/wall/wall-materials.d.ts +11 -7
  156. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  157. package/dist/systems/wall/wall-materials.js +244 -35
  158. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  159. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  160. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  161. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  162. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  163. package/dist/systems/wall/wall-support-extension.test.js +48 -0
  164. package/dist/systems/wall/wall-system.d.ts +2 -2
  165. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  166. package/dist/systems/wall/wall-system.js +329 -174
  167. package/dist/systems/window/window-animation-system.d.ts +12 -0
  168. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  169. package/dist/systems/window/window-animation-system.js +16 -5
  170. package/dist/systems/window/window-system.d.ts +7 -0
  171. package/dist/systems/window/window-system.d.ts.map +1 -1
  172. package/dist/systems/window/window-system.js +240 -14
  173. package/package.json +4 -4
  174. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  175. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  176. package/dist/components/viewer/ground-occluder.js +0 -67
@@ -27,6 +27,23 @@ type ViewerState = {
27
27
  setSceneTheme: (id: string) => void;
28
28
  renderContext: RenderContext;
29
29
  setRenderContext: (context: RenderContext) => void;
30
+ /** True during a GLB bake/export pass. Renderers that normally draw via a
31
+ * collective InstancedMesh (`def.system`) and mount only an invisible per-node
32
+ * proxy can watch this to emit real, visible geometry so the exporter — which
33
+ * clones only the `scene-renderer` subtree — captures them. Transient (never
34
+ * persisted). */
35
+ isExporting: boolean;
36
+ setExporting: (value: boolean) => void;
37
+ /** Item model loads that exhausted their retries — nodeId → asset URL. The
38
+ * scene renders without these items (they settle as skipped); a bake host
39
+ * can persist the map onto the artifact's metadata so a missing item is
40
+ * queryable instead of silently absent. Transient (never persisted). */
41
+ itemLoadFailures: Record<string, string>;
42
+ reportItemLoadFailure: (nodeId: string, url: string) => void;
43
+ clearItemLoadFailure: (nodeId: string) => void;
44
+ /** Suspend the render loop while the canvas is fully covered (e.g. studio gallery). */
45
+ renderPaused: boolean;
46
+ setRenderPaused: (value: boolean) => void;
30
47
  shading: RenderShading;
31
48
  shadingByContext: Partial<Record<RenderContext, RenderShading>>;
32
49
  setShading: (shading: RenderShading) => void;
@@ -40,26 +57,43 @@ type ViewerState = {
40
57
  setShadows: (shadows: boolean) => void;
41
58
  unit: 'metric' | 'imperial';
42
59
  setUnit: (unit: 'metric' | 'imperial') => void;
60
+ /** True once the user explicitly picked a unit. Until then `unit` is a
61
+ * locale-derived default and is not persisted, so the default can keep
62
+ * tracking the browser locale across sessions. */
63
+ unitExplicit: boolean;
43
64
  levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
44
65
  setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
45
- wallMode: 'up' | 'cutaway' | 'down';
46
- setWallMode: (mode: 'up' | 'cutaway' | 'down') => void;
66
+ wallMode: 'up' | 'cutaway' | 'down' | 'translucent';
67
+ setWallMode: (mode: 'up' | 'cutaway' | 'down' | 'translucent') => void;
47
68
  showScans: boolean;
48
69
  setShowScans: (show: boolean) => void;
49
70
  showGuides: boolean;
50
71
  setShowGuides: (show: boolean) => void;
51
72
  showGrid: boolean;
52
73
  setShowGrid: (show: boolean) => void;
74
+ showMeasurements: boolean;
75
+ setShowMeasurements: (show: boolean) => void;
76
+ showZones: boolean;
77
+ setShowZones: (show: boolean) => void;
78
+ transparentBackground: boolean;
79
+ setTransparentBackground: (transparent: boolean) => void;
80
+ inkOpacity: number | null;
81
+ setInkOpacity: (opacity: number | null) => void;
53
82
  projectId: string | null;
54
83
  setProjectId: (id: string | null) => void;
55
84
  projectPreferences: Record<string, {
56
85
  showScans?: boolean;
57
86
  showGuides?: boolean;
58
87
  showGrid?: boolean;
88
+ showMeasurements?: boolean;
59
89
  }>;
60
90
  setSelection: (updates: Partial<SelectionPath>) => void;
61
91
  resetSelection: () => void;
62
92
  outliner: Outliner;
93
+ /** Bumped by GeometrySystem after each rebuild pass so selection/outline
94
+ * effects can re-apply to the freshly swapped meshes. */
95
+ geometryRevision: number;
96
+ bumpGeometryRevision: () => void;
63
97
  exportScene: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null;
64
98
  setExportScene: (fn: ((format?: 'glb' | 'stl' | 'obj') => Promise<void>) | null) => void;
65
99
  debugColors: boolean;
@@ -86,6 +120,15 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
86
120
  setState(state: ViewerState | ((state: ViewerState) => ViewerState), replace: true): unknown;
87
121
  persist: {
88
122
  setOptions: (options: Partial<import("zustand/middleware").PersistOptions<ViewerState, {
123
+ levelMode: "manual" | "stacked" | "exploded" | "solo";
124
+ wallMode: "up" | "down" | "cutaway" | "translucent";
125
+ projectPreferences: Record<string, {
126
+ showScans?: boolean;
127
+ showGuides?: boolean;
128
+ showGrid?: boolean;
129
+ showMeasurements?: boolean;
130
+ }>;
131
+ unit?: "metric" | "imperial" | undefined;
89
132
  cameraMode: "perspective" | "orthographic";
90
133
  sceneTheme: string;
91
134
  shadingByContext: Partial<Record<RenderContext, RenderShading>>;
@@ -93,14 +136,6 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
93
136
  colorPreset: ColorPreset;
94
137
  edges: EdgeMode;
95
138
  shadows: boolean;
96
- unit: "metric" | "imperial";
97
- levelMode: "manual" | "stacked" | "exploded" | "solo";
98
- wallMode: "up" | "down" | "cutaway";
99
- projectPreferences: Record<string, {
100
- showScans?: boolean;
101
- showGuides?: boolean;
102
- showGrid?: boolean;
103
- }>;
104
139
  }, unknown>>) => void;
105
140
  clearStorage: () => void;
106
141
  rehydrate: () => Promise<void> | void;
@@ -108,6 +143,15 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
108
143
  onHydrate: (fn: (state: ViewerState) => void) => () => void;
109
144
  onFinishHydration: (fn: (state: ViewerState) => void) => () => void;
110
145
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<ViewerState, {
146
+ levelMode: "manual" | "stacked" | "exploded" | "solo";
147
+ wallMode: "up" | "down" | "cutaway" | "translucent";
148
+ projectPreferences: Record<string, {
149
+ showScans?: boolean;
150
+ showGuides?: boolean;
151
+ showGrid?: boolean;
152
+ showMeasurements?: boolean;
153
+ }>;
154
+ unit?: "metric" | "imperial" | undefined;
111
155
  cameraMode: "perspective" | "orthographic";
112
156
  sceneTheme: string;
113
157
  shadingByContext: Partial<Record<RenderContext, RenderShading>>;
@@ -115,16 +159,14 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
115
159
  colorPreset: ColorPreset;
116
160
  edges: EdgeMode;
117
161
  shadows: boolean;
118
- unit: "metric" | "imperial";
119
- levelMode: "manual" | "stacked" | "exploded" | "solo";
120
- wallMode: "up" | "down" | "cutaway";
121
- projectPreferences: Record<string, {
122
- showScans?: boolean;
123
- showGuides?: boolean;
124
- showGrid?: boolean;
125
- }>;
126
162
  }, unknown>>;
127
163
  };
128
164
  }>;
165
+ /** Apply an authoritative country code (e.g. IP-derived by the host app) as
166
+ * the unit default. Stronger signal than the timezone heuristic used at store
167
+ * creation, but still a default: it never overrides an explicit user choice
168
+ * and is not persisted (the unit only sticks once the user touches the
169
+ * toggle). */
170
+ export declare function applyCountryUnitDefault(country: string | null | undefined): void;
129
171
  export default useViewer;
130
172
  //# 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":"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"}
1
+ {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,123 @@
1
1
  'use client';
2
2
  import { create } from 'zustand';
3
3
  import { persist } from 'zustand/middleware';
4
+ import { SCENE_THEME_IDS } from '../lib/scene-themes';
5
+ const CAMERA_MODES = ['perspective', 'orthographic'];
6
+ const RENDER_SHADINGS = ['solid', 'rendered'];
7
+ const COLOR_PRESETS = ['clay', 'white', 'mono', 'blueprint'];
8
+ const EDGE_MODES = ['off', 'soft', 'strong'];
9
+ const UNITS = ['metric', 'imperial'];
10
+ const LEVEL_MODES = ['stacked', 'exploded', 'solo', 'manual'];
11
+ const WALL_MODES = ['up', 'cutaway', 'down', 'translucent'];
12
+ // Countries still on imperial/US customary units: United States, Liberia, Myanmar.
13
+ const IMPERIAL_REGIONS = ['US', 'LR', 'MM'];
14
+ // IANA zones for those countries. The timezone tracks the OS clock (actual
15
+ // location), unlike navigator.language where en-US is a common default for
16
+ // users far outside the US.
17
+ const IMPERIAL_TIMEZONES = new Set([
18
+ 'America/New_York',
19
+ 'America/Detroit',
20
+ 'America/Kentucky/Louisville',
21
+ 'America/Kentucky/Monticello',
22
+ 'America/Indiana/Indianapolis',
23
+ 'America/Indiana/Vincennes',
24
+ 'America/Indiana/Winamac',
25
+ 'America/Indiana/Marengo',
26
+ 'America/Indiana/Petersburg',
27
+ 'America/Indiana/Vevay',
28
+ 'America/Indiana/Tell_City',
29
+ 'America/Indiana/Knox',
30
+ 'America/Chicago',
31
+ 'America/Menominee',
32
+ 'America/North_Dakota/Center',
33
+ 'America/North_Dakota/New_Salem',
34
+ 'America/North_Dakota/Beulah',
35
+ 'America/Denver',
36
+ 'America/Boise',
37
+ 'America/Phoenix',
38
+ 'America/Los_Angeles',
39
+ 'America/Anchorage',
40
+ 'America/Juneau',
41
+ 'America/Sitka',
42
+ 'America/Metlakatla',
43
+ 'America/Yakutat',
44
+ 'America/Nome',
45
+ 'America/Adak',
46
+ 'Pacific/Honolulu',
47
+ 'America/Puerto_Rico',
48
+ 'Pacific/Guam',
49
+ 'Africa/Monrovia', // Liberia
50
+ 'Asia/Yangon', // Myanmar
51
+ 'Asia/Rangoon', // Myanmar (legacy alias)
52
+ ]);
53
+ function detectDefaultUnit() {
54
+ if (typeof navigator === 'undefined')
55
+ return 'metric';
56
+ try {
57
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
58
+ if (timeZone)
59
+ return IMPERIAL_TIMEZONES.has(timeZone) ? 'imperial' : 'metric';
60
+ // No timezone available: fall back to an explicit locale region subtag
61
+ // only (never maximize() — it turns a bare "en" into region US).
62
+ const region = new Intl.Locale(navigator.language).region;
63
+ return region && IMPERIAL_REGIONS.includes(region) ? 'imperial' : 'metric';
64
+ }
65
+ catch {
66
+ return 'metric';
67
+ }
68
+ }
69
+ function pickString(value, allowed, fallback) {
70
+ return typeof value === 'string' && allowed.includes(value) ? value : fallback;
71
+ }
72
+ function normalizeShadingByContext(value) {
73
+ if (!value || typeof value !== 'object' || Array.isArray(value))
74
+ return {};
75
+ const next = {};
76
+ for (const [context, shading] of Object.entries(value)) {
77
+ if (context !== 'editor' && context !== 'viewer')
78
+ continue;
79
+ next[context] = pickString(shading, RENDER_SHADINGS, 'rendered');
80
+ }
81
+ return next;
82
+ }
83
+ function normalizeProjectPreferences(value) {
84
+ if (!value || typeof value !== 'object' || Array.isArray(value))
85
+ return {};
86
+ const next = {};
87
+ for (const [projectId, preferences] of Object.entries(value)) {
88
+ if (!preferences || typeof preferences !== 'object' || Array.isArray(preferences))
89
+ continue;
90
+ const record = preferences;
91
+ next[projectId] = {
92
+ ...(typeof record.showScans === 'boolean' ? { showScans: record.showScans } : {}),
93
+ ...(typeof record.showGuides === 'boolean' ? { showGuides: record.showGuides } : {}),
94
+ ...(typeof record.showGrid === 'boolean' ? { showGrid: record.showGrid } : {}),
95
+ ...(typeof record.showMeasurements === 'boolean'
96
+ ? { showMeasurements: record.showMeasurements }
97
+ : {}),
98
+ };
99
+ }
100
+ return next;
101
+ }
102
+ function normalizePersistedViewerState(value) {
103
+ if (!value || typeof value !== 'object' || Array.isArray(value))
104
+ return {};
105
+ const state = value;
106
+ return {
107
+ cameraMode: pickString(state.cameraMode, CAMERA_MODES, 'perspective'),
108
+ sceneTheme: pickString(state.sceneTheme, SCENE_THEME_IDS, 'studio'),
109
+ shadingByContext: normalizeShadingByContext(state.shadingByContext),
110
+ textures: typeof state.textures === 'boolean' ? state.textures : true,
111
+ colorPreset: pickString(state.colorPreset, COLOR_PRESETS, 'clay'),
112
+ edges: pickString(state.edges, EDGE_MODES, 'soft'),
113
+ shadows: typeof state.shadows === 'boolean' ? state.shadows : true,
114
+ unit: pickString(state.unit, UNITS, detectDefaultUnit()),
115
+ unitExplicit: typeof state.unit === 'string' && UNITS.includes(state.unit),
116
+ levelMode: pickString(state.levelMode, LEVEL_MODES, 'stacked'),
117
+ wallMode: pickString(state.wallMode, WALL_MODES, 'up'),
118
+ projectPreferences: normalizeProjectPreferences(state.projectPreferences),
119
+ };
120
+ }
4
121
  const useViewer = create()(persist((set) => ({
5
122
  selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
6
123
  previewSelectedIds: [],
@@ -15,6 +132,21 @@ const useViewer = create()(persist((set) => ({
15
132
  setSceneTheme: (id) => set({ sceneTheme: id }),
16
133
  renderContext: 'editor',
17
134
  setRenderContext: (context) => set({ renderContext: context }),
135
+ isExporting: false,
136
+ setExporting: (value) => set({ isExporting: value }),
137
+ itemLoadFailures: {},
138
+ reportItemLoadFailure: (nodeId, url) => set((state) => state.itemLoadFailures[nodeId] === url
139
+ ? state
140
+ : { itemLoadFailures: { ...state.itemLoadFailures, [nodeId]: url } }),
141
+ clearItemLoadFailure: (nodeId) => set((state) => {
142
+ if (!(nodeId in state.itemLoadFailures))
143
+ return state;
144
+ const next = { ...state.itemLoadFailures };
145
+ delete next[nodeId];
146
+ return { itemLoadFailures: next };
147
+ }),
148
+ renderPaused: false,
149
+ setRenderPaused: (value) => set({ renderPaused: value }),
18
150
  shading: 'rendered',
19
151
  shadingByContext: {},
20
152
  setShading: (shading) => set((state) => ({
@@ -29,8 +161,9 @@ const useViewer = create()(persist((set) => ({
29
161
  setEdges: (edges) => set({ edges }),
30
162
  shadows: true,
31
163
  setShadows: (shadows) => set({ shadows }),
32
- unit: 'metric',
33
- setUnit: (unit) => set({ unit }),
164
+ unit: detectDefaultUnit(),
165
+ unitExplicit: false,
166
+ setUnit: (unit) => set({ unit, unitExplicit: true }),
34
167
  levelMode: 'stacked',
35
168
  setLevelMode: (mode) => set({ levelMode: mode }),
36
169
  wallMode: 'up',
@@ -68,6 +201,23 @@ const useViewer = create()(persist((set) => ({
68
201
  }
69
202
  return { showGrid: show, projectPreferences };
70
203
  }),
204
+ showMeasurements: true,
205
+ setShowMeasurements: (show) => set((state) => {
206
+ const projectPreferences = { ...(state.projectPreferences || {}) };
207
+ if (state.projectId) {
208
+ projectPreferences[state.projectId] = {
209
+ ...(projectPreferences[state.projectId] || {}),
210
+ showMeasurements: show,
211
+ };
212
+ }
213
+ return { showMeasurements: show, projectPreferences };
214
+ }),
215
+ showZones: true,
216
+ setShowZones: (show) => set({ showZones: show }),
217
+ transparentBackground: false,
218
+ setTransparentBackground: (transparent) => set({ transparentBackground: transparent }),
219
+ inkOpacity: null,
220
+ setInkOpacity: (opacity) => set({ inkOpacity: opacity }),
71
221
  projectId: null,
72
222
  setProjectId: (id) => set((state) => {
73
223
  if (!id)
@@ -78,6 +228,7 @@ const useViewer = create()(persist((set) => ({
78
228
  showScans: prefs.showScans ?? true,
79
229
  showGuides: prefs.showGuides ?? true,
80
230
  showGrid: prefs.showGrid ?? true,
231
+ showMeasurements: prefs.showMeasurements ?? true,
81
232
  };
82
233
  }),
83
234
  projectPreferences: {},
@@ -114,6 +265,8 @@ const useViewer = create()(persist((set) => ({
114
265
  previewSelectedIds: [],
115
266
  }),
116
267
  outliner: { selectedObjects: [], hoveredObjects: [] },
268
+ geometryRevision: 0,
269
+ bumpGeometryRevision: () => set((state) => ({ geometryRevision: state.geometryRevision + 1 })),
117
270
  exportScene: null,
118
271
  setExportScene: (fn) => set({ exportScene: fn }),
119
272
  debugColors: false,
@@ -126,6 +279,10 @@ const useViewer = create()(persist((set) => ({
126
279
  setInputDragging: (dragging) => set({ inputDragging: dragging }),
127
280
  }), {
128
281
  name: 'viewer-preferences',
282
+ merge: (persistedState, currentState) => ({
283
+ ...currentState,
284
+ ...normalizePersistedViewerState(persistedState),
285
+ }),
129
286
  partialize: (state) => ({
130
287
  cameraMode: state.cameraMode,
131
288
  sceneTheme: state.sceneTheme,
@@ -134,10 +291,25 @@ const useViewer = create()(persist((set) => ({
134
291
  colorPreset: state.colorPreset,
135
292
  edges: state.edges,
136
293
  shadows: state.shadows,
137
- unit: state.unit,
294
+ ...(state.unitExplicit ? { unit: state.unit } : {}),
138
295
  levelMode: state.levelMode,
139
296
  wallMode: state.wallMode,
140
297
  projectPreferences: state.projectPreferences,
141
298
  }),
142
299
  }));
300
+ /** Apply an authoritative country code (e.g. IP-derived by the host app) as
301
+ * the unit default. Stronger signal than the timezone heuristic used at store
302
+ * creation, but still a default: it never overrides an explicit user choice
303
+ * and is not persisted (the unit only sticks once the user touches the
304
+ * toggle). */
305
+ export function applyCountryUnitDefault(country) {
306
+ if (!country)
307
+ return;
308
+ const state = useViewer.getState();
309
+ if (state.unitExplicit)
310
+ return;
311
+ const unit = IMPERIAL_REGIONS.includes(country.toUpperCase()) ? 'imperial' : 'metric';
312
+ if (state.unit !== unit)
313
+ useViewer.setState({ unit });
314
+ }
143
315
  export default useViewer;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-viewer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-viewer.test.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // include Bun ambient types in its production declaration build.
3
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
4
+ import useViewer from './use-viewer';
5
+ const resetMeasurementPreferences = () => {
6
+ useViewer.setState({
7
+ projectId: null,
8
+ projectPreferences: {},
9
+ showMeasurements: true,
10
+ unit: 'metric',
11
+ });
12
+ };
13
+ beforeEach(resetMeasurementPreferences);
14
+ afterEach(resetMeasurementPreferences);
15
+ describe('measurement display preferences', () => {
16
+ test('stores global visibility per project and defaults new projects to visible', () => {
17
+ const viewer = useViewer.getState();
18
+ viewer.setProjectId('project-a');
19
+ viewer.setShowMeasurements(false);
20
+ expect(useViewer.getState()).toMatchObject({
21
+ projectId: 'project-a',
22
+ showMeasurements: false,
23
+ projectPreferences: {
24
+ 'project-a': { showMeasurements: false },
25
+ },
26
+ });
27
+ useViewer.getState().setProjectId('project-b');
28
+ expect(useViewer.getState().showMeasurements).toBe(true);
29
+ useViewer.getState().setProjectId('project-a');
30
+ expect(useViewer.getState().showMeasurements).toBe(false);
31
+ });
32
+ test('changes display units without changing measurement visibility preferences', () => {
33
+ const viewer = useViewer.getState();
34
+ viewer.setProjectId('project-a');
35
+ viewer.setShowMeasurements(false);
36
+ const preferences = useViewer.getState().projectPreferences;
37
+ useViewer.getState().setUnit('imperial');
38
+ expect(useViewer.getState().unit).toBe('imperial');
39
+ expect(useViewer.getState().projectPreferences).toEqual(preferences);
40
+ expect(useViewer.getState().showMeasurements).toBe(false);
41
+ });
42
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAMjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,eAAO,MAAM,aAAa,YA4BzB,CAAA;AA2DD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,GAC9C,KAAK,CAAC,cAAc,CA0DtB"}
1
+ {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAMjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,eAAO,MAAM,aAAa,YA4BzB,CAAA;AA2DD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAM,GAC9C,KAAK,CAAC,cAAc,CA6DtB"}
@@ -21,7 +21,7 @@ export const CeilingSystem = () => {
21
21
  // Process dirty ceilings
22
22
  dirtyNodes.forEach((id) => {
23
23
  const node = nodes[id];
24
- if (!node || node.type !== 'ceiling')
24
+ if (node?.type !== 'ceiling')
25
25
  return;
26
26
  const mesh = sceneRegistry.nodes.get(id);
27
27
  if (mesh) {
@@ -101,6 +101,9 @@ export function generateCeilingGeometry(ceilingNode, extraHoles = []) {
101
101
  // the whole command encoder.
102
102
  const degenerate = new THREE.BufferGeometry();
103
103
  degenerate.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
104
+ degenerate.setAttribute('normal', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
105
+ degenerate.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(6), 2));
106
+ degenerate.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array(6), 2));
104
107
  return degenerate;
105
108
  }
106
109
  // Create shape from polygon
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=door-animation.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"door-animation.test.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-animation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { DoorNode } from '@pascal-app/core';
5
+ import * as THREE from 'three';
6
+ import { buildDoorPreviewMesh, poseDoorMovingParts } from '../../index';
7
+ // The named moving group each operation-door builder emits at the closed pose.
8
+ const MOVING_GROUP = {
9
+ sliding: 'door-sliding-active',
10
+ pocket: 'door-pocket-leaf',
11
+ barn: 'door-barn-leaf',
12
+ 'garage-tiltup': 'door-tiltup-leaf',
13
+ folding: 'door-fold-0',
14
+ 'garage-sectional': 'door-sectional-0',
15
+ 'garage-rollup': 'door-rollup-curtain',
16
+ };
17
+ function worldPosition(object) {
18
+ object.updateMatrixWorld(true);
19
+ return object.getWorldPosition(new THREE.Vector3());
20
+ }
21
+ describe('operation door open kinematics', () => {
22
+ // Each operation door builds its moving parts in a named group at the closed
23
+ // pose; `poseDoorMovingParts` then drives the open motion (the single source
24
+ // of truth shared by the live system and the GLB exporter). This guards the
25
+ // build-once + pose-at-t contract: the group exists, rests closed, and moves.
26
+ for (const doorType of Object.keys(MOVING_GROUP)) {
27
+ test(`${doorType} builds a moving group that opens`, () => {
28
+ const node = DoorNode.parse({ id: `door_${doorType}`, doorType, operationState: 0 });
29
+ const mesh = buildDoorPreviewMesh(node);
30
+ const group = mesh.getObjectByName(MOVING_GROUP[doorType]);
31
+ expect(group).toBeDefined();
32
+ // Closed snapshot.
33
+ poseDoorMovingParts(node, mesh, 0);
34
+ const closedPos = worldPosition(group);
35
+ const closedScaleY = group.scale.y;
36
+ const closedRotX = group.rotation.x;
37
+ const closedRotY = group.rotation.y;
38
+ // Open snapshot — at least one of translation / rotation / scale changes.
39
+ poseDoorMovingParts(node, mesh, 1);
40
+ const openPos = worldPosition(group);
41
+ const moved = closedPos.distanceTo(openPos) > 0.05 ||
42
+ Math.abs(closedScaleY - group.scale.y) > 0.1 ||
43
+ Math.abs(closedRotX - group.rotation.x) > 0.05 ||
44
+ Math.abs(closedRotY - group.rotation.y) > 0.05;
45
+ expect(moved).toBe(true);
46
+ // Posing back to closed is the identity of the rest pose.
47
+ poseDoorMovingParts(node, mesh, 0);
48
+ expect(worldPosition(group).distanceTo(closedPos)).toBeLessThan(1e-6);
49
+ });
50
+ }
51
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=door-floor-alignment.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"door-floor-alignment.test.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-floor-alignment.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { DoorNode } from '@pascal-app/core';
5
+ import * as THREE from 'three';
6
+ import { buildDoorPreviewMesh } from '../../index';
7
+ const DOOR_TYPES = [
8
+ 'hinged',
9
+ 'double',
10
+ 'french',
11
+ 'folding',
12
+ 'pocket',
13
+ 'barn',
14
+ 'sliding',
15
+ 'garage-sectional',
16
+ 'garage-rollup',
17
+ 'garage-tiltup',
18
+ ];
19
+ function visibleBounds(mesh) {
20
+ mesh.updateMatrixWorld(true);
21
+ const bounds = new THREE.Box3();
22
+ for (const child of mesh.children) {
23
+ if (child.name === 'cutout')
24
+ continue;
25
+ bounds.expandByObject(child, true);
26
+ }
27
+ return bounds;
28
+ }
29
+ describe('door floor alignment', () => {
30
+ for (const doorType of DOOR_TYPES) {
31
+ test(`${doorType} does not extend below the opening floor`, () => {
32
+ const node = DoorNode.parse({
33
+ id: `door_floor-alignment-${doorType}`,
34
+ doorType,
35
+ operationState: 0,
36
+ threshold: true,
37
+ });
38
+ const mesh = buildDoorPreviewMesh(node);
39
+ const bounds = visibleBounds(mesh);
40
+ expect(bounds.min.y).toBeGreaterThanOrEqual(-node.height / 2 - 1e-6);
41
+ });
42
+ }
43
+ test('keeps the wall cutout bottom locked to the opening floor', () => {
44
+ const node = DoorNode.parse({ id: 'door_floor-alignment-cutout' });
45
+ const mesh = buildDoorPreviewMesh(node);
46
+ const cutout = mesh.getObjectByName('cutout');
47
+ cutout.geometry.computeBoundingBox();
48
+ expect(cutout.geometry.boundingBox?.min.y).toBeCloseTo(-node.height / 2, 6);
49
+ });
50
+ });
@@ -1,2 +1,26 @@
1
+ import { type DoorNode } from '@pascal-app/core';
2
+ import * as THREE from 'three';
1
3
  export declare const DoorSystem: () => null;
4
+ /**
5
+ * Pose an operation door's moving parts (sliding/pocket/barn leaf, tilt-up and
6
+ * sectional panels, folding chain, roll-up curtain) at `value` (0 = closed,
7
+ * 1 = open) by transforming the named groups its builder emitted at the closed
8
+ * pose. Returns true when the door type has a pose path and its groups exist.
9
+ *
10
+ * Mirrors `poseWindowMovingParts`: the live door system calls it after a
11
+ * (re)build so `operationState` is reflected, and the GLB exporter calls it on a
12
+ * clone to sample keyframes. Swing doors are not handled here — their leaf group
13
+ * carries a `pascalSwingLeaf` marker the exporter reads directly.
14
+ *
15
+ * Roll-up is the one type whose live geometry changes (slats vanish onto a
16
+ * drum), which a glTF clip can't express; the baked approximation scales the
17
+ * curtain up into the lintel. The live roll-up animation keeps its full-detail
18
+ * rebuild and is intentionally NOT routed through this scale.
19
+ */
20
+ export declare function poseDoorMovingParts(node: DoorNode, mesh: THREE.Object3D | undefined, value: number): boolean;
21
+ /**
22
+ * Build a fresh door mesh for preview/ghost rendering.
23
+ * Returns a mesh with an invisible hitbox root and visible children (frame, panels, hardware).
24
+ */
25
+ export declare function buildDoorPreviewMesh(node: DoorNode): THREE.Mesh;
2
26
  //# sourceMappingURL=door-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"AA8CA,eAAO,MAAM,UAAU,YAwFtB,CAAA"}
1
+ {"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAYd,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA0D9B,eAAO,MAAM,UAAU,YA6GtB,CAAA;AAupCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,KAAK,CAAC,QAAQ,GAAG,SAAS,EAChC,KAAK,EAAE,MAAM,GACZ,OAAO,CA6GT;AAyuCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC,IAAI,CAI/D"}