@pascal-app/viewer 1.0.0-beta.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/capture/asset-url.d.ts +2 -0
  4. package/dist/capture/asset-url.d.ts.map +1 -0
  5. package/dist/capture/asset-url.js +13 -0
  6. package/dist/capture/capture-runtime.d.ts +43 -0
  7. package/dist/capture/capture-runtime.d.ts.map +1 -0
  8. package/dist/capture/capture-runtime.js +228 -0
  9. package/dist/capture/frame.d.ts +4 -0
  10. package/dist/capture/frame.d.ts.map +1 -0
  11. package/dist/capture/frame.js +21 -0
  12. package/dist/capture/index.d.ts +12 -0
  13. package/dist/capture/index.d.ts.map +1 -0
  14. package/dist/capture/index.js +11 -0
  15. package/dist/capture/layer-visibility.d.ts +5 -0
  16. package/dist/capture/layer-visibility.d.ts.map +1 -0
  17. package/dist/capture/layer-visibility.js +11 -0
  18. package/dist/capture/layers/clay-matcap.d.ts +3 -0
  19. package/dist/capture/layers/clay-matcap.d.ts.map +1 -0
  20. package/dist/capture/layers/clay-matcap.js +26 -0
  21. package/dist/capture/layers/device-motion-layer.d.ts +7 -0
  22. package/dist/capture/layers/device-motion-layer.d.ts.map +1 -0
  23. package/dist/capture/layers/device-motion-layer.js +78 -0
  24. package/dist/capture/layers/point-cloud-layer.d.ts +15 -0
  25. package/dist/capture/layers/point-cloud-layer.d.ts.map +1 -0
  26. package/dist/capture/layers/point-cloud-layer.js +106 -0
  27. package/dist/capture/layers/room-model-layer.d.ts +9 -0
  28. package/dist/capture/layers/room-model-layer.d.ts.map +1 -0
  29. package/dist/capture/layers/room-model-layer.js +71 -0
  30. package/dist/capture/layers/surface-mesh-data.d.ts +9 -0
  31. package/dist/capture/layers/surface-mesh-data.d.ts.map +1 -0
  32. package/dist/capture/layers/surface-mesh-data.js +74 -0
  33. package/dist/capture/layers/surface-mesh-layer.d.ts +7 -0
  34. package/dist/capture/layers/surface-mesh-layer.d.ts.map +1 -0
  35. package/dist/capture/layers/surface-mesh-layer.js +16 -0
  36. package/dist/capture/preview.d.ts +3 -0
  37. package/dist/capture/preview.d.ts.map +1 -0
  38. package/dist/capture/preview.js +2 -0
  39. package/dist/capture/source-state.d.ts +22 -0
  40. package/dist/capture/source-state.d.ts.map +1 -0
  41. package/dist/capture/source-state.js +182 -0
  42. package/dist/capture/stream-rendering.d.ts +14 -0
  43. package/dist/capture/stream-rendering.d.ts.map +1 -0
  44. package/dist/capture/stream-rendering.js +69 -0
  45. package/dist/capture/trajectory.d.ts +20 -0
  46. package/dist/capture/trajectory.d.ts.map +1 -0
  47. package/dist/capture/trajectory.js +77 -0
  48. package/dist/components/viewer/batched-mesh-spike.d.ts +14 -0
  49. package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
  50. package/dist/components/viewer/batched-mesh-spike.js +64 -0
  51. package/dist/components/viewer/frame-limiter.d.ts +11 -0
  52. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  53. package/dist/components/viewer/frame-limiter.js +59 -18
  54. package/dist/components/viewer/index.d.ts +15 -0
  55. package/dist/components/viewer/index.d.ts.map +1 -1
  56. package/dist/components/viewer/index.js +74 -82
  57. package/dist/components/viewer/lights.d.ts.map +1 -1
  58. package/dist/components/viewer/lights.js +123 -89
  59. package/dist/components/viewer/perf-monitor.d.ts +6 -1
  60. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  61. package/dist/components/viewer/perf-monitor.js +306 -115
  62. package/dist/components/viewer/perf-panel.d.ts +3 -0
  63. package/dist/components/viewer/perf-panel.d.ts.map +1 -0
  64. package/dist/components/viewer/perf-panel.js +178 -0
  65. package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
  66. package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
  67. package/dist/components/viewer/pointer-raycast-layers.js +28 -0
  68. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  69. package/dist/components/viewer/post-processing.js +124 -83
  70. package/dist/components/viewer/registered-systems.d.ts +14 -5
  71. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  72. package/dist/components/viewer/registered-systems.js +22 -10
  73. package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
  74. package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
  75. package/dist/components/viewer/scene-atmosphere.js +89 -0
  76. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  77. package/dist/components/viewer/scene-environment.js +19 -5
  78. package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
  79. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
  80. package/dist/components/viewer/scene-ground-replacement.js +35 -0
  81. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  82. package/dist/components/viewer/selection-manager.js +7 -2
  83. package/dist/components/viewer/unsupported-gpu-fallback.d.ts +2 -0
  84. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -0
  85. package/dist/components/viewer/unsupported-gpu-fallback.js +4 -0
  86. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  87. package/dist/components/viewer/viewer-camera.js +4 -1
  88. package/dist/components/viewer/viewer-presentations.d.ts +67 -0
  89. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -0
  90. package/dist/components/viewer/viewer-presentations.js +120 -0
  91. package/dist/hooks/use-library-materials-version.d.ts +7 -0
  92. package/dist/hooks/use-library-materials-version.d.ts.map +1 -0
  93. package/dist/hooks/use-library-materials-version.js +10 -0
  94. package/dist/hooks/use-node-events.d.ts.map +1 -1
  95. package/dist/hooks/use-node-events.js +1 -0
  96. package/dist/index.d.ts +20 -8
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +19 -7
  99. package/dist/lib/csg-utils.d.ts +1 -0
  100. package/dist/lib/csg-utils.d.ts.map +1 -1
  101. package/dist/lib/csg-utils.js +6 -1
  102. package/dist/lib/dispose-object3d.d.ts +4 -0
  103. package/dist/lib/dispose-object3d.d.ts.map +1 -0
  104. package/dist/lib/dispose-object3d.js +27 -0
  105. package/dist/lib/geometry-groups.d.ts +17 -0
  106. package/dist/lib/geometry-groups.d.ts.map +1 -0
  107. package/dist/lib/geometry-groups.js +53 -0
  108. package/dist/lib/gpu-perf.d.ts +0 -2
  109. package/dist/lib/gpu-perf.d.ts.map +1 -1
  110. package/dist/lib/gpu-perf.js +7 -21
  111. package/dist/lib/isolation.d.ts +8 -8
  112. package/dist/lib/isolation.d.ts.map +1 -1
  113. package/dist/lib/isolation.js +18 -30
  114. package/dist/lib/layer-pass.d.ts +28 -0
  115. package/dist/lib/layer-pass.d.ts.map +1 -0
  116. package/dist/lib/layer-pass.js +254 -0
  117. package/dist/lib/layers.d.ts +16 -0
  118. package/dist/lib/layers.d.ts.map +1 -1
  119. package/dist/lib/layers.js +18 -0
  120. package/dist/lib/materials.d.ts +12 -0
  121. package/dist/lib/materials.d.ts.map +1 -1
  122. package/dist/lib/materials.js +51 -17
  123. package/dist/lib/merged-outline-node.d.ts +17 -8
  124. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  125. package/dist/lib/merged-outline-node.js +194 -52
  126. package/dist/lib/perf-actions.d.ts +55 -0
  127. package/dist/lib/perf-actions.d.ts.map +1 -0
  128. package/dist/lib/perf-actions.js +211 -0
  129. package/dist/lib/perf-observers.d.ts +2 -0
  130. package/dist/lib/perf-observers.d.ts.map +1 -0
  131. package/dist/lib/perf-observers.js +17 -0
  132. package/dist/lib/perf-panel-store.d.ts +71 -0
  133. package/dist/lib/perf-panel-store.d.ts.map +1 -0
  134. package/dist/lib/perf-panel-store.js +29 -0
  135. package/dist/lib/perf-tracks.d.ts +43 -0
  136. package/dist/lib/perf-tracks.d.ts.map +1 -0
  137. package/dist/lib/perf-tracks.js +113 -0
  138. package/dist/lib/pointer-events.d.ts +7 -0
  139. package/dist/lib/pointer-events.d.ts.map +1 -0
  140. package/dist/lib/pointer-events.js +680 -0
  141. package/dist/lib/post-processing-resources.d.ts +12 -0
  142. package/dist/lib/post-processing-resources.d.ts.map +1 -0
  143. package/dist/lib/post-processing-resources.js +19 -0
  144. package/dist/lib/renderer-capability.d.ts +56 -0
  145. package/dist/lib/renderer-capability.d.ts.map +1 -0
  146. package/dist/lib/renderer-capability.js +116 -0
  147. package/dist/lib/scene-visibility.d.ts +15 -0
  148. package/dist/lib/scene-visibility.d.ts.map +1 -0
  149. package/dist/lib/scene-visibility.js +69 -0
  150. package/dist/lib/shadow-only.d.ts +16 -0
  151. package/dist/lib/shadow-only.d.ts.map +1 -1
  152. package/dist/lib/shadow-only.js +8 -17
  153. package/dist/lib/snapshot-pipeline.d.ts +9 -0
  154. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  155. package/dist/lib/snapshot-pipeline.js +33 -9
  156. package/dist/store/use-viewer.d.ts +7 -6
  157. package/dist/store/use-viewer.d.ts.map +1 -1
  158. package/dist/systems/ceiling/ceiling-system.js +1 -1
  159. package/dist/systems/door/door-animation-system.d.ts.map +1 -1
  160. package/dist/systems/door/door-animation-system.js +6 -9
  161. package/dist/systems/door/door-system.d.ts.map +1 -1
  162. package/dist/systems/door/door-system.js +13 -2
  163. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  164. package/dist/systems/floor-elevation/floor-elevation-system.js +9 -0
  165. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  166. package/dist/systems/geometry/geometry-system.js +5 -28
  167. package/dist/systems/level/level-system.d.ts.map +1 -1
  168. package/dist/systems/level/level-system.js +8 -2
  169. package/dist/systems/level/level-utils.d.ts +9 -0
  170. package/dist/systems/level/level-utils.d.ts.map +1 -1
  171. package/dist/systems/level/level-utils.js +14 -0
  172. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
  173. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
  174. package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
  175. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  176. package/dist/systems/roof/roof-system.js +1196 -58
  177. package/dist/systems/scan/scan-system.js +6 -5
  178. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  179. package/dist/systems/slab/slab-system.js +1 -2
  180. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  181. package/dist/systems/stair/stair-system.js +9 -8
  182. package/dist/systems/surface-hole-geometry.d.ts +1 -1
  183. package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
  184. package/dist/systems/surface-hole-geometry.js +1 -1
  185. package/dist/systems/wall/level-miter-cache.d.ts +10 -0
  186. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -0
  187. package/dist/systems/wall/level-miter-cache.js +40 -0
  188. package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
  189. package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
  190. package/dist/systems/wall/wall-build-lifecycle.js +74 -0
  191. package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
  192. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-cutout-cache.js +304 -0
  194. package/dist/systems/wall/wall-cutout.d.ts +24 -1
  195. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  196. package/dist/systems/wall/wall-cutout.js +33 -140
  197. package/dist/systems/wall/wall-material-variant.d.ts +31 -0
  198. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
  199. package/dist/systems/wall/wall-material-variant.js +10 -0
  200. package/dist/systems/wall/wall-materials.d.ts +6 -0
  201. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  202. package/dist/systems/wall/wall-materials.js +29 -13
  203. package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
  204. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
  205. package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
  206. package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
  207. package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
  208. package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
  209. package/dist/systems/wall/wall-system.d.ts +13 -2
  210. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  211. package/dist/systems/wall/wall-system.js +413 -148
  212. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  213. package/dist/systems/window/window-animation-system.js +9 -8
  214. package/dist/systems/window/window-system.d.ts +1 -0
  215. package/dist/systems/window/window-system.d.ts.map +1 -1
  216. package/dist/systems/window/window-system.js +16 -2
  217. package/package.json +18 -3
  218. package/dist/components/viewer/scene-bvh.test.d.ts +0 -2
  219. package/dist/components/viewer/scene-bvh.test.d.ts.map +0 -1
  220. package/dist/components/viewer/scene-bvh.test.js +0 -13
  221. package/dist/lib/csg-utils.test.d.ts +0 -2
  222. package/dist/lib/csg-utils.test.d.ts.map +0 -1
  223. package/dist/lib/csg-utils.test.js +0 -45
  224. package/dist/lib/materials.test.d.ts +0 -2
  225. package/dist/lib/materials.test.d.ts.map +0 -1
  226. package/dist/lib/materials.test.js +0 -25
  227. package/dist/lib/merged-outline-node.test.d.ts +0 -2
  228. package/dist/lib/merged-outline-node.test.d.ts.map +0 -1
  229. package/dist/lib/merged-outline-node.test.js +0 -22
  230. package/dist/lib/polygon-union.d.ts +0 -4
  231. package/dist/lib/polygon-union.d.ts.map +0 -1
  232. package/dist/lib/polygon-union.js +0 -289
  233. package/dist/lib/polygon-union.test.d.ts +0 -2
  234. package/dist/lib/polygon-union.test.d.ts.map +0 -1
  235. package/dist/lib/polygon-union.test.js +0 -105
  236. package/dist/lib/texture-reference.test.d.ts +0 -2
  237. package/dist/lib/texture-reference.test.d.ts.map +0 -1
  238. package/dist/lib/texture-reference.test.js +0 -86
  239. package/dist/store/use-viewer.test.d.ts +0 -2
  240. package/dist/store/use-viewer.test.d.ts.map +0 -1
  241. package/dist/store/use-viewer.test.js +0 -61
  242. package/dist/systems/door/door-animation.test.d.ts +0 -2
  243. package/dist/systems/door/door-animation.test.d.ts.map +0 -1
  244. package/dist/systems/door/door-animation.test.js +0 -51
  245. package/dist/systems/door/door-floor-alignment.test.d.ts +0 -2
  246. package/dist/systems/door/door-floor-alignment.test.d.ts.map +0 -1
  247. package/dist/systems/door/door-floor-alignment.test.js +0 -50
  248. package/dist/systems/geometry/geometry-system.test.d.ts +0 -2
  249. package/dist/systems/geometry/geometry-system.test.d.ts.map +0 -1
  250. package/dist/systems/geometry/geometry-system.test.js +0 -30
  251. package/dist/systems/slab/slab-system.test.d.ts +0 -2
  252. package/dist/systems/slab/slab-system.test.d.ts.map +0 -1
  253. package/dist/systems/slab/slab-system.test.js +0 -102
  254. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +0 -2
  255. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +0 -1
  256. package/dist/systems/wall/opening-cutout-geometry.test.js +0 -172
  257. package/dist/systems/wall/wall-opening-cutout.test.d.ts +0 -2
  258. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +0 -1
  259. package/dist/systems/wall/wall-opening-cutout.test.js +0 -59
  260. package/dist/systems/wall/wall-support-extension.test.d.ts +0 -2
  261. package/dist/systems/wall/wall-support-extension.test.d.ts.map +0 -1
  262. package/dist/systems/wall/wall-support-extension.test.js +0 -116
  263. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +0 -2
  264. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +0 -1
  265. package/dist/systems/wall/wall-terrain-bottom.test.js +0 -34
