@pascal-app/viewer 0.9.1 → 1.0.0-beta.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 (206) hide show
  1. package/README.md +20 -5
  2. package/dist/components/error-boundary.d.ts +6 -0
  3. package/dist/components/error-boundary.d.ts.map +1 -1
  4. package/dist/components/error-boundary.js +6 -0
  5. package/dist/components/renderers/node-renderer.d.ts +5 -1
  6. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/node-renderer.js +5 -2
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  10. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  11. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  12. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  13. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  14. package/dist/components/viewer/frame-limiter.js +59 -4
  15. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  16. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  17. package/dist/components/viewer/glb-interactive.js +394 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  19. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  20. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  22. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  23. package/dist/components/viewer/glb-replace-instances.js +61 -0
  24. package/dist/components/viewer/glb-scene.d.ts +54 -0
  25. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  26. package/dist/components/viewer/glb-scene.js +920 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
  30. package/dist/components/viewer/index.d.ts +19 -2
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +113 -12
  33. package/dist/components/viewer/lights.d.ts.map +1 -1
  34. package/dist/components/viewer/lights.js +36 -5
  35. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  36. package/dist/components/viewer/perf-monitor.js +17 -2
  37. package/dist/components/viewer/post-processing.d.ts +7 -1
  38. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  39. package/dist/components/viewer/post-processing.js +180 -24
  40. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  41. package/dist/components/viewer/registered-systems.js +6 -4
  42. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  43. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-bvh.js +3 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  46. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  47. package/dist/components/viewer/scene-bvh.test.js +13 -0
  48. package/dist/components/viewer/scene-environment.d.ts +2 -0
  49. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  50. package/dist/components/viewer/scene-environment.js +79 -0
  51. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  52. package/dist/components/viewer/selection-manager.js +23 -12
  53. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  54. package/dist/hooks/use-gltf-ktx2.js +2 -24
  55. package/dist/hooks/use-node-events.d.ts +1 -1
  56. package/dist/hooks/use-node-events.d.ts.map +1 -1
  57. package/dist/hooks/use-node-events.js +22 -14
  58. package/dist/index.d.ts +24 -10
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +29 -13
  61. package/dist/lib/backdrop.d.ts +33 -0
  62. package/dist/lib/backdrop.d.ts.map +1 -0
  63. package/dist/lib/backdrop.js +92 -0
  64. package/dist/lib/box-uv.d.ts +13 -0
  65. package/dist/lib/box-uv.d.ts.map +1 -0
  66. package/dist/lib/box-uv.js +32 -0
  67. package/dist/lib/csg-utils.d.ts +3 -9
  68. package/dist/lib/csg-utils.d.ts.map +1 -1
  69. package/dist/lib/csg-utils.js +65 -5
  70. package/dist/lib/csg-utils.test.d.ts +2 -0
  71. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  72. package/dist/lib/csg-utils.test.js +45 -0
  73. package/dist/lib/edge-style.d.ts +1 -0
  74. package/dist/lib/edge-style.d.ts.map +1 -1
  75. package/dist/lib/edge-style.js +26 -6
  76. package/dist/lib/hero-pose.d.ts +47 -0
  77. package/dist/lib/hero-pose.d.ts.map +1 -0
  78. package/dist/lib/hero-pose.js +227 -0
  79. package/dist/lib/ink-edges.d.ts +1 -1
  80. package/dist/lib/ink-edges.d.ts.map +1 -1
  81. package/dist/lib/ink-edges.js +13 -7
  82. package/dist/lib/ktx2-loader.d.ts +46 -0
  83. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  84. package/dist/lib/ktx2-loader.js +142 -0
  85. package/dist/lib/layers.d.ts +10 -0
  86. package/dist/lib/layers.d.ts.map +1 -1
  87. package/dist/lib/layers.js +10 -0
  88. package/dist/lib/materials.d.ts +16 -1
  89. package/dist/lib/materials.d.ts.map +1 -1
  90. package/dist/lib/materials.js +158 -34
  91. package/dist/lib/materials.test.d.ts +2 -0
  92. package/dist/lib/materials.test.d.ts.map +1 -0
  93. package/dist/lib/materials.test.js +25 -0
  94. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  95. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  96. package/dist/lib/merged-outline-node.test.js +22 -0
  97. package/dist/lib/scene-themes.d.ts +1 -0
  98. package/dist/lib/scene-themes.d.ts.map +1 -1
  99. package/dist/lib/scene-themes.js +25 -16
  100. package/dist/lib/shadow-only.d.ts +4 -0
  101. package/dist/lib/shadow-only.d.ts.map +1 -0
  102. package/dist/lib/shadow-only.js +36 -0
  103. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  104. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  105. package/dist/lib/snapshot-pipeline.js +254 -0
  106. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  107. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  108. package/dist/lib/terrain-perimeter-fill.js +75 -0
  109. package/dist/lib/texture-node-guard.d.ts +13 -0
  110. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  111. package/dist/lib/texture-node-guard.js +39 -0
  112. package/dist/lib/texture-reference.d.ts +31 -0
  113. package/dist/lib/texture-reference.d.ts.map +1 -0
  114. package/dist/lib/texture-reference.js +139 -0
  115. package/dist/lib/texture-reference.test.d.ts +2 -0
  116. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  117. package/dist/lib/texture-reference.test.js +86 -0
  118. package/dist/lib/tsl-compat.d.ts +12 -0
  119. package/dist/lib/tsl-compat.d.ts.map +1 -0
  120. package/dist/lib/tsl-compat.js +13 -0
  121. package/dist/store/use-viewer.d.ts +73 -18
  122. package/dist/store/use-viewer.d.ts.map +1 -1
  123. package/dist/store/use-viewer.js +191 -4
  124. package/dist/store/use-viewer.test.d.ts +2 -0
  125. package/dist/store/use-viewer.test.d.ts.map +1 -0
  126. package/dist/store/use-viewer.test.js +61 -0
  127. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  128. package/dist/systems/ceiling/ceiling-system.js +12 -5
  129. package/dist/systems/door/door-animation.test.d.ts +2 -0
  130. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  131. package/dist/systems/door/door-animation.test.js +51 -0
  132. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  133. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  134. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  135. package/dist/systems/door/door-system.d.ts +24 -0
  136. package/dist/systems/door/door-system.d.ts.map +1 -1
  137. package/dist/systems/door/door-system.js +494 -161
  138. package/dist/systems/fence/fence-system.d.ts +17 -0
  139. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  140. package/dist/systems/fence/fence-system.js +221 -53
  141. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  142. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  143. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  144. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  145. package/dist/systems/geometry/geometry-system.js +95 -26
  146. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  147. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  148. package/dist/systems/geometry/geometry-system.test.js +30 -0
  149. package/dist/systems/interactive/control-widget.d.ts +10 -0
  150. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  151. package/dist/systems/interactive/control-widget.js +35 -0
  152. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  153. package/dist/systems/interactive/interactive-system.js +55 -56
  154. package/dist/systems/item/item-system.d.ts.map +1 -1
  155. package/dist/systems/item/item-system.js +20 -6
  156. package/dist/systems/level/level-system.d.ts.map +1 -1
  157. package/dist/systems/level/level-system.js +34 -9
  158. package/dist/systems/level/level-utils.d.ts +0 -3
  159. package/dist/systems/level/level-utils.d.ts.map +1 -1
  160. package/dist/systems/level/level-utils.js +7 -45
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +29 -8
  163. package/dist/systems/roof/roof-system.d.ts +21 -8
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +1079 -221
  166. package/dist/systems/slab/slab-system.d.ts +7 -4
  167. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  168. package/dist/systems/slab/slab-system.js +31 -83
  169. package/dist/systems/slab/slab-system.test.js +54 -14
  170. package/dist/systems/stair/stair-system.js +7 -4
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  173. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  174. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  175. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  176. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  177. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  178. package/dist/systems/wall/wall-cutout.js +49 -17
  179. package/dist/systems/wall/wall-materials.d.ts +11 -7
  180. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-materials.js +244 -35
  182. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  183. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  184. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  185. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  186. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  187. package/dist/systems/wall/wall-support-extension.test.js +116 -0
  188. package/dist/systems/wall/wall-system.d.ts +4 -8
  189. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  190. package/dist/systems/wall/wall-system.js +397 -177
  191. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  192. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  194. package/dist/systems/window/window-animation-system.d.ts +12 -0
  195. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  196. package/dist/systems/window/window-animation-system.js +16 -5
  197. package/dist/systems/window/window-system.d.ts +7 -0
  198. package/dist/systems/window/window-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-system.js +240 -14
  200. package/package.json +4 -4
  201. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  202. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  203. package/dist/components/viewer/ground-occluder.js +0 -67
  204. package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
  205. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  206. package/dist/components/viewer/walkthrough-controls.js +0 -131
