@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
@@ -0,0 +1,289 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=polygon-union.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polygon-union.test.d.ts","sourceRoot":"","sources":["../../src/lib/polygon-union.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,105 @@
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 { subtractPolygonsFromPolygon, unionPolygons } from './polygon-union';
5
+ function polygonArea(points) {
6
+ let area = 0;
7
+ for (let i = 0; i < points.length; i++) {
8
+ const current = points[i];
9
+ const next = points[(i + 1) % points.length];
10
+ area += current[0] * next[1] - next[0] * current[1];
11
+ }
12
+ return Math.abs(area / 2);
13
+ }
14
+ describe('unionPolygons', () => {
15
+ test('collapses a contained polygon into the containing polygon', () => {
16
+ const small = [
17
+ [0, 0],
18
+ [1, 0],
19
+ [1, 1],
20
+ [0, 1],
21
+ ];
22
+ const large = [
23
+ [-1, -1],
24
+ [2, -1],
25
+ [2, 2],
26
+ [-1, 2],
27
+ ];
28
+ const result = unionPolygons([small, large]);
29
+ expect(result).toHaveLength(1);
30
+ expect(polygonArea(result[0])).toBeCloseTo(9);
31
+ });
32
+ test('combines overlapping rectangles into one boundary', () => {
33
+ const left = [
34
+ [0, 0],
35
+ [2, 0],
36
+ [2, 2],
37
+ [0, 2],
38
+ ];
39
+ const right = [
40
+ [1, 1],
41
+ [3, 1],
42
+ [3, 3],
43
+ [1, 3],
44
+ ];
45
+ const result = unionPolygons([left, right]);
46
+ expect(result).toHaveLength(1);
47
+ expect(result[0]).toHaveLength(8);
48
+ expect(polygonArea(result[0])).toBeCloseTo(7);
49
+ });
50
+ test('keeps disjoint polygons as separate boundaries', () => {
51
+ const left = [
52
+ [0, 0],
53
+ [1, 0],
54
+ [1, 1],
55
+ [0, 1],
56
+ ];
57
+ const right = [
58
+ [2, 0],
59
+ [3, 0],
60
+ [3, 1],
61
+ [2, 1],
62
+ ];
63
+ const result = unionPolygons([left, right]);
64
+ expect(result).toHaveLength(2);
65
+ expect(result.map(polygonArea)).toEqual([1, 1]);
66
+ });
67
+ });
68
+ describe('subtractPolygonsFromPolygon', () => {
69
+ test('turns a boundary-overlapping cutter into an indentation', () => {
70
+ const slab = [
71
+ [0, 0],
72
+ [4, 0],
73
+ [4, 3],
74
+ [0, 3],
75
+ ];
76
+ const cutout = [
77
+ [1, -0.5],
78
+ [3, -0.5],
79
+ [3, 1],
80
+ [1, 1],
81
+ ];
82
+ const result = subtractPolygonsFromPolygon(slab, [cutout]);
83
+ expect(result).toHaveLength(1);
84
+ expect(result[0]).toContainEqual([1, 1]);
85
+ expect(result[0]).toContainEqual([3, 1]);
86
+ expect(polygonArea(result[0])).toBeCloseTo(10);
87
+ });
88
+ test('returns separate contours when a cutter splits the subject', () => {
89
+ const slab = [
90
+ [0, 0],
91
+ [4, 0],
92
+ [4, 3],
93
+ [0, 3],
94
+ ];
95
+ const cutout = [
96
+ [1.5, -1],
97
+ [2.5, -1],
98
+ [2.5, 4],
99
+ [1.5, 4],
100
+ ];
101
+ const result = subtractPolygonsFromPolygon(slab, [cutout]);
102
+ expect(result).toHaveLength(2);
103
+ expect(result.map(polygonArea).sort((a, b) => a - b)).toEqual([4.5, 4.5]);
104
+ });
105
+ });
@@ -0,0 +1,29 @@
1
+ import type { SurfaceRole } from '@pascal-app/core';
2
+ export type SceneTheme = {
3
+ id: string;
4
+ name: string;
5
+ appearance: 'light' | 'dark';
6
+ background: string;
7
+ ground: string;
8
+ ambient: {
9
+ color: string;
10
+ intensity: number;
11
+ };
12
+ hemi?: {
13
+ sky: string;
14
+ ground: string;
15
+ intensity: number;
16
+ };
17
+ lights: Array<{
18
+ position: [number, number, number];
19
+ color: string;
20
+ intensity: number;
21
+ castShadow?: boolean;
22
+ }>;
23
+ toneMappingExposure: number;
24
+ clayTints?: Partial<Record<SurfaceRole, string>>;
25
+ };
26
+ export declare const SCENE_THEMES: SceneTheme[];
27
+ export declare const SCENE_THEME_IDS: string[];
28
+ export declare function getSceneTheme(id: string): SceneTheme;
29
+ //# sourceMappingURL=scene-themes.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,193 @@
1
+ export const SCENE_THEMES = [
2
+ {
3
+ id: 'studio',
4
+ name: 'Studio',
5
+ appearance: 'light',
6
+ background: '#ffffff',
7
+ ground: '#f4f4f2',
8
+ ambient: { color: '#ffffff', intensity: 0.15 },
9
+ hemi: { sky: '#ffffff', ground: '#aaa49a', intensity: 0.6 },
10
+ lights: [
11
+ { position: [10, 10, 10], color: '#ffffff', intensity: 4, castShadow: true },
12
+ { position: [-10, 10, -10], color: '#ffffff', intensity: 0.75 },
13
+ ],
14
+ toneMappingExposure: 0.9,
15
+ clayTints: {
16
+ wall: '#e9e5db',
17
+ floor: '#d8d2c4',
18
+ ceiling: '#f1ede4',
19
+ roof: '#c4bba6',
20
+ glazing: '#cdd8df',
21
+ },
22
+ },
23
+ {
24
+ id: 'paper',
25
+ name: 'Paper',
26
+ appearance: 'light',
27
+ background: '#ede9df',
28
+ ground: '#e7e1d3',
29
+ ambient: { color: '#fff9eb', intensity: 0.55 },
30
+ hemi: { sky: '#fff5d9', ground: '#c2b89c', intensity: 0.35 },
31
+ lights: [
32
+ { position: [16, 22, 12], color: '#fff1c8', intensity: 2.6, castShadow: true },
33
+ { position: [-14, 10, -6], color: '#dde5ff', intensity: 0.35 },
34
+ ],
35
+ toneMappingExposure: 1,
36
+ clayTints: {
37
+ wall: '#efe9da',
38
+ floor: '#ddd4bf',
39
+ ceiling: '#f5efe0',
40
+ roof: '#b9b09a',
41
+ glazing: '#cdd5d8',
42
+ },
43
+ },
44
+ {
45
+ id: 'sunset',
46
+ name: 'Sunset',
47
+ appearance: 'light',
48
+ background: '#f6e8d4',
49
+ ground: '#ecd9bf',
50
+ ambient: { color: '#ffd9a8', intensity: 0.45 },
51
+ hemi: { sky: '#ffd9a8', ground: '#5b4634', intensity: 0.4 },
52
+ lights: [
53
+ { position: [22, 8, 8], color: '#ffb070', intensity: 3.4, castShadow: true },
54
+ { position: [-14, 16, -10], color: '#a4b8ff', intensity: 0.4 },
55
+ ],
56
+ toneMappingExposure: 1,
57
+ clayTints: {
58
+ wall: '#f3e3cf',
59
+ floor: '#e2cdab',
60
+ ceiling: '#f6e7d2',
61
+ roof: '#a6764f',
62
+ glazing: '#e7c9a8',
63
+ },
64
+ },
65
+ {
66
+ id: 'overcast',
67
+ name: 'Overcast',
68
+ appearance: 'light',
69
+ background: '#e6e7e6',
70
+ ground: '#dadcd9',
71
+ ambient: { color: '#eef0ef', intensity: 1.1 },
72
+ hemi: { sky: '#f4f5f3', ground: '#bcbfbb', intensity: 0.9 },
73
+ lights: [{ position: [12, 28, 10], color: '#f4f5f3', intensity: 0.8, castShadow: true }],
74
+ toneMappingExposure: 0.95,
75
+ clayTints: {
76
+ wall: '#dedfdc',
77
+ floor: '#cdcec9',
78
+ ceiling: '#e8e9e6',
79
+ roof: '#a3a49e',
80
+ glazing: '#c6cdd0',
81
+ },
82
+ },
83
+ {
84
+ id: 'blueprint',
85
+ name: 'Blueprint',
86
+ appearance: 'light',
87
+ background: '#dde6ef',
88
+ ground: '#c9d6e6',
89
+ ambient: { color: '#cfdcec', intensity: 0.7 },
90
+ hemi: { sky: '#dfeaf6', ground: '#5b6b80', intensity: 0.55 },
91
+ lights: [
92
+ { position: [16, 24, 12], color: '#e6efff', intensity: 1.8, castShadow: true },
93
+ { position: [-12, 10, -8], color: '#9fb6d8', intensity: 0.4 },
94
+ ],
95
+ toneMappingExposure: 0.95,
96
+ clayTints: {
97
+ wall: '#9fb6d2',
98
+ floor: '#8ba2c2',
99
+ ceiling: '#aec0d8',
100
+ roof: '#5f789b',
101
+ glazing: '#b6d7ea',
102
+ },
103
+ },
104
+ {
105
+ id: 'mediterranean',
106
+ name: 'Mediterranean',
107
+ appearance: 'light',
108
+ background: '#bdd6e8',
109
+ ground: '#ddd2bb',
110
+ ambient: { color: '#d6e6f3', intensity: 0.5 },
111
+ hemi: { sky: '#a8c8e2', ground: '#d8c9a4', intensity: 0.6 },
112
+ lights: [
113
+ { position: [18, 20, 12], color: '#fff4d4', intensity: 3.6, castShadow: true },
114
+ { position: [-12, 8, -8], color: '#8fb3d8', intensity: 0.7 },
115
+ ],
116
+ toneMappingExposure: 0.9,
117
+ clayTints: {
118
+ wall: '#f6f1e6',
119
+ floor: '#e0d6c2',
120
+ ceiling: '#f3ede0',
121
+ roof: '#3e6585',
122
+ glazing: '#bcd3e2',
123
+ },
124
+ },
125
+ {
126
+ id: 'twilight',
127
+ name: 'Twilight',
128
+ appearance: 'dark',
129
+ background: '#3a3550',
130
+ ground: '#4a4566',
131
+ ambient: { color: '#a89cc8', intensity: 0.35 },
132
+ hemi: { sky: '#d8a8c0', ground: '#1c1830', intensity: 0.5 },
133
+ lights: [
134
+ { position: [-14, 22, -10], color: '#a4b6e8', intensity: 1.4, castShadow: true },
135
+ { position: [14, 6, 8], color: '#ffb070', intensity: 0.9 },
136
+ ],
137
+ toneMappingExposure: 1.1,
138
+ clayTints: {
139
+ wall: '#c5b9cf',
140
+ floor: '#ad9fbb',
141
+ ceiling: '#d2c6dc',
142
+ roof: '#5b4f74',
143
+ glazing: '#c3b6d4',
144
+ },
145
+ },
146
+ {
147
+ id: 'night',
148
+ name: 'Night',
149
+ appearance: 'dark',
150
+ background: '#1f2433',
151
+ ground: '#2b3247',
152
+ ambient: { color: '#a0b0ff', intensity: 0.07 },
153
+ hemi: { sky: '#3a4666', ground: '#0e111c', intensity: 0.4 },
154
+ lights: [
155
+ { position: [10, 10, 10], color: '#e0e5ff', intensity: 0.8, castShadow: true },
156
+ { position: [-10, 10, -10], color: '#8090ff', intensity: 0.2 },
157
+ ],
158
+ toneMappingExposure: 0.9,
159
+ clayTints: {
160
+ wall: '#aab3c6',
161
+ floor: '#98a1b5',
162
+ ceiling: '#b7bfd0',
163
+ roof: '#5b6680',
164
+ glazing: '#aebbd0',
165
+ },
166
+ },
167
+ {
168
+ id: 'verdant',
169
+ name: 'Verdant',
170
+ appearance: 'light',
171
+ background: '#d6e4d2',
172
+ ground: '#c7d6b4',
173
+ ambient: { color: '#e3efdd', intensity: 0.5 },
174
+ hemi: { sky: '#cfe6cf', ground: '#8ea06f', intensity: 0.65 },
175
+ lights: [
176
+ { position: [16, 22, 12], color: '#fff6d8', intensity: 3, castShadow: true },
177
+ { position: [-12, 10, -8], color: '#bfe0c2', intensity: 0.5 },
178
+ ],
179
+ toneMappingExposure: 0.95,
180
+ clayTints: {
181
+ wall: '#eef0e6',
182
+ floor: '#d8ddc6',
183
+ ceiling: '#f1f3ea',
184
+ roof: '#6f8a5a',
185
+ glazing: '#c4dcd0',
186
+ },
187
+ },
188
+ ];
189
+ export const SCENE_THEME_IDS = SCENE_THEMES.map((theme) => theme.id);
190
+ const SCENE_THEME_BY_ID = new Map(SCENE_THEMES.map((theme) => [theme.id, theme]));
191
+ export function getSceneTheme(id) {
192
+ return SCENE_THEME_BY_ID.get(id) ?? SCENE_THEMES[0];
193
+ }