@pascal-app/viewer 0.8.0 → 0.9.1

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 (194) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  10. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  11. package/dist/components/viewer/ground-occluder.js +12 -24
  12. package/dist/components/viewer/index.d.ts +37 -1
  13. package/dist/components/viewer/index.d.ts.map +1 -1
  14. package/dist/components/viewer/index.js +186 -63
  15. package/dist/components/viewer/lights.d.ts +1 -1
  16. package/dist/components/viewer/lights.d.ts.map +1 -1
  17. package/dist/components/viewer/lights.js +170 -43
  18. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  19. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  20. package/dist/components/viewer/perf-monitor.js +99 -11
  21. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  22. package/dist/components/viewer/post-processing.js +231 -54
  23. package/dist/components/viewer/registered-systems.d.ts +12 -0
  24. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  25. package/dist/components/viewer/registered-systems.js +47 -0
  26. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  27. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  28. package/dist/components/viewer/scene-bvh.js +94 -0
  29. package/dist/components/viewer/selection-manager.d.ts +1 -1
  30. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  31. package/dist/components/viewer/selection-manager.js +20 -5
  32. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  33. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  34. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  35. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  36. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  37. package/dist/hooks/use-node-events.d.ts +5 -73
  38. package/dist/hooks/use-node-events.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.js +19 -7
  40. package/dist/index.d.ts +36 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +67 -3
  43. package/dist/lib/csg-utils.d.ts +16 -0
  44. package/dist/lib/csg-utils.d.ts.map +1 -0
  45. package/dist/lib/csg-utils.js +33 -0
  46. package/dist/lib/drawable-geometry.d.ts +21 -0
  47. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  48. package/dist/lib/drawable-geometry.js +22 -0
  49. package/dist/lib/edge-style.d.ts +3 -0
  50. package/dist/lib/edge-style.d.ts.map +1 -0
  51. package/dist/lib/edge-style.js +11 -0
  52. package/dist/lib/gpu-perf.d.ts +4 -0
  53. package/dist/lib/gpu-perf.d.ts.map +1 -0
  54. package/dist/lib/gpu-perf.js +23 -0
  55. package/dist/lib/ink-edges.d.ts +9 -0
  56. package/dist/lib/ink-edges.d.ts.map +1 -0
  57. package/dist/lib/ink-edges.js +43 -0
  58. package/dist/lib/isolation.d.ts +39 -0
  59. package/dist/lib/isolation.d.ts.map +1 -0
  60. package/dist/lib/isolation.js +103 -0
  61. package/dist/lib/layers.d.ts +18 -0
  62. package/dist/lib/layers.d.ts.map +1 -1
  63. package/dist/lib/layers.js +18 -0
  64. package/dist/lib/materials.d.ts +25 -15
  65. package/dist/lib/materials.d.ts.map +1 -1
  66. package/dist/lib/materials.js +277 -62
  67. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  68. package/dist/lib/merged-outline-node.js +7 -0
  69. package/dist/lib/polygon-union.d.ts +4 -0
  70. package/dist/lib/polygon-union.d.ts.map +1 -0
  71. package/dist/lib/polygon-union.js +289 -0
  72. package/dist/lib/polygon-union.test.d.ts +2 -0
  73. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  74. package/dist/lib/polygon-union.test.js +105 -0
  75. package/dist/lib/scene-themes.d.ts +29 -0
  76. package/dist/lib/scene-themes.d.ts.map +1 -0
  77. package/dist/lib/scene-themes.js +193 -0
  78. package/dist/store/use-viewer.d.ts +42 -4
  79. package/dist/store/use-viewer.d.ts.map +1 -1
  80. package/dist/store/use-viewer.js +26 -3
  81. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  82. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  83. package/dist/systems/ceiling/ceiling-system.js +62 -12
  84. package/dist/systems/door/door-system.d.ts.map +1 -1
  85. package/dist/systems/door/door-system.js +373 -22
  86. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  87. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  88. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  89. package/dist/systems/fence/fence-system.d.ts +3 -0
  90. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  91. package/dist/systems/fence/fence-system.js +9 -5
  92. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  93. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  94. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  95. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  96. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  97. package/dist/systems/geometry/geometry-system.js +279 -0
  98. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  99. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  100. package/dist/systems/item/item-system.d.ts +7 -0
  101. package/dist/systems/item/item-system.d.ts.map +1 -1
  102. package/dist/systems/item/item-system.js +8 -11
  103. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  104. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  105. package/dist/systems/item-light/item-light-system.js +18 -5
  106. package/dist/systems/roof/roof-materials.d.ts +3 -2
  107. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  108. package/dist/systems/roof/roof-materials.js +38 -14
  109. package/dist/systems/roof/roof-system.d.ts +27 -0
  110. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  111. package/dist/systems/roof/roof-system.js +396 -59
  112. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  113. package/dist/systems/slab/slab-system.js +167 -49
  114. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  115. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  116. package/dist/systems/slab/slab-system.test.js +62 -0
  117. package/dist/systems/stair/stair-materials.d.ts +4 -3
  118. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  119. package/dist/systems/stair/stair-materials.js +30 -15
  120. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  121. package/dist/systems/stair/stair-system.js +50 -61
  122. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  123. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  124. package/dist/systems/surface-hole-geometry.js +4 -0
  125. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  126. package/dist/systems/wall/wall-cutout.js +18 -2
  127. package/dist/systems/wall/wall-materials.d.ts +4 -3
  128. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  129. package/dist/systems/wall/wall-materials.js +52 -28
  130. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  131. package/dist/systems/wall/wall-system.js +171 -26
  132. package/dist/systems/window/window-system.d.ts.map +1 -1
  133. package/dist/systems/window/window-system.js +71 -7
  134. package/package.json +3 -4
  135. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  136. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  137. package/dist/components/renderers/building/building-renderer.js +0 -11
  138. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  139. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  140. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  141. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  142. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  143. package/dist/components/renderers/column/column-renderer.js +0 -597
  144. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  145. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  146. package/dist/components/renderers/door/door-renderer.js +0 -16
  147. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  148. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  149. package/dist/components/renderers/fence/fence-renderer.js +0 -15
  150. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  151. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  152. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  153. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  154. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  155. package/dist/components/renderers/item/item-renderer.js +0 -172
  156. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  157. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  158. package/dist/components/renderers/level/level-renderer.js +0 -11
  159. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  160. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  161. package/dist/components/renderers/roof/roof-materials.js +0 -16
  162. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  163. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  164. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  165. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  166. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  167. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  168. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  169. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  170. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  171. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  172. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  173. package/dist/components/renderers/site/site-renderer.js +0 -106
  174. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  175. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  176. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  177. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  178. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  179. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  180. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  181. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  182. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  183. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  184. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  185. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
  186. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  187. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  188. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  189. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  190. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  191. package/dist/components/renderers/window/window-renderer.js +0 -21
  192. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  193. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  194. package/dist/components/renderers/zone/zone-renderer.js +0 -181