@@ -1 +1 @@
1
- {"version":3,"file":"scene-themes.d.ts","sourceRoot":"","sources":["../../src/lib/scene-themes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAEnD,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IAIZ,UAAU,EAAE,OAAO,GAAG,MAAM,CAAA;IAC5B,UAAU,EAAE,MAAM,CAAA;IAIlB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,MAAM,EAAE,KAAK,CAAC;QACZ,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAClC,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAC,CAAA;IACF,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,EA2LpC,CAAA;AAED,eAAO,MAAM,eAAe,UAAwC,CAAA;AAIpE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAEpD"}
1
+ {"version":3,"file":"scene-themes.d.ts","sourceRoot":"","sources":["../../src/lib/scene-themes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAEnD,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IAIZ,UAAU,EAAE,OAAO,GAAG,MAAM,CAAA;IAC5B,UAAU,EAAE,MAAM,CAAA;IAIlB,aAAa,CAAC,EAAE,MAAM,CAAA;IAItB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,MAAM,EAAE,KAAK,CAAC;QACZ,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAClC,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAC,CAAA;IACF,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,EAoMpC,CAAA;AAED,eAAO,MAAM,eAAe,UAAwC,CAAA;AAIpE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAEpD"}
@@ -3,13 +3,14 @@ export const SCENE_THEMES = [
3
3
  id: 'studio',
4
4
  name: 'Studio',
5
5
  appearance: 'light',
6
- background: '#ffffff',
7
- ground: '#f4f4f2',
6
+ background: '#fbfbfa',
7
+ backgroundSky: '#b6cfe7',
8
+ ground: '#e9e7e2',
8
9
  ambient: { color: '#ffffff', intensity: 0.15 },
9
- hemi: { sky: '#ffffff', ground: '#aaa49a', intensity: 0.6 },
10
+ hemi: { sky: '#ffffff', ground: '#aaa49a', intensity: 0.45 },
10
11
  lights: [
11
12
  { position: [10, 10, 10], color: '#ffffff', intensity: 4, castShadow: true },
12
- { position: [-10, 10, -10], color: '#ffffff', intensity: 0.75 },
13
+ { position: [-10, 10, -10], color: '#ffffff', intensity: 0.6 },
13
14
  ],
14
15
  toneMappingExposure: 0.9,
15
16
  clayTints: {
@@ -25,6 +26,7 @@ export const SCENE_THEMES = [
25
26
  name: 'Paper',
26
27
  appearance: 'light',
27
28
  background: '#ede9df',
29
+ backgroundSky: '#c0d2e4',
28
30
  ground: '#e7e1d3',
29
31
  ambient: { color: '#fff9eb', intensity: 0.55 },
30
32
  hemi: { sky: '#fff5d9', ground: '#c2b89c', intensity: 0.35 },
@@ -46,6 +48,7 @@ export const SCENE_THEMES = [
46
48
  name: 'Sunset',
47
49
  appearance: 'light',
48
50
  background: '#f6e8d4',
51
+ backgroundSky: '#b5bede',
49
52
  ground: '#ecd9bf',
50
53
  ambient: { color: '#ffd9a8', intensity: 0.45 },
51
54
  hemi: { sky: '#ffd9a8', ground: '#5b4634', intensity: 0.4 },
@@ -67,11 +70,12 @@ export const SCENE_THEMES = [
67
70
  name: 'Overcast',
68
71
  appearance: 'light',
69
72
  background: '#e6e7e6',
73
+ backgroundSky: '#c3ccd6',
70
74
  ground: '#dadcd9',
71
75
  ambient: { color: '#eef0ef', intensity: 1.1 },
72
76
  hemi: { sky: '#f4f5f3', ground: '#bcbfbb', intensity: 0.9 },
73
77
  lights: [{ position: [12, 28, 10], color: '#f4f5f3', intensity: 0.8, castShadow: true }],
74
- toneMappingExposure: 0.95,
78
+ toneMappingExposure: 0.9,
75
79
  clayTints: {
76
80
  wall: '#dedfdc',
77
81
  floor: '#cdcec9',
@@ -85,6 +89,7 @@ export const SCENE_THEMES = [
85
89
  name: 'Blueprint',
86
90
  appearance: 'light',
87
91
  background: '#dde6ef',
92
+ backgroundSky: '#a5c4e2',
88
93
  ground: '#c9d6e6',
89
94
  ambient: { color: '#cfdcec', intensity: 0.7 },
90
95
  hemi: { sky: '#dfeaf6', ground: '#5b6b80', intensity: 0.55 },
@@ -92,7 +97,7 @@ export const SCENE_THEMES = [
92
97
  { position: [16, 24, 12], color: '#e6efff', intensity: 1.8, castShadow: true },
93
98
  { position: [-12, 10, -8], color: '#9fb6d8', intensity: 0.4 },
94
99
  ],
95
- toneMappingExposure: 0.95,
100
+ toneMappingExposure: 0.9,
96
101
  clayTints: {
97
102
  wall: '#9fb6d2',
98
103
  floor: '#8ba2c2',
@@ -106,6 +111,7 @@ export const SCENE_THEMES = [
106
111
  name: 'Mediterranean',
107
112
  appearance: 'light',
108
113
  background: '#bdd6e8',
114
+ backgroundSky: '#8ab4d6',
109
115
  ground: '#ddd2bb',
110
116
  ambient: { color: '#d6e6f3', intensity: 0.5 },
111
117
  hemi: { sky: '#a8c8e2', ground: '#d8c9a4', intensity: 0.6 },
@@ -127,14 +133,15 @@ export const SCENE_THEMES = [
127
133
  name: 'Twilight',
128
134
  appearance: 'dark',
129
135
  background: '#3a3550',
130
- ground: '#4a4566',
131
- ambient: { color: '#a89cc8', intensity: 0.35 },
132
- hemi: { sky: '#d8a8c0', ground: '#1c1830', intensity: 0.5 },
136
+ backgroundSky: '#272338',
137
+ ground: '#67618a',
138
+ ambient: { color: '#a89cc8', intensity: 0.55 },
139
+ hemi: { sky: '#d8a8c0', ground: '#3a3450', intensity: 0.7 },
133
140
  lights: [
134
141
  { position: [-14, 22, -10], color: '#a4b6e8', intensity: 1.4, castShadow: true },
135
142
  { position: [14, 6, 8], color: '#ffb070', intensity: 0.9 },
136
143
  ],
137
- toneMappingExposure: 1.1,
144
+ toneMappingExposure: 0.9,
138
145
  clayTints: {
139
146
  wall: '#c5b9cf',
140
147
  floor: '#ad9fbb',
@@ -148,12 +155,13 @@ export const SCENE_THEMES = [
148
155
  name: 'Night',
149
156
  appearance: 'dark',
150
157
  background: '#1f2433',
151
- ground: '#2b3247',
152
- ambient: { color: '#a0b0ff', intensity: 0.07 },
153
- hemi: { sky: '#3a4666', ground: '#0e111c', intensity: 0.4 },
158
+ backgroundSky: '#12161f',
159
+ ground: '#4a5470',
160
+ ambient: { color: '#a0b0ff', intensity: 0.25 },
161
+ hemi: { sky: '#3a4666', ground: '#232a3d', intensity: 0.55 },
154
162
  lights: [
155
- { position: [10, 10, 10], color: '#e0e5ff', intensity: 0.8, castShadow: true },
156
- { position: [-10, 10, -10], color: '#8090ff', intensity: 0.2 },
163
+ { position: [10, 10, 10], color: '#e0e5ff', intensity: 1.2, castShadow: true },
164
+ { position: [-10, 10, -10], color: '#8090ff', intensity: 0.3 },
157
165
  ],
158
166
  toneMappingExposure: 0.9,
159
167
  clayTints: {
@@ -169,6 +177,7 @@ export const SCENE_THEMES = [
169
177
  name: 'Verdant',
170
178
  appearance: 'light',
171
179
  background: '#d6e4d2',
180
+ backgroundSky: '#aecde0',
172
181
  ground: '#c7d6b4',
173
182
  ambient: { color: '#e3efdd', intensity: 0.5 },
174
183
  hemi: { sky: '#cfe6cf', ground: '#8ea06f', intensity: 0.65 },
@@ -176,7 +185,7 @@ export const SCENE_THEMES = [
176
185
  { position: [16, 22, 12], color: '#fff6d8', intensity: 3, castShadow: true },
177
186
  { position: [-12, 10, -8], color: '#bfe0c2', intensity: 0.5 },
178
187
  ],
179
- toneMappingExposure: 0.95,
188
+ toneMappingExposure: 0.9,
180
189
  clayTints: {
181
190
  wall: '#eef0e6',
182
191
  floor: '#d8ddc6',
@@ -0,0 +1,4 @@
1
+ import type { Object3D } from 'three';
2
+ export declare function applyShadowOnly(root: Object3D): void;
3
+ export declare function clearShadowOnly(root: Object3D): void;
4
+ //# sourceMappingURL=shadow-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadow-only.d.ts","sourceRoot":"","sources":["../../src/lib/shadow-only.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAuBrC,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CASpD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAOpD"}
@@ -0,0 +1,36 @@
1
+ import { SCENE_LAYER, SHADOW_ONLY_LAYER } from './layers';
2
+ /**
3
+ * Shadow-caster-only hiding: removes an object (and its descendants) from the
4
+ * color passes while keeping it in the shadow map, so a hidden roof or level
5
+ * still shadows the interior and sun enters through windows correctly.
6
+ *
7
+ * Uses layer masks instead of `visible = false` for two reasons: `visible`
8
+ * cascades (and, critically, prunes the object from the shadow pass too),
9
+ * while layers are tested per-object against the rendering camera — the main
10
+ * camera never enables {@link SHADOW_ONLY_LAYER}, but every shadow-casting
11
+ * light's shadow camera does (see `lights.tsx`).
12
+ *
13
+ * The original `layers.mask` is stashed under a private Symbol so
14
+ * {@link clearShadowOnly} restores the exact prior state. Both calls are
15
+ * idempotent and cheap to reapply.
16
+ */
17
+ const ORIGINAL_LAYERS = Symbol('pascal:shadow-only:original-layers');
18
+ export function applyShadowOnly(root) {
19
+ root.traverse((obj) => {
20
+ const carrier = obj;
21
+ if (carrier[ORIGINAL_LAYERS] === undefined) {
22
+ carrier[ORIGINAL_LAYERS] = obj.layers.mask;
23
+ }
24
+ obj.layers.disable(SCENE_LAYER);
25
+ obj.layers.enable(SHADOW_ONLY_LAYER);
26
+ });
27
+ }
28
+ export function clearShadowOnly(root) {
29
+ root.traverse((obj) => {
30
+ const carrier = obj;
31
+ if (carrier[ORIGINAL_LAYERS] === undefined)
32
+ return;
33
+ obj.layers.mask = carrier[ORIGINAL_LAYERS];
34
+ delete carrier[ORIGINAL_LAYERS];
35
+ });
36
+ }
@@ -0,0 +1,42 @@
1
+ import { type Camera, type Scene } from 'three';
2
+ import { type WebGPURenderer } from 'three/webgpu';
3
+ import { type EdgeMode } from './edge-style';
4
+ export declare const THUMBNAIL_WIDTH = 1920;
5
+ export declare const THUMBNAIL_HEIGHT = 1080;
6
+ export type SnapshotCaptureMode = 'standard' | 'viewport' | 'area';
7
+ export type SnapshotCropRegion = {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export type SnapshotSize = {
14
+ w: number;
15
+ h: number;
16
+ };
17
+ export type SnapshotCaptureResult = {
18
+ blob: Blob;
19
+ outW: number;
20
+ outH: number;
21
+ };
22
+ export type SnapshotPipeline = {
23
+ applyEnvironment: ({ theme, transparent, grade, edges, camera, }: {
24
+ theme: string;
25
+ transparent: boolean;
26
+ grade: boolean;
27
+ edges: EdgeMode;
28
+ camera: Camera;
29
+ }) => void;
30
+ capture: ({ captureMode, cropRegion, standardSize, }: {
31
+ captureMode?: SnapshotCaptureMode;
32
+ cropRegion?: SnapshotCropRegion;
33
+ standardSize?: SnapshotSize;
34
+ }) => Promise<SnapshotCaptureResult>;
35
+ dispose: () => void;
36
+ };
37
+ export declare function createSnapshotPipeline({ renderer, scene, camera, }: {
38
+ renderer: WebGPURenderer;
39
+ scene: Scene;
40
+ camera: Camera;
41
+ }): Promise<SnapshotPipeline | null>;
42
+ //# sourceMappingURL=snapshot-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/snapshot-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAkB,KAAK,KAAK,EAAoB,MAAM,OAAO,CAAA;AAqBjF,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAGhF,OAAO,EAAE,KAAK,QAAQ,EAAqC,MAAM,cAAc,CAAA;AAK/E,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;AAElE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,IAAI,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,WAAW,EACX,KAAK,EACL,KAAK,EACL,MAAM,GACP,EAAE;QACD,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,EAAE,OAAO,CAAA;QACpB,KAAK,EAAE,OAAO,CAAA;QACd,KAAK,EAAE,QAAQ,CAAA;QACf,MAAM,EAAE,MAAM,CAAA;KACf,KAAK,IAAI,CAAA;IACV,OAAO,EAAE,CAAC,EACR,WAAW,EACX,UAAU,EACV,YAAY,GACb,EAAE;QACD,WAAW,CAAC,EAAE,mBAAmB,CAAA;QACjC,UAAU,CAAC,EAAE,kBAAkB,CAAA;QAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;KAC5B,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACpC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,QAAQ,EACR,KAAK,EACL,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,cAAc,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoSnC"}
@@ -0,0 +1,254 @@
1
+ import { Color, Matrix4, UnsignedByteType } from 'three';
2
+ import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
3
+ import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
4
+ import { fxaa } from 'three/examples/jsm/tsl/display/FXAANode.js';
5
+ import { convertToTexture, diffuseColor, float, mix, mrt, normalView, output, pass, sample, saturation, screenUV, smoothstep, uniform, vec3, vec4, } from 'three/tsl';
6
+ import { RenderPipeline, RenderTarget } from 'three/webgpu';
7
+ import { GRADE_PARAMS, SSGI_PARAMS } from '../components/viewer/post-processing';
8
+ import { backdropGradient, deepSkyColor, horizonHazeColor } from './backdrop';
9
+ import { edgeColorFor, edgeOpacityScaleFor } from './edge-style';
10
+ import { inkedEdges } from './ink-edges';
11
+ import { getSceneTheme } from './scene-themes';
12
+ import { packNormalToRGB, unpackRGBToNormal } from './tsl-compat';
13
+ export const THUMBNAIL_WIDTH = 1920;
14
+ export const THUMBNAIL_HEIGHT = 1080;
15
+ export async function createSnapshotPipeline({ renderer, scene, camera, }) {
16
+ try {
17
+ if (renderer.init)
18
+ await renderer.init();
19
+ // Backdrop compositing for scene snapshots (studio renders, project
20
+ // thumbnails): theme background + sky gradient, same world-ray math as the
21
+ // viewport backdrop in viewer's post-processing. Uniform-driven so the one
22
+ // cached pipeline serves both opaque and transparent (preset/item) captures.
23
+ const bgColorUniform = uniform(new Color('#ffffff'));
24
+ const bgSkyUniform = uniform(new Color('#ffffff'));
25
+ const bgSkyDeepUniform = uniform(new Color('#ffffff'));
26
+ const bgHazeUniform = uniform(new Color('#ffffff'));
27
+ const bgProjInvUniform = uniform(new Matrix4());
28
+ const bgCamWorldUniform = uniform(new Matrix4());
29
+ const bgMixUniform = uniform(1);
30
+ const gradeMixUniform = uniform(0);
31
+ const inkMixUniform = uniform(0);
32
+ const inkColorUniform = uniform(new Color('#1a1d24'));
33
+ const inkOpacityUniform = uniform(0.5);
34
+ const inkOpacityScaleUniform = uniform(1);
35
+ // pass() handles MRT internally for all material types, including custom
36
+ // shaders — unlike renderer.setMRT() which crashes on non-NodeMaterials.
37
+ // pass() also respects camera.layers, so caller-disabled objects are filtered.
38
+ const scenePass = pass(scene, camera);
39
+ scenePass.setMRT(mrt({
40
+ output,
41
+ diffuseColor,
42
+ normal: packNormalToRGB(normalView),
43
+ }));
44
+ const scenePassColor = scenePass.getTextureNode('output');
45
+ const scenePassDepth = scenePass.getTextureNode('depth');
46
+ const scenePassNormal = scenePass.getTextureNode('normal');
47
+ scenePass.getTexture('diffuseColor').type = UnsignedByteType;
48
+ scenePass.getTexture('normal').type = UnsignedByteType;
49
+ const sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)));
50
+ const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
51
+ giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
52
+ giPass.stepCount.value = SSGI_PARAMS.stepCount;
53
+ giPass.radius.value = SSGI_PARAMS.radius;
54
+ giPass.expFactor.value = SSGI_PARAMS.expFactor;
55
+ giPass.thickness.value = SSGI_PARAMS.thickness;
56
+ giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting;
57
+ giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity;
58
+ giPass.giIntensity.value = SSGI_PARAMS.giIntensity;
59
+ giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
60
+ giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
61
+ giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
62
+ // r185: SSGI's AO lives in its own single-channel texture (getAONode)
63
+ // rather than the alpha of one packed rgba texture.
64
+ const aoTexture = giPass.getAONode();
65
+ const aoAsRgb = vec4(aoTexture.r, aoTexture.r, aoTexture.r, float(1));
66
+ const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
67
+ denoisePass.index.value = 0;
68
+ denoisePass.radius.value = 4;
69
+ // Same far-field AO fade as the viewport pipeline — without it the
70
+ // horizon picks up a visible AO line in captures.
71
+ const aoFarFade = smoothstep(float(0.9994), float(0.9998), scenePassDepth.sample(screenUV).r);
72
+ const ao = mix(denoisePass.r, float(1), aoFarFade);
73
+ const aoRgb = scenePassColor.rgb.mul(ao);
74
+ // Ink edges, mirroring the viewport pipeline (AO → ink → grade) so
75
+ // captures carry the same soft/strong edge look the canvas shows.
76
+ // Uniform-gated like grade/backdrop: one cached pipeline serves all modes.
77
+ // Radius scales with render height: a supersampled capture (4K → 1080p)
78
+ // would otherwise halve the apparent line weight vs the viewport's 1px.
79
+ const inkRadius = Math.max(1, Math.round(renderer.domElement.height / 1080));
80
+ const inkedRgb = inkedEdges({
81
+ sceneRgb: aoRgb,
82
+ depthTex: scenePassDepth,
83
+ normalTex: scenePassNormal,
84
+ inkColor: inkColorUniform,
85
+ radius: inkRadius,
86
+ opacity: float(inkOpacityUniform).mul(inkOpacityScaleUniform),
87
+ });
88
+ const ungradedSceneRgb = mix(aoRgb, inkedRgb, inkMixUniform);
89
+ const gradeRgb = (rgb) => saturation(rgb.div(0.18).pow(vec3(GRADE_PARAMS.contrast)).mul(0.18), GRADE_PARAMS.saturation);
90
+ const sceneRgb = mix(ungradedSceneRgb, gradeRgb(ungradedSceneRgb), gradeMixUniform);
91
+ // Per-pixel world ray from the capture camera → sky gradient above the
92
+ // horizon (dir.y = 0), flat background below — mirrors the viewport
93
+ // backdrop. bgMix 0 bypasses it and keeps the capture transparent.
94
+ const ndc = vec4(screenUV.x.mul(2).sub(1), float(1).sub(screenUV.y).mul(2).sub(1), 1, 1);
95
+ const viewRay = bgProjInvUniform.mul(ndc);
96
+ const worldDir = bgCamWorldUniform.mul(vec4(viewRay.xyz, 0)).xyz.normalize();
97
+ const ungradedBgGradient = backdropGradient({
98
+ dirY: worldDir.y,
99
+ background: bgColorUniform,
100
+ haze: bgHazeUniform,
101
+ sky: bgSkyUniform,
102
+ skyDeep: bgSkyDeepUniform,
103
+ });
104
+ const bgGradient = mix(ungradedBgGradient, gradeRgb(ungradedBgGradient), gradeMixUniform);
105
+ const alpha = scenePassColor.a;
106
+ const finalOutput = vec4(mix(sceneRgb, mix(bgGradient, sceneRgb, alpha), bgMixUniform), mix(alpha, float(1), bgMixUniform));
107
+ // FXAA requires a texture node as input; convertToTexture renders finalOutput
108
+ // into an intermediate RT so FXAA can sample it with neighbour UV offsets.
109
+ const aaOutput = fxaa(convertToTexture(finalOutput));
110
+ const pipeline = new RenderPipeline(renderer);
111
+ pipeline.outputNode = aaOutput;
112
+ // Dedicated render target — pipeline outputs here instead of the canvas,
113
+ // so R3F's main render loop can never overwrite our capture.
114
+ const { width, height } = renderer.domElement;
115
+ const renderTarget = new RenderTarget(width, height, { depthBuffer: true });
116
+ return {
117
+ applyEnvironment: ({ theme, transparent, grade, edges, camera: captureCamera }) => {
118
+ const sceneTheme = getSceneTheme(theme);
119
+ inkMixUniform.value = edges === 'off' ? 0 : 1;
120
+ inkOpacityUniform.value = edges === 'strong' ? 1 : 0.5;
121
+ inkColorUniform.value.set(edgeColorFor(sceneTheme.background));
122
+ inkOpacityScaleUniform.value = edgeOpacityScaleFor(sceneTheme.background);
123
+ bgColorUniform.value.set(sceneTheme.background);
124
+ bgSkyUniform.value.set(sceneTheme.backgroundSky ?? sceneTheme.background);
125
+ bgSkyDeepUniform.value.set(deepSkyColor(sceneTheme.backgroundSky ?? sceneTheme.background));
126
+ bgHazeUniform.value.set(horizonHazeColor(sceneTheme.backgroundSky ?? sceneTheme.background, sceneTheme.appearance));
127
+ bgMixUniform.value = transparent ? 0 : 1;
128
+ gradeMixUniform.value = grade ? 1 : 0;
129
+ // The capture camera never joins the scene graph, so its matrixWorld
130
+ // is only refreshed by the render itself — too late for the backdrop
131
+ // uniforms below.
132
+ captureCamera.updateMatrixWorld();
133
+ bgProjInvUniform.value.copy(captureCamera.projectionMatrixInverse);
134
+ bgCamWorldUniform.value.copy(captureCamera.matrixWorld);
135
+ },
136
+ capture: async ({ captureMode, cropRegion, standardSize }) => {
137
+ const standardW = standardSize?.w ?? THUMBNAIL_WIDTH;
138
+ const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT;
139
+ const { width: captureWidth, height: captureHeight } = renderer.domElement;
140
+ // Resize RT if the canvas dimensions changed
141
+ if (renderTarget.width !== captureWidth || renderTarget.height !== captureHeight) {
142
+ renderTarget.setSize(captureWidth, captureHeight);
143
+ }
144
+ try {
145
+ ;
146
+ renderer.setClearAlpha(0);
147
+ renderer.setRenderTarget(renderTarget);
148
+ pipeline.render();
149
+ }
150
+ finally {
151
+ renderer.setRenderTarget(null);
152
+ }
153
+ // Let callers restore visibility and other capture policy immediately
154
+ // after the render, before the asynchronous GPU readback begins.
155
+ await Promise.resolve();
156
+ // Read pixels from the RT asynchronously.
157
+ // WebGPU copyTextureToBuffer aligns each row to 256 bytes, so we must
158
+ // depad the rows before constructing ImageData.
159
+ const pixels = (await renderer.readRenderTargetPixelsAsync(renderTarget, 0, 0, captureWidth, captureHeight));
160
+ const actualBytesPerRow = captureWidth * 4;
161
+ const tightTotal = actualBytesPerRow * captureHeight;
162
+ const paddedBytesPerRow = Math.ceil(actualBytesPerRow / 256) * 256;
163
+ // Two readback shapes to handle:
164
+ // - WebGPU (`copyTextureToBuffer`): top-down + 256-byte row padding
165
+ // when width*4 isn't already a multiple of 256.
166
+ // - WebGL2 fallback (iOS Chrome, etc.): tightly-packed but bottom-up
167
+ // (OpenGL framebuffer convention).
168
+ // `isWebGPURenderer` lies — it stays true even when the renderer
169
+ // falls back to the WebGL backend. Inspect the actual backend
170
+ // instead (presence of a GPU device, or backend constructor name).
171
+ const backend = renderer.backend;
172
+ const isWebGPU = !!backend?.device ||
173
+ backend?.isWebGPUBackend === true ||
174
+ backend?.constructor?.name === 'WebGPUBackend';
175
+ let tightPixels;
176
+ if (isWebGPU) {
177
+ // WebGPU: depad rows if needed; orientation is already top-down.
178
+ if (paddedBytesPerRow === actualBytesPerRow) {
179
+ tightPixels = new Uint8ClampedArray(pixels.buffer, pixels.byteOffset, Math.min(pixels.byteLength, tightTotal));
180
+ }
181
+ else {
182
+ tightPixels = new Uint8ClampedArray(tightTotal);
183
+ for (let row = 0; row < captureHeight; row++) {
184
+ tightPixels.set(pixels.subarray(row * paddedBytesPerRow, row * paddedBytesPerRow + actualBytesPerRow), row * actualBytesPerRow);
185
+ }
186
+ }
187
+ }
188
+ else {
189
+ // WebGL2: tight buffer in bottom-up order — flip rows.
190
+ tightPixels = new Uint8ClampedArray(tightTotal);
191
+ for (let row = 0; row < captureHeight; row++) {
192
+ const srcStart = (captureHeight - 1 - row) * actualBytesPerRow;
193
+ tightPixels.set(pixels.subarray(srcStart, srcStart + actualBytesPerRow), row * actualBytesPerRow);
194
+ }
195
+ }
196
+ const imageData = new ImageData(tightPixels, captureWidth, captureHeight);
197
+ const srcCanvas = new OffscreenCanvas(captureWidth, captureHeight);
198
+ srcCanvas.getContext('2d').putImageData(imageData, 0, 0);
199
+ let outW;
200
+ let outH;
201
+ let blob;
202
+ if (captureMode === 'viewport') {
203
+ outW = captureWidth;
204
+ outH = captureHeight;
205
+ const offscreen = new OffscreenCanvas(outW, outH);
206
+ offscreen.getContext('2d').drawImage(srcCanvas, 0, 0);
207
+ blob = await offscreen.convertToBlob({ type: 'image/png' });
208
+ }
209
+ else if (captureMode === 'area' && cropRegion) {
210
+ const sx = Math.round(cropRegion.x * captureWidth);
211
+ const sy = Math.round(cropRegion.y * captureHeight);
212
+ outW = Math.round(cropRegion.width * captureWidth);
213
+ outH = Math.round(cropRegion.height * captureHeight);
214
+ const offscreen = new OffscreenCanvas(outW, outH);
215
+ offscreen.getContext('2d').drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH);
216
+ blob = await offscreen.convertToBlob({ type: 'image/png' });
217
+ }
218
+ else {
219
+ // Standard: center-crop to the requested aspect (default 1920×1080)
220
+ const srcAspect = captureWidth / captureHeight;
221
+ const dstAspect = standardW / standardH;
222
+ let sx = 0;
223
+ let sy = 0;
224
+ let sWidth = captureWidth;
225
+ let sHeight = captureHeight;
226
+ if (srcAspect > dstAspect) {
227
+ sWidth = Math.round(captureHeight * dstAspect);
228
+ sx = Math.round((captureWidth - sWidth) / 2);
229
+ }
230
+ else if (srcAspect < dstAspect) {
231
+ sHeight = Math.round(captureWidth / dstAspect);
232
+ sy = Math.round((captureHeight - sHeight) / 2);
233
+ }
234
+ outW = standardW;
235
+ outH = standardH;
236
+ const offscreen = new OffscreenCanvas(outW, outH);
237
+ offscreen
238
+ .getContext('2d')
239
+ .drawImage(srcCanvas, sx, sy, sWidth, sHeight, 0, 0, outW, outH);
240
+ blob = await offscreen.convertToBlob({ type: 'image/png' });
241
+ }
242
+ return { blob, outW, outH };
243
+ },
244
+ dispose: () => {
245
+ pipeline.dispose();
246
+ renderTarget.dispose();
247
+ },
248
+ };
249
+ }
250
+ catch (error) {
251
+ console.error('[thumbnail] Failed to build post-processing pipeline, will use fallback render.', error);
252
+ return null;
253
+ }
254
+ }
@@ -0,0 +1,7 @@
1
+ import { BufferGeometry } from 'three';
2
+ export type TerrainPerimeterPoint = {
3
+ x: number;
4
+ z: number;
5
+ };
6
+ export declare function buildTerrainPerimeterFillGeometry(points: readonly TerrainPerimeterPoint[], bottomY: readonly number[], topY: number, epsilon?: number): BufferGeometry | null;
7
+ //# sourceMappingURL=terrain-perimeter-fill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terrain-perimeter-fill.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-perimeter-fill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA+C,MAAM,OAAO,CAAA;AAGnF,MAAM,MAAM,qBAAqB,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,SAAS,qBAAqB,EAAE,EACxC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAO,GACb,cAAc,GAAG,IAAI,CA2EvB"}
@@ -0,0 +1,75 @@
1
+ import { BufferGeometry, Float32BufferAttribute, ShapeUtils, Vector2 } from 'three';
2
+ import { ensureRenderableGeometryAttributes } from './csg-utils';
3
+ export function buildTerrainPerimeterFillGeometry(points, bottomY, topY, epsilon = 1e-6) {
4
+ if (points.length < 3 || bottomY.length !== points.length)
5
+ return null;
6
+ if (bottomY.every((y) => y >= topY - epsilon))
7
+ return null;
8
+ let signedArea = 0;
9
+ for (let index = 0; index < points.length; index += 1) {
10
+ const a = points[index];
11
+ const b = points[(index + 1) % points.length];
12
+ signedArea += a.x * b.z - b.x * a.z;
13
+ }
14
+ const counterClockwise = signedArea > 0;
15
+ const positions = [];
16
+ const push = (point, y) => {
17
+ positions.push(point.x, y, point.z);
18
+ };
19
+ for (let index = 0; index < points.length; index += 1) {
20
+ const next = (index + 1) % points.length;
21
+ const a = points[index];
22
+ const b = points[next];
23
+ const ay = bottomY[index];
24
+ const by = bottomY[next];
25
+ if (ay >= topY - epsilon && by >= topY - epsilon)
26
+ continue;
27
+ if (counterClockwise) {
28
+ push(a, topY);
29
+ push(b, by);
30
+ push(a, ay);
31
+ push(a, topY);
32
+ push(b, topY);
33
+ push(b, by);
34
+ }
35
+ else {
36
+ push(a, topY);
37
+ push(a, ay);
38
+ push(b, by);
39
+ push(a, topY);
40
+ push(b, by);
41
+ push(b, topY);
42
+ }
43
+ }
44
+ const faces = ShapeUtils.triangulateShape(points.map((point) => new Vector2(point.x, point.z)), []);
45
+ for (const face of faces) {
46
+ const [ia, ib, ic] = face;
47
+ if (ia == null || ib == null || ic == null)
48
+ continue;
49
+ if (bottomY[ia] >= topY - epsilon &&
50
+ bottomY[ib] >= topY - epsilon &&
51
+ bottomY[ic] >= topY - epsilon) {
52
+ continue;
53
+ }
54
+ const a = points[ia];
55
+ const b = points[ib];
56
+ const c = points[ic];
57
+ const cross = (b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x);
58
+ push(a, bottomY[ia]);
59
+ if (cross >= 0) {
60
+ push(b, bottomY[ib]);
61
+ push(c, bottomY[ic]);
62
+ }
63
+ else {
64
+ push(c, bottomY[ic]);
65
+ push(b, bottomY[ib]);
66
+ }
67
+ }
68
+ if (positions.length === 0)
69
+ return null;
70
+ const geometry = new BufferGeometry();
71
+ geometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
72
+ geometry.computeVertexNormals();
73
+ ensureRenderableGeometryAttributes(geometry);
74
+ return geometry;
75
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * three's node system pulls texture uniforms from materials each frame via
3
+ * reference nodes, and several override-material passes (shadow, depth/normal
4
+ * prepasses) copy per-object texture slots onto shared materials whose cached
5
+ * per-mesh node graphs can disagree about a slot's presence. When they do,
6
+ * `TextureNode.update` dereferences a null texture and the exception kills the
7
+ * whole render pass — the scene goes black. Substitute a 1×1 black fallback
8
+ * instead (skipping is not enough: the null would still reach the backend's
9
+ * texture-binding WeakMap). The slot renders black for a frame and recovers
10
+ * as soon as the reference pulls a real value again.
11
+ */
12
+ export declare function installTextureNodeNullGuard(): void;
13
+ //# sourceMappingURL=texture-node-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"texture-node-guard.d.ts","sourceRoot":"","sources":["../../src/lib/texture-node-guard.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAiBlD"}
@@ -0,0 +1,39 @@
1
+ import { DataTexture, TextureNode } from 'three/webgpu';
2
+ let installed = false;
3
+ let reported = 0;
4
+ let fallbackTexture = null;
5
+ function getFallbackTexture() {
6
+ if (!fallbackTexture) {
7
+ fallbackTexture = new DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);
8
+ fallbackTexture.needsUpdate = true;
9
+ }
10
+ return fallbackTexture;
11
+ }
12
+ /**
13
+ * three's node system pulls texture uniforms from materials each frame via
14
+ * reference nodes, and several override-material passes (shadow, depth/normal
15
+ * prepasses) copy per-object texture slots onto shared materials whose cached
16
+ * per-mesh node graphs can disagree about a slot's presence. When they do,
17
+ * `TextureNode.update` dereferences a null texture and the exception kills the
18
+ * whole render pass — the scene goes black. Substitute a 1×1 black fallback
19
+ * instead (skipping is not enough: the null would still reach the backend's
20
+ * texture-binding WeakMap). The slot renders black for a frame and recovers
21
+ * as soon as the reference pulls a real value again.
22
+ */
23
+ export function installTextureNodeNullGuard() {
24
+ if (installed)
25
+ return;
26
+ installed = true;
27
+ const prototype = TextureNode.prototype;
28
+ const originalUpdate = prototype.update;
29
+ prototype.update = function update() {
30
+ if (this.value == null) {
31
+ if (reported < 5) {
32
+ reported += 1;
33
+ console.warn(`[viewer] TextureNode ${this.uuid} has no texture — using fallback`);
34
+ }
35
+ this.value = getFallbackTexture();
36
+ }
37
+ originalUpdate.call(this);
38
+ };
39
+ }