@@ -1,12 +1,7 @@
1
1
  import { emitter, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
- import { AWNING_WINDOW_SASH_NAME, CASEMENT_WINDOW_SASH_NAME, DOUBLE_HUNG_BOTTOM_SASH_NAME, DOUBLE_HUNG_TOP_SASH_NAME, FRENCH_CASEMENT_LEFT_SASH_NAME, FRENCH_CASEMENT_RIGHT_SASH_NAME, HOPPER_WINDOW_SASH_NAME, LOUVERED_WINDOW_SLATS_NAME, SINGLE_HUNG_ACTIVE_SASH_NAME, SLIDING_WINDOW_ACTIVE_PANEL_NAME, } from './window-system';
3
+ import { AWNING_WINDOW_SASH_NAME, CASEMENT_WINDOW_SASH_NAME, DOUBLE_HUNG_BOTTOM_SASH_NAME, DOUBLE_HUNG_TOP_SASH_NAME, FRENCH_CASEMENT_LEFT_SASH_NAME, FRENCH_CASEMENT_RIGHT_SASH_NAME, HOPPER_WINDOW_SASH_NAME, LOUVERED_WINDOW_SLATS_NAME, pendingWindowAnimationRebuilds, SINGLE_HUNG_ACTIVE_SASH_NAME, SLIDING_WINDOW_ACTIVE_PANEL_NAME, } from './window-system';
4
4
  const easeWindowAnimation = (value) => value * value * (3 - 2 * value);
5
- function markWindowDirty(windowId) {
6
- const scene = useScene.getState();
7
- const node = scene.nodes[windowId];
8
- scene.dirtyNodes.add(windowId);
9
- }
10
5
  /**
11
6
  * Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
12
7
  * 1 = open) by mutating the named child groups under `mesh`. Returns true when
@@ -123,18 +118,24 @@ export const WindowAnimationSystem = () => {
123
118
  const value = animation.from + (animation.to - animation.from) * easeWindowAnimation(progress);
124
119
  interactive.setWindowOpenState(typedWindowId, { [animation.field]: value });
125
120
  const appliedDirectly = applyDirectWindowAnimation(typedWindowId, value);
121
+ // A dirty mark is one-shot work, not a needs-frame signal — per-tick
122
+ // marks kept the scene from ever settling. Types without a direct pose
123
+ // path get a transient rebuild request instead.
126
124
  if (!appliedDirectly)
127
- markWindowDirty(typedWindowId);
125
+ pendingWindowAnimationRebuilds.add(typedWindowId);
128
126
  if (progress < 1)
129
127
  continue;
130
128
  interactive.cancelWindowAnimation(typedWindowId);
131
129
  if (animation.persist) {
132
130
  scene.updateNode(typedWindowId, { [animation.field]: animation.to });
133
131
  interactive.removeWindowOpenState(typedWindowId);
134
- markWindowDirty(typedWindowId);
132
+ // One-shot: the rebuild re-derives the pose from the persisted node.
133
+ scene.markDirty(typedWindowId);
135
134
  }
136
135
  else {
137
136
  interactive.setWindowOpenState(typedWindowId, { [animation.field]: animation.to });
137
+ if (!appliedDirectly)
138
+ scene.markDirty(typedWindowId);
138
139
  }
139
140
  emitter.emit('window:animation-completed', {
140
141
  windowId: typedWindowId,
@@ -10,6 +10,7 @@ export declare const DOUBLE_HUNG_BOTTOM_SASH_NAME = "double-hung-bottom-sash";
10
10
  export declare const LOUVERED_WINDOW_SLATS_NAME = "louvered-window-slats";
11
11
  export declare const AWNING_WINDOW_SASH_NAME = "awning-window-sash";
12
12
  export declare const HOPPER_WINDOW_SASH_NAME = "hopper-window-sash";
13
+ export declare const pendingWindowAnimationRebuilds: Set<`building_${string}` | `level_${string}` | `elevator_${string}` | `item_${string}` | `block_${string}` | `ceiling_${string}` | `column_${string}` | `wall_${string}` | `roof_${string}` | `slab_${string}` | `fence_${string}` | `construction-dimension_${string}` | `structural-grid_${string}` | `zone_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `site_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `cabinet-module_${string}` | `cabinet_${string}` | `rseg_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`>;
13
14
  export declare const WindowSystem: () => null;
14
15
  /**
15
16
  * Build a fresh window mesh for preview/ghost rendering.
@@ -1 +1 @@
1
- {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAwB9B,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAM3D,eAAO,MAAM,YAAY,YA4GxB,CAAA;AA28GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
1
+ {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAyB9B,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAS3D,eAAO,MAAM,8BAA8B,kkCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAoHxB,CAAA;AA28GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
4
4
  import * as THREE from 'three';
5
5
  import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
6
6
  import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
7
+ import { timeSpan } from '../../lib/perf-tracks';
7
8
  import useViewer from '../../store/use-viewer';
8
9
  import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
9
10
  // Invisible material for root mesh — used as selection hitbox only
@@ -30,6 +31,10 @@ export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
30
31
  const MAX_WINDOW_REBUILDS_PER_FRAME = 16;
31
32
  const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME;
32
33
  const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8;
34
+ // Transient rebuild requests from WindowAnimationSystem for windows whose type
35
+ // has no direct pose path: drained every frame. Deliberately not dirtyNodes —
36
+ // a running animation must not keep the dirty set from reaching zero.
37
+ export const pendingWindowAnimationRebuilds = new Set();
33
38
  export const WindowSystem = () => {
34
39
  const dirtyNodes = useScene((state) => state.dirtyNodes);
35
40
  const clearDirty = useScene((state) => state.clearDirty);
@@ -74,7 +79,7 @@ export const WindowSystem = () => {
74
79
  }
75
80
  }, [sceneMaterials]);
76
81
  useFrame(() => {
77
- if (dirtyNodes.size === 0)
82
+ if (dirtyNodes.size === 0 && pendingWindowAnimationRebuilds.size === 0)
78
83
  return;
79
84
  baseMaterial = textures
80
85
  ? getBaseMaterial(shading)
@@ -94,6 +99,13 @@ export const WindowSystem = () => {
94
99
  return;
95
100
  dirtyWindowIds.push(id);
96
101
  });
102
+ if (pendingWindowAnimationRebuilds.size > 0) {
103
+ for (const id of pendingWindowAnimationRebuilds) {
104
+ if (nodes[id]?.type === 'window' && !dirtyWindowIds.includes(id))
105
+ dirtyWindowIds.push(id);
106
+ }
107
+ pendingWindowAnimationRebuilds.clear();
108
+ }
97
109
  const useProgressiveWindowRebuilds = dirtyWindowIds.length > WINDOW_PROGRESSIVE_DIRTY_THRESHOLD;
98
110
  const frameStartedAt = performance.now();
99
111
  let rebuiltWindowsThisFrame = 0;
@@ -116,7 +128,9 @@ export const WindowSystem = () => {
116
128
  // Merge any live override (width / height / position) so the mesh
117
129
  // rebuild reflects the in-flight drag without zustand churn.
118
130
  const effectiveNode = getEffectiveNode(node);
119
- updateWindowMesh(effectiveNode, mesh);
131
+ timeSpan('window', () => updateWindowMesh(effectiveNode, mesh), {
132
+ properties: [['node', id]],
133
+ });
120
134
  clearDirty(id);
121
135
  rebuiltWindowsThisFrame += 1;
122
136
  // Rebuild the parent wall so its cutout reflects the updated window geometry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,16 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./capture": {
15
+ "types": "./dist/capture/index.d.ts",
16
+ "import": "./dist/capture/index.js",
17
+ "default": "./dist/capture/index.js"
18
+ },
19
+ "./capture/preview": {
20
+ "types": "./dist/capture/preview.d.ts",
21
+ "import": "./dist/capture/preview.js",
22
+ "default": "./dist/capture/preview.js"
13
23
  }
14
24
  },
15
25
  "files": [
@@ -19,14 +29,16 @@
19
29
  "scripts": {
20
30
  "build": "tsc --build",
21
31
  "dev": "tsgo --build --watch",
32
+ "test": "bun test src",
22
33
  "prepublishOnly": "npm run build"
23
34
  },
24
35
  "peerDependencies": {
25
- "@pascal-app/core": "^1.0.0-beta.4",
36
+ "@pascal-app/core": "^1.0.0",
26
37
  "@react-three/drei": "^10",
27
38
  "@react-three/fiber": "^9",
28
39
  "react": "^18 || ^19",
29
- "three": "^0.185"
40
+ "react-dom": "^18 || ^19",
41
+ "three": "^0.186"
30
42
  },
31
43
  "dependencies": {
32
44
  "three-bvh-csg": "^0.0.18",
@@ -34,9 +46,12 @@
34
46
  "zustand": "^5"
35
47
  },
36
48
  "devDependencies": {
49
+ "@pascal-app/core": "^1.0.0",
37
50
  "@pascal/typescript-config": "*",
51
+ "@react-three/test-renderer": "^9.1.0",
38
52
  "@types/node": "^22",
39
53
  "@types/react": "^19.2.2",
54
+ "@types/react-dom": "19.2.2",
40
55
  "@types/three": "^0.184.0",
41
56
  "typescript": "6.0.3"
42
57
  },
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=scene-bvh.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scene-bvh.test.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-bvh.test.ts"],"names":[],"mappings":""}
@@ -1,13 +0,0 @@
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 { describe, expect, test } from 'bun:test';
4
- import { Mesh } from 'three';
5
- import { isSceneBvhExcluded } from './scene-bvh';
6
- describe('scene BVH exclusions', () => {
7
- test('keeps annotation meshes on their normal raycast path', () => {
8
- expect(isSceneBvhExcluded(new Mesh())).toBe(false);
9
- const annotation = new Mesh();
10
- annotation.userData.excludeFromBvh = true;
11
- expect(isSceneBvhExcluded(annotation)).toBe(true);
12
- });
13
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=csg-utils.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"csg-utils.test.d.ts","sourceRoot":"","sources":["../../src/lib/csg-utils.test.ts"],"names":[],"mappings":""}
@@ -1,45 +0,0 @@
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 * as THREE from 'three';
5
- import { ensureRenderableGeometryAttributes } from './csg-utils';
6
- describe('ensureRenderableGeometryAttributes', () => {
7
- test('fills missing render attributes to match position count', () => {
8
- const geometry = new THREE.BufferGeometry();
9
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
10
- ensureRenderableGeometryAttributes(geometry);
11
- expect(geometry.getAttribute('position')?.count).toBe(3);
12
- expect(geometry.getAttribute('normal')?.count).toBe(3);
13
- expect(geometry.getAttribute('uv')?.count).toBe(3);
14
- expect(geometry.getAttribute('uv2')?.count).toBe(3);
15
- });
16
- test('replaces render attributes with the wrong item size', () => {
17
- const geometry = new THREE.BufferGeometry();
18
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
19
- geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0]), 1));
20
- geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0]), 1));
21
- ensureRenderableGeometryAttributes(geometry);
22
- expect(geometry.getAttribute('uv')?.itemSize).toBe(2);
23
- expect(geometry.getAttribute('uv2')?.itemSize).toBe(2);
24
- expect(geometry.getAttribute('uv')?.count).toBe(3);
25
- expect(geometry.getAttribute('uv2')?.count).toBe(3);
26
- });
27
- test('copies uv into uv2 without depending on backing array layout', () => {
28
- const geometry = new THREE.BufferGeometry();
29
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
30
- geometry.setAttribute('uv', new THREE.InterleavedBufferAttribute(new THREE.InterleavedBuffer(new Float32Array([0, 0, 7, 1, 0, 8, 0, 1, 9]), 3), 2, 0));
31
- ensureRenderableGeometryAttributes(geometry);
32
- const uv2 = geometry.getAttribute('uv2');
33
- expect(Array.from(uv2.array)).toEqual([0, 0, 1, 0, 0, 1]);
34
- });
35
- test('replaces empty geometries with a degenerate renderable triangle', () => {
36
- const geometry = new THREE.BufferGeometry();
37
- ensureRenderableGeometryAttributes(geometry);
38
- expect(geometry.getIndex()).toBeNull();
39
- expect(geometry.groups).toHaveLength(0);
40
- expect(geometry.getAttribute('position')?.count).toBe(3);
41
- expect(geometry.getAttribute('normal')?.count).toBe(3);
42
- expect(geometry.getAttribute('uv')?.count).toBe(3);
43
- expect(geometry.getAttribute('uv2')?.count).toBe(3);
44
- });
45
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=materials.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"materials.test.d.ts","sourceRoot":"","sources":["../../src/lib/materials.test.ts"],"names":[],"mappings":""}
@@ -1,25 +0,0 @@
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 { getTextureKey, resolveTextureRepeat } from './materials';
5
- function materialWithRepeat(repeat) {
6
- return {
7
- texture: {
8
- url: 'https://example.com/texture.png',
9
- repeat,
10
- },
11
- };
12
- }
13
- describe('legacy texture repeat values', () => {
14
- test('normalizes tuple, scalar, and Vector2-shaped repeats', () => {
15
- expect(resolveTextureRepeat([2, 3], undefined)).toEqual([2, 3]);
16
- expect(resolveTextureRepeat(2, undefined)).toEqual([2, 2]);
17
- expect(resolveTextureRepeat({ x: 2, y: 3 }, undefined)).toEqual([2, 3]);
18
- });
19
- test('falls back to scale for malformed repeats', () => {
20
- expect(resolveTextureRepeat({ width: 2 }, 4)).toEqual([4, 4]);
21
- });
22
- test('keeps distinct Vector2-shaped repeats in distinct cache entries', () => {
23
- expect(getTextureKey(materialWithRepeat({ x: 2, y: 3 }))).not.toBe(getTextureKey(materialWithRepeat({ x: 4, y: 5 })));
24
- });
25
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=merged-outline-node.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"merged-outline-node.test.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.test.ts"],"names":[],"mappings":""}
@@ -1,22 +0,0 @@
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 { Object3D, PerspectiveCamera, Scene } from 'three';
5
- import { mergedOutline } from './merged-outline-node';
6
- describe('merged outline rendering', () => {
7
- test('keeps selected outlines active during camera interaction', () => {
8
- const cameraInteractionActive = true;
9
- const params = {
10
- enabled: () => !cameraInteractionActive,
11
- primaryObjects: [new Object3D()],
12
- };
13
- const outline = mergedOutline(new Scene(), new PerspectiveCamera(), params);
14
- const frame = {
15
- get renderer() {
16
- throw new Error('outline renderer was reached');
17
- },
18
- };
19
- expect(() => outline.updateBefore(frame)).toThrow('outline renderer was reached');
20
- outline.dispose();
21
- });
22
- });
@@ -1,4 +0,0 @@
1
- export type Point2D = [number, number];
2
- export declare function unionPolygons(polygons: Point2D[][]): Point2D[][];
3
- export declare function subtractPolygonsFromPolygon(subject: Point2D[], cutters: Point2D[][]): Point2D[][];
4
- //# sourceMappingURL=polygon-union.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"polygon-union.d.ts","sourceRoot":"","sources":["../../src/lib/polygon-union.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAwStC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,CAShE;AA4CD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,CAkBjG"}
@@ -1,289 +0,0 @@
1
- const EPSILON = 1e-7;
2
- const KEY_SCALE = 1e6;
3
- function pointsEqual(a, b, tolerance = EPSILON) {
4
- return Math.hypot(a[0] - b[0], a[1] - b[1]) <= tolerance;
5
- }
6
- function pointKey(point) {
7
- return `${Math.round(point[0] * KEY_SCALE)}:${Math.round(point[1] * KEY_SCALE)}`;
8
- }
9
- function interpolate(a, b, t) {
10
- return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
11
- }
12
- function cross(ax, ay, bx, by) {
13
- return ax * by - ay * bx;
14
- }
15
- function polygonArea(points) {
16
- let area = 0;
17
- for (let i = 0; i < points.length; i++) {
18
- const current = points[i];
19
- const next = points[(i + 1) % points.length];
20
- area += current[0] * next[1] - next[0] * current[1];
21
- }
22
- return area / 2;
23
- }
24
- function pointOnSegment(point, start, end) {
25
- const dx = end[0] - start[0];
26
- const dz = end[1] - start[1];
27
- const crossValue = cross(point[0] - start[0], point[1] - start[1], dx, dz);
28
- if (Math.abs(crossValue) > EPSILON)
29
- return false;
30
- const dot = (point[0] - start[0]) * (point[0] - end[0]) + (point[1] - start[1]) * (point[1] - end[1]);
31
- return dot <= EPSILON;
32
- }
33
- function pointInPolygonOrOnBoundary(point, polygon) {
34
- if (polygon.some((start, index) => pointOnSegment(point, start, polygon[(index + 1) % polygon.length]))) {
35
- return true;
36
- }
37
- return pointInPolygon(point, polygon);
38
- }
39
- function pointInPolygon(point, polygon) {
40
- let inside = false;
41
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
42
- const pi = polygon[i];
43
- const pj = polygon[j];
44
- if (pointOnSegment(point, pj, pi))
45
- return false;
46
- const intersects = pi[1] > point[1] !== pj[1] > point[1] &&
47
- point[0] < ((pj[0] - pi[0]) * (point[1] - pi[1])) / (pj[1] - pi[1]) + pi[0];
48
- if (intersects)
49
- inside = !inside;
50
- }
51
- return inside;
52
- }
53
- function normalizeRing(ring) {
54
- const normalized = [];
55
- for (const [x, z] of ring) {
56
- if (!Number.isFinite(x) || !Number.isFinite(z))
57
- continue;
58
- const point = [x, z];
59
- const previous = normalized[normalized.length - 1];
60
- if (!previous || !pointsEqual(previous, point)) {
61
- normalized.push(point);
62
- }
63
- }
64
- const first = normalized[0];
65
- const last = normalized[normalized.length - 1];
66
- if (first && last && pointsEqual(first, last)) {
67
- normalized.pop();
68
- }
69
- if (normalized.length < 3 || Math.abs(polygonArea(normalized)) <= EPSILON)
70
- return [];
71
- return polygonArea(normalized) < 0 ? [...normalized].reverse() : normalized;
72
- }
73
- function addSplit(edge, t) {
74
- if (t < -EPSILON || t > 1 + EPSILON)
75
- return;
76
- const clamped = Math.max(0, Math.min(1, t));
77
- if (edge.splits.some((split) => Math.abs(split - clamped) <= EPSILON))
78
- return;
79
- edge.splits.push(clamped);
80
- }
81
- function parameterOnEdge(point, edge) {
82
- const dx = edge.end[0] - edge.start[0];
83
- const dz = edge.end[1] - edge.start[1];
84
- const lengthSquared = dx * dx + dz * dz;
85
- if (lengthSquared <= EPSILON)
86
- return 0;
87
- return ((point[0] - edge.start[0]) * dx + (point[1] - edge.start[1]) * dz) / lengthSquared;
88
- }
89
- function addIntersectionSplits(left, right) {
90
- const rx = left.end[0] - left.start[0];
91
- const rz = left.end[1] - left.start[1];
92
- const sx = right.end[0] - right.start[0];
93
- const sz = right.end[1] - right.start[1];
94
- const qpx = right.start[0] - left.start[0];
95
- const qpz = right.start[1] - left.start[1];
96
- const denominator = cross(rx, rz, sx, sz);
97
- const numerator = cross(qpx, qpz, rx, rz);
98
- if (Math.abs(denominator) <= EPSILON) {
99
- if (Math.abs(numerator) > EPSILON)
100
- return;
101
- for (const point of [left.start, left.end, right.start, right.end]) {
102
- if (pointOnSegment(point, left.start, left.end) &&
103
- pointOnSegment(point, right.start, right.end)) {
104
- addSplit(left, parameterOnEdge(point, left));
105
- addSplit(right, parameterOnEdge(point, right));
106
- }
107
- }
108
- return;
109
- }
110
- const t = cross(qpx, qpz, sx, sz) / denominator;
111
- const u = cross(qpx, qpz, rx, rz) / denominator;
112
- if (t < -EPSILON || t > 1 + EPSILON || u < -EPSILON || u > 1 + EPSILON)
113
- return;
114
- addSplit(left, t);
115
- addSplit(right, u);
116
- }
117
- function buildEdges(polygons) {
118
- const edges = [];
119
- polygons.forEach((polygon, polygonIndex) => {
120
- for (let i = 0; i < polygon.length; i++) {
121
- edges.push({
122
- start: polygon[i],
123
- end: polygon[(i + 1) % polygon.length],
124
- polygonIndex,
125
- splits: [0, 1],
126
- });
127
- }
128
- });
129
- for (let i = 0; i < edges.length; i++) {
130
- for (let j = i + 1; j < edges.length; j++) {
131
- const left = edges[i];
132
- const right = edges[j];
133
- if (left.polygonIndex === right.polygonIndex)
134
- continue;
135
- addIntersectionSplits(left, right);
136
- }
137
- }
138
- return edges;
139
- }
140
- function buildBoundarySegments(edges, polygons) {
141
- const segments = [];
142
- for (const edge of edges) {
143
- const splits = [...edge.splits].sort((a, b) => a - b);
144
- for (let i = 0; i < splits.length - 1; i++) {
145
- const startT = splits[i];
146
- const endT = splits[i + 1];
147
- if (endT - startT <= EPSILON)
148
- continue;
149
- const start = interpolate(edge.start, edge.end, startT);
150
- const end = interpolate(edge.start, edge.end, endT);
151
- const mid = interpolate(edge.start, edge.end, (startT + endT) / 2);
152
- const insideAnother = polygons.some((polygon, index) => index !== edge.polygonIndex && pointInPolygon(mid, polygon));
153
- if (!insideAnother) {
154
- segments.push({ start, end, used: false });
155
- }
156
- }
157
- }
158
- return removeDuplicateInteriorSegments(segments);
159
- }
160
- function segmentKey(segment) {
161
- const start = pointKey(segment.start);
162
- const end = pointKey(segment.end);
163
- return start < end ? `${start}|${end}` : `${end}|${start}`;
164
- }
165
- function removeDuplicateInteriorSegments(segments) {
166
- const groups = new Map();
167
- for (const segment of segments) {
168
- const key = segmentKey(segment);
169
- const group = groups.get(key);
170
- if (group) {
171
- group.push(segment);
172
- }
173
- else {
174
- groups.set(key, [segment]);
175
- }
176
- }
177
- const result = [];
178
- for (const group of groups.values()) {
179
- if (group.length === 1) {
180
- result.push(group[0]);
181
- continue;
182
- }
183
- const firstStart = pointKey(group[0].start);
184
- const firstEnd = pointKey(group[0].end);
185
- const hasOppositeDirection = group.some((segment) => pointKey(segment.start) === firstEnd && pointKey(segment.end) === firstStart);
186
- if (!hasOppositeDirection) {
187
- result.push(group[0]);
188
- }
189
- }
190
- return result;
191
- }
192
- function assembleRings(segments) {
193
- const byStart = new Map();
194
- for (const segment of segments) {
195
- const key = pointKey(segment.start);
196
- const group = byStart.get(key);
197
- if (group) {
198
- group.push(segment);
199
- }
200
- else {
201
- byStart.set(key, [segment]);
202
- }
203
- }
204
- const rings = [];
205
- for (const firstSegment of segments) {
206
- if (firstSegment.used)
207
- continue;
208
- firstSegment.used = true;
209
- const ring = [firstSegment.start, firstSegment.end];
210
- const startKey = pointKey(firstSegment.start);
211
- let currentKey = pointKey(firstSegment.end);
212
- while (currentKey !== startKey) {
213
- const next = byStart.get(currentKey)?.find((segment) => !segment.used);
214
- if (!next)
215
- break;
216
- next.used = true;
217
- ring.push(next.end);
218
- currentKey = pointKey(next.end);
219
- }
220
- if (currentKey !== startKey)
221
- continue;
222
- const last = ring[ring.length - 1];
223
- if (last && pointsEqual(ring[0], last)) {
224
- ring.pop();
225
- }
226
- const normalized = normalizeRing(ring);
227
- if (normalized.length >= 3) {
228
- rings.push(normalized);
229
- }
230
- }
231
- return rings;
232
- }
233
- export function unionPolygons(polygons) {
234
- const validPolygons = polygons.map(normalizeRing).filter((polygon) => polygon.length >= 3);
235
- if (validPolygons.length <= 1)
236
- return validPolygons;
237
- const edges = buildEdges(validPolygons);
238
- const segments = buildBoundarySegments(edges, validPolygons);
239
- const rings = assembleRings(segments);
240
- return rings.length > 0 ? rings : validPolygons;
241
- }
242
- function buildDifferenceBoundarySegments(edges, polygons) {
243
- const subject = polygons[0];
244
- const cutters = polygons.slice(1);
245
- if (!subject)
246
- return [];
247
- const segments = [];
248
- for (const edge of edges) {
249
- const splits = [...edge.splits].sort((a, b) => a - b);
250
- for (let i = 0; i < splits.length - 1; i++) {
251
- const startT = splits[i];
252
- const endT = splits[i + 1];
253
- if (endT - startT <= EPSILON)
254
- continue;
255
- const start = interpolate(edge.start, edge.end, startT);
256
- const end = interpolate(edge.start, edge.end, endT);
257
- const mid = interpolate(edge.start, edge.end, (startT + endT) / 2);
258
- if (edge.polygonIndex === 0) {
259
- const insideCutter = cutters.some((cutter) => pointInPolygonOrOnBoundary(mid, cutter));
260
- if (!insideCutter) {
261
- segments.push({ start, end, used: false });
262
- }
263
- continue;
264
- }
265
- const insideSubject = pointInPolygon(mid, subject);
266
- const insideAnotherCutter = cutters.some((cutter, cutterIndex) => cutterIndex !== edge.polygonIndex - 1 && pointInPolygonOrOnBoundary(mid, cutter));
267
- if (insideSubject && !insideAnotherCutter) {
268
- segments.push({ start: end, end: start, used: false });
269
- }
270
- }
271
- }
272
- return removeDuplicateInteriorSegments(segments);
273
- }
274
- export function subtractPolygonsFromPolygon(subject, cutters) {
275
- const validSubject = normalizeRing(subject);
276
- if (validSubject.length < 3)
277
- return [];
278
- const validCutters = cutters.map(normalizeRing).filter((polygon) => polygon.length >= 3);
279
- if (validCutters.length === 0)
280
- return [validSubject];
281
- const polygons = [validSubject, ...validCutters];
282
- const edges = buildEdges(polygons);
283
- const segments = buildDifferenceBoundarySegments(edges, polygons);
284
- const rings = assembleRings(segments);
285
- if (rings.length > 0)
286
- return rings;
287
- const fullyCovered = validSubject.every((point) => validCutters.some((cutter) => pointInPolygonOrOnBoundary(point, cutter)));
288
- return fullyCovered ? [] : [validSubject];
289
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=polygon-union.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"polygon-union.test.d.ts","sourceRoot":"","sources":["../../src/lib/polygon-union.test.ts"],"names":[],"mappings":""}