@@ -1,5 +1,5 @@
1
1
  import { type AnyNode } from '@pascal-app/core';
2
2
  export declare const NodeRenderer: ({ nodeId }: {
3
3
  nodeId: AnyNode["id"];
4
- }) => import("react/jsx-runtime").JSX.Element | null;
4
+ }) => import("react").JSX.Element | null;
5
5
  //# sourceMappingURL=node-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,kBAAkB,CAAA;AAqBzD,eAAO,MAAM,YAAY,GAAI,YAAY;IAAE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,mDA4BjE,CAAA"}
1
+ {"version":3,"file":"node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAA+C,MAAM,kBAAkB,CAAA;AAqB5F,eAAO,MAAM,YAAY,GAAI,YAAY;IAAE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,uCAwBjE,CAAA"}
@@ -1,28 +1,44 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useScene } from '@pascal-app/core';
4
- import { BuildingRenderer } from './building/building-renderer';
5
- import { CeilingRenderer } from './ceiling/ceiling-renderer';
6
- import { ColumnRenderer } from './column/column-renderer';
7
- import { DoorRenderer } from './door/door-renderer';
8
- import { FenceRenderer } from './fence/fence-renderer';
9
- import { GuideRenderer } from './guide/guide-renderer';
10
- import { ItemRenderer } from './item/item-renderer';
11
- import { LevelRenderer } from './level/level-renderer';
12
- import { RoofRenderer } from './roof/roof-renderer';
13
- import { RoofSegmentRenderer } from './roof-segment/roof-segment-renderer';
14
- import { ScanRenderer } from './scan/scan-renderer';
15
- import { SiteRenderer } from './site/site-renderer';
16
- import { SlabRenderer } from './slab/slab-renderer';
17
- import { SpawnRenderer } from './spawn/spawn-renderer';
18
- import { StairRenderer } from './stair/stair-renderer';
19
- import { StairSegmentRenderer } from './stair-segment/stair-segment-renderer';
20
- import { WallRenderer } from './wall/wall-renderer';
21
- import { WindowRenderer } from './window/window-renderer';
22
- import { ZoneRenderer } from './zone/zone-renderer';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { nodeRegistry, useScene } from '@pascal-app/core';
4
+ import { lazy, Suspense } from 'react';
5
+ import { ParametricNodeRenderer } from './parametric-node-renderer';
6
+ // Cache lazy components by their RendererSource so React.lazy isn't re-invoked
7
+ // on every render — that would create a new Suspense boundary each time.
8
+ const lazyCache = new WeakMap();
9
+ function getRegistryRenderer(source) {
10
+ const cached = lazyCache.get(source);
11
+ if (cached)
12
+ return cached;
13
+ // GLB / instanced-GLB sources lower onto built-in renderers landed in
14
+ // Phase 5 — for now only parametric (lazy module) sources are honored.
15
+ if (source.kind !== 'parametric')
16
+ return null;
17
+ const Comp = lazy(source.module);
18
+ lazyCache.set(source, Comp);
19
+ return Comp;
20
+ }
23
21
  export const NodeRenderer = ({ nodeId }) => {
24
22
  const node = useScene((state) => state.nodes[nodeId]);
25
23
  if (!node)
26
24
  return null;
27
- return (_jsxs(_Fragment, { children: [node.type === 'site' && _jsx(SiteRenderer, { node: node }), node.type === 'building' && _jsx(BuildingRenderer, { node: node }), node.type === 'ceiling' && _jsx(CeilingRenderer, { node: node }), node.type === 'column' && _jsx(ColumnRenderer, { node: node }), node.type === 'level' && _jsx(LevelRenderer, { node: node }), node.type === 'item' && _jsx(ItemRenderer, { node: node }), node.type === 'slab' && _jsx(SlabRenderer, { node: node }), node.type === 'spawn' && _jsx(SpawnRenderer, { node: node }), node.type === 'wall' && _jsx(WallRenderer, { node: node }), node.type === 'fence' && _jsx(FenceRenderer, { node: node }), node.type === 'door' && _jsx(DoorRenderer, { node: node }), node.type === 'window' && _jsx(WindowRenderer, { node: node }), node.type === 'zone' && _jsx(ZoneRenderer, { node: node }), node.type === 'roof' && _jsx(RoofRenderer, { node: node }), node.type === 'roof-segment' && _jsx(RoofSegmentRenderer, { node: node }), node.type === 'stair' && _jsx(StairRenderer, { node: node }), node.type === 'stair-segment' && _jsx(StairSegmentRenderer, { node: node }), node.type === 'scan' && _jsx(ScanRenderer, { node: node }), node.type === 'guide' && _jsx(GuideRenderer, { node: node })] }));
25
+ const def = nodeRegistry.get(node.type);
26
+ if (!def)
27
+ return null;
28
+ // Two-checkbox dispatch (see wiki/architecture/node-definitions.md):
29
+ // 1. Custom renderer — JSX-side composition for kinds that need GLB,
30
+ // drei, <Html>, instancing, shader materials.
31
+ // 2. Else, if the kind ships `def.geometry`, the generic empty-group
32
+ // <ParametricNodeRenderer> is filled by <GeometrySystem> from the
33
+ // pure builder.
34
+ if (def.renderer) {
35
+ const Renderer = getRegistryRenderer(def.renderer);
36
+ if (!Renderer)
37
+ return null;
38
+ return (_jsx(Suspense, { fallback: null, children: _jsx(Renderer, { node: node }) }));
39
+ }
40
+ if (def.geometry) {
41
+ return _jsx(ParametricNodeRenderer, { node: node });
42
+ }
43
+ return null;
28
44
  };
@@ -0,0 +1,5 @@
1
+ import { type AnyNode } from '@pascal-app/core';
2
+ export declare const ParametricNodeRenderer: ({ node }: {
3
+ node: AnyNode;
4
+ }) => import("react").JSX.Element;
5
+ //# sourceMappingURL=parametric-node-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parametric-node-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/parametric-node-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EAMb,MAAM,kBAAkB,CAAA;AAwCzB,eAAO,MAAM,sBAAsB,GAAI,UAAU;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,gCA4CjE,CAAA"}
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useLiveNodeOverrides, useLiveTransforms, useRegistry, useScene, } from '@pascal-app/core';
4
+ import { useLayoutEffect, useRef } from 'react';
5
+ import { useNodeEvents } from '../../hooks/use-node-events';
6
+ import { NodeRenderer } from './node-renderer';
7
+ export const ParametricNodeRenderer = ({ node }) => {
8
+ const ref = useRef(null);
9
+ const n = node;
10
+ const handlers = useNodeEvents(node, node.type);
11
+ const liveTransform = useLiveTransforms((s) => s.get(node.id));
12
+ // Registry arrow handles (rotation gizmo, position-affecting patches)
13
+ // write to `useLiveNodeOverrides`. GeometrySystem already merges that
14
+ // for the mesh body; we also fold it into the outer group's
15
+ // position/rotation so the rotation arrow shows live motion instead
16
+ // of snapping only on commit. Per-node subscription so unrelated
17
+ // override writes don't re-render the whole tree.
18
+ const liveOverride = useLiveNodeOverrides((s) => s.overrides.get(node.id));
19
+ const overrideRotation = liveOverride?.rotation;
20
+ const overridePosition = liveOverride?.position;
21
+ useRegistry(node.id, node.type, ref);
22
+ useLayoutEffect(() => {
23
+ useScene.getState().markDirty(node.id);
24
+ }, [node.id]);
25
+ const position = liveTransform?.position ?? overridePosition ?? n.position ?? [0, 0, 0];
26
+ const rawRotation = overrideRotation ?? n.rotation;
27
+ const rotation = liveTransform?.rotation !== undefined
28
+ ? [0, liveTransform.rotation, 0]
29
+ : typeof rawRotation === 'number'
30
+ ? [0, rawRotation, 0]
31
+ : (rawRotation ?? [0, 0, 0]);
32
+ return (_jsx("group", { position: position, ref: ref, rotation: rotation, visible: n.visible !== false, ...handlers, children: Array.isArray(n.children) &&
33
+ n.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, `${node.id}:${childId}`))) }));
34
+ };
@@ -1,2 +1,2 @@
1
- export declare const SceneRenderer: () => import("react/jsx-runtime").JSX.Element;
1
+ export declare const SceneRenderer: () => import("react").JSX.Element;
2
2
  //# sourceMappingURL=scene-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/scene-renderer.tsx"],"names":[],"mappings":"AAKA,eAAO,MAAM,aAAa,+CAUzB,CAAA"}
1
+ {"version":3,"file":"scene-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/renderers/scene-renderer.tsx"],"names":[],"mappings":"AAKA,eAAO,MAAM,aAAa,mCAUzB,CAAA"}
@@ -1,2 +1,2 @@
1
- export declare const GroundOccluder: () => import("react/jsx-runtime").JSX.Element;
1
+ export declare const GroundOccluder: () => import("react").JSX.Element;
2
2
  //# sourceMappingURL=ground-occluder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ground-occluder.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/ground-occluder.tsx"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc,+CAuG1B,CAAA"}
1
+ {"version":3,"file":"ground-occluder.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/ground-occluder.tsx"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc,mCAuF1B,CAAA"}
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useScene } from '@pascal-app/core';
3
- import polygonClipping from 'polygon-clipping';
4
3
  import { useMemo } from 'react';
5
4
  import * as THREE from 'three';
5
+ import { unionPolygons } from '../../lib/polygon-union';
6
+ import { getSceneTheme } from '../../lib/scene-themes';
6
7
  import useViewer from '../../store/use-viewer';
7
8
  export const GroundOccluder = () => {
8
- const theme = useViewer((state) => state.theme);
9
- const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa';
9
+ const bgColor = useViewer((state) => getSceneTheme(state.sceneTheme).ground);
10
10
  const nodes = useScene((state) => state.nodes);
11
11
  const shape = useMemo(() => {
12
12
  const s = new THREE.Shape();
@@ -49,28 +49,16 @@ export const GroundOccluder = () => {
49
49
  polygons.push(node.polygon);
50
50
  });
51
51
  if (polygons.length > 0) {
52
- // Format for polygon-clipping: [[[x, y], [x, y], ...]]
53
- const multiPolygons = polygons.map((pts) => {
54
- const ring = pts.map((p) => [p[0], -p[1]]); // Negate Y (which was Z)
55
- return [ring];
56
- });
57
- // Union all polygons together to prevent artifacts from overlapping
58
- const unionedPolygons = polygonClipping.union(multiPolygons[0], ...multiPolygons.slice(1));
59
- // Add each resulting unioned polygon as a hole
60
- for (const geom of unionedPolygons) {
61
- // First ring in each geometry is the exterior ring
62
- if (geom.length > 0) {
63
- const ring = geom[0];
64
- const hole = new THREE.Path();
65
- if (ring.length > 0) {
66
- hole.moveTo(ring[0][0], ring[0][1]);
67
- for (let i = 1; i < ring.length; i++) {
68
- hole.lineTo(ring[i][0], ring[i][1]);
69
- }
70
- hole.closePath();
71
- s.holes.push(hole);
72
- }
52
+ for (const ring of unionPolygons(polygons.map((pts) => pts.map((p) => [p[0], -p[1]])))) {
53
+ if (ring.length < 3)
54
+ continue;
55
+ const hole = new THREE.Path();
56
+ hole.moveTo(ring[0][0], ring[0][1]);
57
+ for (let i = 1; i < ring.length; i++) {
58
+ hole.lineTo(ring[i][0], ring[i][1]);
73
59
  }
60
+ hole.closePath();
61
+ s.holes.push(hole);
74
62
  }
75
63
  }
76
64
  return s;
@@ -1,5 +1,8 @@
1
+ import { type AnyNodeId } from '@pascal-app/core';
1
2
  import { type ThreeToJSXElements } from '@react-three/fiber';
2
3
  import * as THREE from 'three/webgpu';
4
+ import type { ColorPreset, RenderShading } from '../../lib/materials';
5
+ import { type RenderContext } from '../../store/use-viewer';
3
6
  import { type HoverStyles } from './post-processing';
4
7
  declare module '@react-three/fiber' {
5
8
  interface ThreeElements extends ThreeToJSXElements<typeof THREE> {
@@ -11,7 +14,40 @@ interface ViewerProps {
11
14
  selectionManager?: 'default' | 'custom';
12
15
  perf?: boolean;
13
16
  useBvh?: boolean;
17
+ renderContext?: RenderContext;
18
+ defaultRender?: {
19
+ shading?: RenderShading;
20
+ textures?: boolean;
21
+ colorPreset?: ColorPreset;
22
+ };
23
+ /**
24
+ * Visibility filter on the live canvas. When non-null, every registered
25
+ * node group whose id is not in `isolate` (or in the isolated set's
26
+ * ancestor / descendant closure) is hidden. Pass `null` (or omit) to
27
+ * clear. Powers the unified preset-capture flow (community modal sets
28
+ * this to the subtree it wants to thumbnail) and is the building block
29
+ * for a future focus-mode UX.
30
+ */
31
+ isolate?: AnyNodeId[] | null;
32
+ /**
33
+ * Host-controlled key for scene readiness. Change it whenever a new scene
34
+ * graph is being loaded; the viewer will report not-ready until the graph is
35
+ * mounted, build systems have had a frame to settle, and one rendered frame
36
+ * has presented the new content.
37
+ */
38
+ sceneReadyKey?: string | number | null;
39
+ onSceneReadyChange?: (ready: boolean) => void;
14
40
  }
15
- declare const Viewer: React.FC<ViewerProps>;
41
+ /** Imperative handle exposed via `ref` on `<Viewer>`. */
42
+ export type ViewerHandle = {
43
+ /**
44
+ * Apply / clear the same visibility filter as the `isolate` prop. Useful
45
+ * for transient cases (a temporary hover-to-isolate UX) where holding
46
+ * the value in React state would be over-engineering. Passing `null`
47
+ * clears.
48
+ */
49
+ setIsolated(ids: AnyNodeId[] | null): void;
50
+ };
51
+ declare const Viewer: import("react").ForwardRefExoticComponent<ViewerProps & import("react").RefAttributes<ViewerHandle>>;
16
52
  export default Viewer;
17
53
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAEhG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAwBrC,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA+B1F,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AAkFD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,QAAA,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAwGjC,CAAA;AASD,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkB,KAAK,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAEhG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAIrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAErE,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAQtE,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAM1F,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AAuOD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,WAAW,CAAC,EAAE,WAAW,CAAA;KAC1B,CAAA;IACD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9C;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED,QAAA,MAAM,MAAM,sGAmKV,CAAA;AAkBF,eAAe,MAAM,CAAA"}
@@ -1,56 +1,26 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Bvh } from '@react-three/drei';
3
+ import { nodeRegistry, StairOpeningSystem, sceneRegistry, useScene, } from '@pascal-app/core';
4
4
  import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
5
- import { useEffect, useMemo, useRef } from 'react';
5
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
6
6
  import * as THREE from 'three/webgpu';
7
- import useViewer from '../../store/use-viewer';
8
- import { CeilingSystem } from '../../systems/ceiling/ceiling-system';
9
- import { DoorAnimationSystem } from '../../systems/door/door-animation-system';
10
- import { DoorSystem } from '../../systems/door/door-system';
11
- import { FenceSystem } from '../../systems/fence/fence-system';
12
- import { GuideSystem } from '../../systems/guide/guide-system';
13
- import { ItemSystem } from '../../systems/item/item-system';
14
- import { ItemLightSystem } from '../../systems/item-light/item-light-system';
15
- import { LevelSystem } from '../../systems/level/level-system';
16
- import { RoofSystem } from '../../systems/roof/roof-system';
17
- import { ScanSystem } from '../../systems/scan/scan-system';
18
- import { SlabSystem } from '../../systems/slab/slab-system';
19
- import { StairSystem } from '../../systems/stair/stair-system';
20
- import { WallCutout } from '../../systems/wall/wall-cutout';
21
- import { WallSystem } from '../../systems/wall/wall-system';
22
- import { WindowAnimationSystem } from '../../systems/window/window-animation-system';
23
- import { WindowSystem } from '../../systems/window/window-system';
24
- import { ZoneSystem } from '../../systems/zone/zone-system';
7
+ import { hasDrawableGeometry } from '../../lib/drawable-geometry';
8
+ import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
9
+ import { applyIsolation, clearIsolation } from '../../lib/isolation';
10
+ import { getSceneTheme } from '../../lib/scene-themes';
11
+ import useViewer, {} from '../../store/use-viewer';
12
+ import { FloorElevationSystem } from '../../systems/floor-elevation/floor-elevation-system';
13
+ import { GeometrySystem } from '../../systems/geometry/geometry-system';
25
14
  import { ErrorBoundary } from '../error-boundary';
26
15
  import { SceneRenderer } from '../renderers/scene-renderer';
27
16
  import FrameLimiter from './frame-limiter';
28
17
  import { Lights } from './lights';
29
18
  import { PerfMonitor } from './perf-monitor';
30
19
  import PostProcessing, { DEFAULT_HOVER_STYLES } from './post-processing';
20
+ import { RegisteredSystems } from './registered-systems';
21
+ import { SceneBvh } from './scene-bvh';
31
22
  import { SelectionManager } from './selection-manager';
32
23
  import { ViewerCamera } from './viewer-camera';
33
- function AnimatedBackground({ isDark }) {
34
- const targetColor = useMemo(() => new THREE.Color(), []);
35
- const initialized = useRef(false);
36
- useFrame(({ scene }, delta) => {
37
- const dt = Math.min(delta, 0.1) * 4;
38
- const targetHex = isDark ? '#1f2433' : '#ffffff';
39
- if (!(scene.background && scene.background instanceof THREE.Color)) {
40
- scene.background = new THREE.Color(targetHex);
41
- initialized.current = true;
42
- return;
43
- }
44
- if (!initialized.current) {
45
- scene.background.set(targetHex);
46
- initialized.current = true;
47
- return;
48
- }
49
- targetColor.set(targetHex);
50
- scene.background.lerp(targetColor, dt);
51
- });
52
- return null;
53
- }
54
24
  extend(THREE);
55
25
  // R3F's <Canvas> useLayoutEffect has no deps, so any re-render (theme switch,
56
26
  // parent re-render, StrictMode double-mount) re-invokes `configure()`. With a
@@ -67,6 +37,49 @@ extend(THREE);
67
37
  // concurrent configure() calls await the same init instead of creating two
68
38
  // renderers in parallel and only caching the second.
69
39
  const WEBGPU_RENDERER_CACHE = new WeakMap();
40
+ const SCENE_READY_SETTLED_FRAMES = 2;
41
+ const SCENE_READY_MAX_WAIT_FRAMES = 180;
42
+ const DIRTY_BUILD_KINDS = new Set([
43
+ 'ceiling',
44
+ 'door',
45
+ 'item',
46
+ 'roof',
47
+ 'roof-segment',
48
+ 'stair',
49
+ 'stair-segment',
50
+ 'wall',
51
+ 'window',
52
+ ]);
53
+ const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakSet();
54
+ /**
55
+ * Renderer-level safety net against the empty-vertex-buffer crash.
56
+ *
57
+ * Wraps the per-object render function so any draw whose geometry has a count-0
58
+ * `position` attribute is skipped instead of submitted. One such draw leaves
59
+ * WebGPU vertex buffer slot 0 unbound, which the validator rejects and which
60
+ * poisons the *whole* command encoder — so a single stray empty mesh (e.g. a
61
+ * transient placeholder, or a derived edge/outline geometry) flickers the entire
62
+ * canvas, not just itself. See `hasDrawableGeometry`.
63
+ *
64
+ * The custom render-object function is the documented three.js hook for this
65
+ * (`Renderer.setRenderObjectFunction`); it must call `renderObject()` for
66
+ * everything it keeps. `MergedOutlineNode` captures and restores this function
67
+ * around its passes, so the guard survives outline rendering (its own passes
68
+ * carry the same check inline).
69
+ */
70
+ function installEmptyDrawGuard(renderer) {
71
+ renderer.setRenderObjectFunction((object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId) => {
72
+ if (!hasDrawableGeometry(geometry)) {
73
+ if (warnedEmptyDraw && !warnedEmptyDraw.has(geometry ?? object)) {
74
+ warnedEmptyDraw.add(geometry ?? object);
75
+ console.warn('[viewer] skipped a draw with an empty position buffer (would poison the WebGPU command encoder)', { name: object?.name, type: object?.type, material: material?.name });
76
+ }
77
+ return;
78
+ }
79
+ ;
80
+ renderer.renderObject(object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId);
81
+ });
82
+ }
70
83
  function GPUDeviceWatcher() {
71
84
  const gl = useThree((s) => s.gl);
72
85
  useEffect(() => {
@@ -84,46 +97,149 @@ function GPUDeviceWatcher() {
84
97
  features: Array.from(device.features ?? []),
85
98
  });
86
99
  device.lost.then((info) => {
87
- console.error(`[viewer] WebGPU device lost: reason="${info.reason}", message="${info.message}". ` +
100
+ console.error(`[viewer] WebGPU device lost: reason="${info.reason ?? 'unknown'}", message="${info.message ?? ''}". ` +
88
101
  'The page must be reloaded to recover the GPU context.');
89
102
  });
90
103
  // Uncaptured errors are normally silent (only console-warned by Chrome at
91
104
  // best). Pipe them to console.error so silent mobile crashes show up.
92
105
  const onUncapturedError = (event) => {
93
- console.error('[viewer] WebGPU uncaptured error:', event.error.message, event.error);
106
+ console.error('[viewer] WebGPU uncaptured error:', event?.error?.message, event?.error);
94
107
  };
95
- device.addEventListener('uncapturederror', onUncapturedError);
108
+ device.addEventListener?.('uncapturederror', onUncapturedError);
96
109
  return () => {
97
- device.removeEventListener('uncapturederror', onUncapturedError);
110
+ device.removeEventListener?.('uncapturederror', onUncapturedError);
98
111
  };
99
112
  }, [gl]);
100
113
  return null;
101
114
  }
102
- const Viewer = ({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, }) => {
103
- const theme = useViewer((state) => state.theme);
104
- return (_jsxs(Canvas, { camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${theme === 'dark' ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, 1.5], frameloop: "never", gl: ((props) => {
115
+ function ToneMappingExposure() {
116
+ const sceneTheme = useViewer((state) => state.sceneTheme);
117
+ const gl = useThree((state) => state.gl);
118
+ const invalidate = useThree((state) => state.invalidate);
119
+ useEffect(() => {
120
+ gl.toneMappingExposure = getSceneTheme(sceneTheme).toneMappingExposure;
121
+ invalidate();
122
+ }, [gl, invalidate, sceneTheme]);
123
+ return null;
124
+ }
125
+ function hasPendingSceneBuildWork() {
126
+ const { dirtyNodes, nodes } = useScene.getState();
127
+ for (const id of dirtyNodes) {
128
+ const node = nodes[id];
129
+ if (!node)
130
+ continue;
131
+ const def = nodeRegistry.get(node.type);
132
+ if (def?.geometry || def?.capabilities?.floorPlaced || DIRTY_BUILD_KINDS.has(node.type)) {
133
+ return true;
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+ function hasCommittedSceneRoot() {
139
+ const { nodes, rootNodeIds } = useScene.getState();
140
+ if (rootNodeIds.length === 0)
141
+ return Object.keys(nodes).length === 0;
142
+ return rootNodeIds.some((id) => sceneRegistry.nodes.has(id));
143
+ }
144
+ function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, }) {
145
+ const readyRef = useRef(false);
146
+ const settledFramesRef = useRef(0);
147
+ const waitedFramesRef = useRef(0);
148
+ const onSceneReadyChangeRef = useRef(onSceneReadyChange);
149
+ useEffect(() => {
150
+ onSceneReadyChangeRef.current = onSceneReadyChange;
151
+ }, [onSceneReadyChange]);
152
+ useEffect(() => {
153
+ void sceneReadyKey;
154
+ readyRef.current = false;
155
+ settledFramesRef.current = 0;
156
+ waitedFramesRef.current = 0;
157
+ onSceneReadyChangeRef.current?.(false);
158
+ }, [sceneReadyKey]);
159
+ useFrame(() => {
160
+ if (!(onSceneReadyChangeRef.current && !readyRef.current))
161
+ return;
162
+ waitedFramesRef.current += 1;
163
+ if (waitedFramesRef.current < SCENE_READY_MAX_WAIT_FRAMES &&
164
+ (!hasCommittedSceneRoot() || hasPendingSceneBuildWork())) {
165
+ settledFramesRef.current = 0;
166
+ return;
167
+ }
168
+ settledFramesRef.current += 1;
169
+ if (settledFramesRef.current < SCENE_READY_SETTLED_FRAMES)
170
+ return;
171
+ readyRef.current = true;
172
+ onSceneReadyChangeRef.current(true);
173
+ }, 10);
174
+ return null;
175
+ }
176
+ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', defaultRender, isolate, sceneReadyKey, onSceneReadyChange, }, ref) {
177
+ useImperativeHandle(ref, () => ({
178
+ setIsolated: (ids) => applyIsolation(ids),
179
+ }), []);
180
+ // Track the most recently-applied isolation so the cleanup path can
181
+ // restore visibility even if the prop is removed while the component is
182
+ // still mounted. `clearIsolation()` is a no-op when nothing was applied.
183
+ const isolateRef = useRef(undefined);
184
+ useEffect(() => {
185
+ isolateRef.current = isolate ?? null;
186
+ applyIsolation(isolate ?? null);
187
+ return () => {
188
+ // Only clear if this effect was the one that applied — protects
189
+ // against a parent unmount racing with a setIsolated() consumer.
190
+ if (isolateRef.current === isolate)
191
+ clearIsolation();
192
+ };
193
+ }, [isolate]);
194
+ const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
195
+ const defaultShading = defaultRender?.shading;
196
+ const defaultTextures = defaultRender?.textures;
197
+ const defaultColorPreset = defaultRender?.colorPreset;
198
+ const hasDefaultRender = defaultRender != null;
199
+ useEffect(() => {
200
+ const ctx = renderContext;
201
+ useViewer.getState().setRenderContext(ctx);
202
+ const { shading, shadingByContext, setShading } = useViewer.getState();
203
+ setShading(shadingByContext[ctx] ?? defaultShading ?? shading);
204
+ if (!hasDefaultRender || typeof window === 'undefined')
205
+ return;
206
+ let persistedState = {};
207
+ const rawPreferences = window.localStorage.getItem('viewer-preferences');
208
+ if (rawPreferences) {
209
+ try {
210
+ const parsed = JSON.parse(rawPreferences);
211
+ if (parsed &&
212
+ typeof parsed === 'object' &&
213
+ parsed.state &&
214
+ typeof parsed.state === 'object') {
215
+ persistedState = parsed.state;
216
+ }
217
+ }
218
+ catch { }
219
+ }
220
+ if (defaultTextures !== undefined && !('textures' in persistedState)) {
221
+ useViewer.getState().setTextures(defaultTextures);
222
+ }
223
+ if (defaultColorPreset && !('colorPreset' in persistedState)) {
224
+ useViewer.getState().setColorPreset(defaultColorPreset);
225
+ }
226
+ }, [defaultColorPreset, defaultShading, defaultTextures, hasDefaultRender, renderContext]);
227
+ // Coarse-pointer devices (phones/tablets) get a tighter DPR ceiling to keep
228
+ // fragment-shader cost down — saves another ~30% over 1.5x on high-DPI mobile.
229
+ // Desktops (fine pointer) keep the original 1.5 cap.
230
+ const maxDpr = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5;
231
+ return (_jsxs(Canvas, { camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, maxDpr], frameloop: "never", gl: ((props) => {
105
232
  const canvas = props.canvas;
106
233
  const cached = canvas ? WEBGPU_RENDERER_CACHE.get(canvas) : undefined;
107
234
  if (cached)
108
235
  return cached;
109
- // Surface the env we're about to ask WebGPU for — catches "no
110
- // navigator.gpu" / "adapter request failed" silently failing in
111
- // mobile WebViews where WebGPU is gated behind flags.
112
- const hasGpu = typeof navigator !== 'undefined' && 'gpu' in navigator;
113
- console.log('[viewer] Creating WebGPURenderer', {
114
- hasNavigatorGPU: hasGpu,
115
- ua: typeof navigator !== 'undefined' ? navigator.userAgent : 'n/a',
116
- });
117
236
  const promise = (async () => {
118
237
  try {
119
238
  const renderer = new THREE.WebGPURenderer(props);
120
239
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
121
- renderer.toneMappingExposure = 0.9;
240
+ renderer.toneMappingExposure = getSceneTheme(useViewer.getState().sceneTheme).toneMappingExposure;
122
241
  await renderer.init();
123
- console.log('[viewer] WebGPURenderer ready', {
124
- backend: renderer.backend?.constructor?.name,
125
- isWebGPU: renderer.isWebGPURenderer === true,
126
- });
242
+ installEmptyDrawGuard(renderer);
127
243
  return renderer;
128
244
  }
129
245
  catch (err) {
@@ -144,11 +260,18 @@ const Viewer = ({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager
144
260
  }, shadows: {
145
261
  type: THREE.PCFShadowMap,
146
262
  enabled: true,
147
- }, children: [_jsx(FrameLimiter, { fps: 50 }), _jsx(ViewerCamera, {}), _jsx(GPUDeviceWatcher, {}), _jsxs(ErrorBoundary, { fallback: null, scope: "viewer-scene", children: [_jsx(Lights, {}), useBvh ? (_jsx(Bvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {})), _jsx(LevelSystem, {}), _jsx(GuideSystem, {}), _jsx(ScanSystem, {}), _jsx(WallCutout, {}), _jsx(CeilingSystem, {}), _jsx(DoorAnimationSystem, {}), _jsx(WindowAnimationSystem, {}), _jsx(DoorSystem, {}), _jsx(FenceSystem, {}), _jsx(ItemSystem, {}), _jsx(RoofSystem, {}), _jsx(SlabSystem, {}), _jsx(StairSystem, {}), _jsx(WallSystem, {}), _jsx(WindowSystem, {}), _jsx(ZoneSystem, {}), _jsx(PostProcessing, { hoverStyles: hoverStyles }), _jsx(ItemLightSystem, {}), selectionManager === 'default' && _jsx(SelectionManager, {}), perf && _jsx(PerfMonitor, {}), children] })] }));
148
- };
263
+ }, children: [_jsx(FrameLimiter, { fps: 50 }), _jsx(ViewerCamera, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ToneMappingExposure, {}), _jsx(SceneReadyTracker, { onSceneReadyChange: onSceneReadyChange, sceneReadyKey: sceneReadyKey }), _jsxs(ErrorBoundary, { fallback: null, scope: "viewer-scene", children: [_jsx(Lights, {}), useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {})), _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RegisteredSystems, {}), _jsx(PostProcessing, { hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), children] })] }));
264
+ });
149
265
  const DebugRenderer = () => {
150
266
  useFrame(({ gl, scene, camera }) => {
267
+ const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
151
268
  gl.render(scene, camera);
269
+ if (PERF_OVERLAY_ENABLED) {
270
+ const queue = gl.backend?.device?.queue;
271
+ queue?.onSubmittedWorkDone?.().then(() => {
272
+ pushGpuSample(performance.now() - submittedAt);
273
+ });
274
+ }
152
275
  });
153
276
  return null;
154
277
  };
@@ -1,2 +1,2 @@
1
- export declare function Lights(): import("react/jsx-runtime").JSX.Element;
1
+ export declare function Lights(): import("react").JSX.Element;
2
2
  //# sourceMappingURL=lights.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AAMA,wBAAgB,MAAM,4CAoIrB"}
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AAqDA,wBAAgB,MAAM,gCA2NrB"}