@ifc-lite/renderer 1.43.0 → 1.44.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 (102) hide show
  1. package/dist/camera-animation.d.ts +20 -32
  2. package/dist/camera-animation.d.ts.map +1 -1
  3. package/dist/camera-animation.js +71 -358
  4. package/dist/camera-animation.js.map +1 -1
  5. package/dist/camera-controls.d.ts +11 -51
  6. package/dist/camera-controls.d.ts.map +1 -1
  7. package/dist/camera-controls.js +140 -12
  8. package/dist/camera-controls.js.map +1 -1
  9. package/dist/camera-first-person.d.ts +52 -0
  10. package/dist/camera-first-person.d.ts.map +1 -0
  11. package/dist/camera-first-person.js +119 -0
  12. package/dist/camera-first-person.js.map +1 -0
  13. package/dist/camera-framing.d.ts +86 -0
  14. package/dist/camera-framing.d.ts.map +1 -0
  15. package/dist/camera-framing.js +230 -0
  16. package/dist/camera-framing.js.map +1 -0
  17. package/dist/camera-guards.d.ts +148 -0
  18. package/dist/camera-guards.d.ts.map +1 -0
  19. package/dist/camera-guards.js +150 -0
  20. package/dist/camera-guards.js.map +1 -0
  21. package/dist/camera-matrices.d.ts +14 -0
  22. package/dist/camera-matrices.d.ts.map +1 -0
  23. package/dist/camera-matrices.js +146 -0
  24. package/dist/camera-matrices.js.map +1 -0
  25. package/dist/camera-preset-view.d.ts +56 -0
  26. package/dist/camera-preset-view.d.ts.map +1 -0
  27. package/dist/camera-preset-view.js +180 -0
  28. package/dist/camera-preset-view.js.map +1 -0
  29. package/dist/camera-projection.d.ts +1 -1
  30. package/dist/camera-projection.d.ts.map +1 -1
  31. package/dist/camera-projection.js +83 -30
  32. package/dist/camera-projection.js.map +1 -1
  33. package/dist/camera-state.d.ts +62 -0
  34. package/dist/camera-state.d.ts.map +1 -0
  35. package/dist/camera-state.js +5 -0
  36. package/dist/camera-state.js.map +1 -0
  37. package/dist/camera.d.ts +22 -11
  38. package/dist/camera.d.ts.map +1 -1
  39. package/dist/camera.js +75 -120
  40. package/dist/camera.js.map +1 -1
  41. package/dist/environment.d.ts.map +1 -1
  42. package/dist/environment.js +23 -1
  43. package/dist/environment.js.map +1 -1
  44. package/dist/fill-bridge-anchor.d.ts +32 -0
  45. package/dist/fill-bridge-anchor.d.ts.map +1 -0
  46. package/dist/fill-bridge-anchor.js +82 -0
  47. package/dist/fill-bridge-anchor.js.map +1 -0
  48. package/dist/fill-predicates.d.ts +44 -0
  49. package/dist/fill-predicates.d.ts.map +1 -0
  50. package/dist/fill-predicates.js +65 -0
  51. package/dist/fill-predicates.js.map +1 -0
  52. package/dist/fill-triangulate.d.ts +102 -0
  53. package/dist/fill-triangulate.d.ts.map +1 -0
  54. package/dist/fill-triangulate.js +333 -0
  55. package/dist/fill-triangulate.js.map +1 -0
  56. package/dist/index.d.ts +154 -8
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +294 -45
  59. package/dist/index.js.map +1 -1
  60. package/dist/math.d.ts +49 -1
  61. package/dist/math.d.ts.map +1 -1
  62. package/dist/math.js +122 -67
  63. package/dist/math.js.map +1 -1
  64. package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
  65. package/dist/renderer-symbolic-overlays.js +20 -25
  66. package/dist/renderer-symbolic-overlays.js.map +1 -1
  67. package/dist/scene-geometry.d.ts +32 -0
  68. package/dist/scene-geometry.d.ts.map +1 -1
  69. package/dist/scene-geometry.js +66 -0
  70. package/dist/scene-geometry.js.map +1 -1
  71. package/dist/scene.d.ts +25 -0
  72. package/dist/scene.d.ts.map +1 -1
  73. package/dist/scene.js +91 -109
  74. package/dist/scene.js.map +1 -1
  75. package/dist/section-2d-lift.d.ts +109 -0
  76. package/dist/section-2d-lift.d.ts.map +1 -0
  77. package/dist/section-2d-lift.js +168 -0
  78. package/dist/section-2d-lift.js.map +1 -0
  79. package/dist/section-2d-line-buffer.d.ts +58 -0
  80. package/dist/section-2d-line-buffer.d.ts.map +1 -0
  81. package/dist/section-2d-line-buffer.js +109 -0
  82. package/dist/section-2d-line-buffer.js.map +1 -0
  83. package/dist/section-2d-overlay.d.ts +28 -77
  84. package/dist/section-2d-overlay.d.ts.map +1 -1
  85. package/dist/section-2d-overlay.js +161 -593
  86. package/dist/section-2d-overlay.js.map +1 -1
  87. package/dist/section-plane-basis.d.ts +22 -3
  88. package/dist/section-plane-basis.d.ts.map +1 -1
  89. package/dist/section-plane-basis.js +51 -10
  90. package/dist/section-plane-basis.js.map +1 -1
  91. package/dist/section-plane.d.ts.map +1 -1
  92. package/dist/section-plane.js +10 -1
  93. package/dist/section-plane.js.map +1 -1
  94. package/dist/shaders/section-2d-overlay.wgsl.d.ts +102 -0
  95. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -0
  96. package/dist/shaders/section-2d-overlay.wgsl.js +281 -0
  97. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -0
  98. package/dist/symbolic-overlay-pipelines.d.ts +10 -39
  99. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  100. package/dist/symbolic-overlay-pipelines.js +20 -163
  101. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  102. package/package.json +3 -2
@@ -0,0 +1,281 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * WGSL shaders for the section 2D overlay (cut-cap fills + overlay lines).
6
+ *
7
+ * Two pipelines share this file because they deliberately share ONE 160-byte
8
+ * uniform LAYOUT: the fill shader reads `viewProj … params2` (the first 144 B)
9
+ * and the line shader reads `viewProj`, `planeOffset` and the appended
10
+ * `lineColor` at byte offset 144. They share the layout, not the storage — each
11
+ * draw site gets its own record in one buffer, addressed by a dynamic
12
+ * bind-group offset; see {@link SECTION_2D_UNIFORM_SLOT_INDEX}.
13
+ * The two `struct Uniforms` declarations below
14
+ * must therefore stay field-for-field compatible, and every TypeScript writer
15
+ * must address the buffer through `SECTION_2D_UNIFORM_SLOTS` rather than a
16
+ * hand-written index. `section-2d-uniform-layout.test.ts` parses both structs
17
+ * out of these sources and fails if either drifts from the slot table.
18
+ */
19
+ /**
20
+ * Float32 indices of each field in the shared uniform buffer.
21
+ *
22
+ * WGSL std140-style layout: `mat4x4<f32>` is 64 B (16 floats) and each
23
+ * `vec4<f32>` is 16 B (4 floats), all naturally aligned, so the offsets are a
24
+ * simple running sum. These are FLOAT offsets — multiply by 4 for the byte
25
+ * offsets quoted in the shader comments.
26
+ */
27
+ export const SECTION_2D_UNIFORM_SLOTS = {
28
+ /** mat4x4<f32> — floats 0..15 (bytes 0..63) */
29
+ viewProj: 0,
30
+ /** vec4<f32> — floats 16..19 (bytes 64..79) */
31
+ planeOffset: 16,
32
+ /** vec4<f32> — floats 20..23 (bytes 80..95) */
33
+ capFillColor: 20,
34
+ /** vec4<f32> — floats 24..27 (bytes 96..111) */
35
+ capStrokeColor: 24,
36
+ /** vec4<f32> x=patternId, y=spacingPx, z=angleRad, w=widthPx — floats 28..31 */
37
+ params: 28,
38
+ /** vec4<f32> x=secondaryAngleRad, y,z,w reserved — floats 32..35 */
39
+ params2: 32,
40
+ /** vec4<f32> overlay / section-cut line colour — floats 36..39 (bytes 144..159) */
41
+ lineColor: 36,
42
+ };
43
+ /** Total float count of one uniform record (40 floats = 160 bytes). */
44
+ export const SECTION_2D_UNIFORM_FLOATS = 40;
45
+ /** Total byte size of one uniform record. */
46
+ export const SECTION_2D_UNIFORM_BYTES = SECTION_2D_UNIFORM_FLOATS * 4;
47
+ /**
48
+ * One uniform slot per draw site.
49
+ *
50
+ * The overlay renderer issues up to six draws into a **single** render pass —
51
+ * the section cut cap plus five world-space line families — and every one of
52
+ * them needs a different `lineColor` (and, for the cap, a different
53
+ * `planeOffset` and the whole cap-style tail). With one record shared between
54
+ * them the last `queue.writeBuffer` before submit is what the GPU sees for all
55
+ * six: `writeBuffer` is a *queue* operation, so it is applied before the
56
+ * command buffer that references it executes, no matter where in the encoding
57
+ * it was issued. Every earlier draw therefore rendered with the last draw's
58
+ * uniforms — most visibly, the clash box's colour bleeding onto the annotation,
59
+ * alignment, grid and DXF overlays, and the cap losing its fill colour and
60
+ * hatch to the zeroed tail the line draws write.
61
+ *
62
+ * Each site owning a fixed slot, addressed with a dynamic bind-group offset,
63
+ * is what makes the six draws independent while keeping the buffer, the layout
64
+ * and the bind group under the single owner issue #2456 insisted on. A slot per
65
+ * site (rather than a bump allocator) needs no per-frame reset hook: each site
66
+ * draws at most once per pass.
67
+ */
68
+ export const SECTION_2D_UNIFORM_SLOT_INDEX = {
69
+ /** The section cut cap: fill + outline, which share one record by design. */
70
+ sectionCut: 0,
71
+ annotation: 1,
72
+ alignment: 2,
73
+ grid: 3,
74
+ dxf: 4,
75
+ clashBox: 5,
76
+ };
77
+ /** How many uniform records the shared buffer holds. */
78
+ export const SECTION_2D_UNIFORM_SLOT_COUNT = 6;
79
+ /**
80
+ * Byte stride between uniform slots for `device`.
81
+ *
82
+ * A dynamic offset must be a multiple of `minUniformBufferOffsetAlignment`,
83
+ * which a device reports for itself (256 is the guaranteed-supported default,
84
+ * but a device may allow less). Round the 160-byte record up to that alignment
85
+ * rather than assuming either number.
86
+ */
87
+ export function sectionUniformSlotStride(device) {
88
+ const alignment = device.limits?.minUniformBufferOffsetAlignment ?? 256;
89
+ const safe = Number.isFinite(alignment) && alignment > 0 ? alignment : 256;
90
+ return Math.ceil(SECTION_2D_UNIFORM_BYTES / safe) * safe;
91
+ }
92
+ /**
93
+ * Fill shader for the cut cap.
94
+ *
95
+ * Applies the user-defined cap style (fill colour + screen-space hatch) on top
96
+ * of the EXACT 2D section polygons produced by SectionCutter. Per-vertex colour
97
+ * DRIVES the fill when a polygon opts in (alpha >= 0): a material-layer
98
+ * wall/slab fills each layer with its own IfcMaterial colour, matching the 3D
99
+ * build-up. Polygons that pass the sentinel alpha -1 fall back to the uniform
100
+ * cap style, so single-material cuts read as one architectural section exactly
101
+ * as before. Hatch + stroke apply over either base.
102
+ */
103
+ export const SECTION_2D_CAP_FILL_WGSL = /* wgsl */ `
104
+ struct Uniforms {
105
+ viewProj: mat4x4<f32>,
106
+ planeOffset: vec4<f32>, // Small offset to render slightly in front of section plane
107
+ capFillColor: vec4<f32>,
108
+ capStrokeColor: vec4<f32>,
109
+ // x=patternId, y=spacingPx, z=angleRad, w=widthPx
110
+ params: vec4<f32>,
111
+ // x=secondaryAngleRad, y,z,w reserved
112
+ params2: vec4<f32>,
113
+ }
114
+ @binding(0) @group(0) var<uniform> uniforms: Uniforms;
115
+
116
+ struct VertexInput {
117
+ @location(0) position: vec3<f32>,
118
+ @location(1) color: vec4<f32>,
119
+ }
120
+
121
+ struct VertexOutput {
122
+ @builtin(position) position: vec4<f32>,
123
+ @location(0) color: vec4<f32>,
124
+ }
125
+
126
+ @vertex
127
+ fn vs_main(input: VertexInput) -> VertexOutput {
128
+ var output: VertexOutput;
129
+ let offsetPos = input.position + uniforms.planeOffset.xyz;
130
+ output.position = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
131
+ output.color = input.color;
132
+ return output;
133
+ }
134
+
135
+ // Screen-space hatch pattern helpers (ported from section-cap.wgsl).
136
+ fn lineMask(u: f32, s: f32, w: f32) -> f32 {
137
+ let f = fract(u / s) * s;
138
+ let d = min(f, s - f);
139
+ return 1.0 - smoothstep(w * 0.5, w * 0.5 + 1.0, d);
140
+ }
141
+ fn rotate(p: vec2<f32>, a: f32) -> vec2<f32> {
142
+ let c = cos(a);
143
+ let s = sin(a);
144
+ return vec2<f32>(c * p.x - s * p.y, s * p.x + c * p.y);
145
+ }
146
+ fn hatchIntensity(fragCoord: vec2<f32>, patternId: u32, spacing: f32, angle: f32, width: f32, angle2: f32) -> f32 {
147
+ let p = fragCoord;
148
+ if (patternId == 0u) { return 0.0; } // solid
149
+ if (patternId == 1u) { // diagonal
150
+ let r = rotate(p, angle);
151
+ return lineMask(r.x, spacing, width);
152
+ }
153
+ if (patternId == 2u) { // cross-hatch
154
+ let r = rotate(p, angle);
155
+ let r2 = rotate(p, angle2);
156
+ return max(lineMask(r.x, spacing, width), lineMask(r2.x, spacing, width));
157
+ }
158
+ if (patternId == 3u) { return lineMask(p.y, spacing, width); } // horizontal
159
+ if (patternId == 4u) { return lineMask(p.x, spacing, width); } // vertical
160
+ if (patternId == 5u) {
161
+ // Concrete (ISO 128-50): clean regular dot grid. The previous
162
+ // version layered dashes on top which looked noisy and broken.
163
+ // Dots sit at every grid intersection; radius scales with
164
+ // stroke width so the user's width slider works consistently.
165
+ let gx = p.x - round(p.x / spacing) * spacing;
166
+ let gy = p.y - round(p.y / spacing) * spacing;
167
+ let d = sqrt(gx * gx + gy * gy);
168
+ let radius = max(1.0, width * 1.2);
169
+ return 1.0 - smoothstep(radius, radius + 1.0, d);
170
+ }
171
+ if (patternId == 6u) { // brick
172
+ let bandH = spacing;
173
+ let band = floor(p.y / bandH);
174
+ // Signed parity. p is @builtin(position) today, so it is a
175
+ // framebuffer coordinate and never negative — but u32() of a
176
+ // negative float is an out-of-range conversion in WGSL, and the
177
+ // moment this hatch is fed anything but fragCoord (a world- or
178
+ // drawing-space variant) the running bond would break across
179
+ // y = 0 in a backend-dependent way. i32(-1) & 1 is 1, so the
180
+ // parity alternates correctly in both directions.
181
+ let offset = select(0.0, bandH, (i32(band) & 1) == 1);
182
+ let horiz = lineMask(p.y, bandH, width);
183
+ let vertPos = p.x + offset * 0.5;
184
+ let vert = step(fract(vertPos / (bandH * 2.0)), 0.02);
185
+ return max(horiz, vert);
186
+ }
187
+ if (patternId == 7u) { // insulation
188
+ let y = spacing * 0.5 * sin(p.x * 6.2831853 / spacing) + p.y;
189
+ return lineMask(y, spacing, width);
190
+ }
191
+ return 0.0;
192
+ }
193
+
194
+ struct FragOut {
195
+ @location(0) color: vec4<f32>,
196
+ @location(1) objectId: vec4<f32>,
197
+ }
198
+
199
+ @fragment
200
+ fn fs_main(input: VertexOutput) -> FragOut {
201
+ let patternId = u32(uniforms.params.x + 0.5);
202
+ let spacing = max(2.0, uniforms.params.y);
203
+ let angle = uniforms.params.z;
204
+ let width = max(1.0, uniforms.params.w);
205
+ let angle2 = uniforms.params2.x;
206
+
207
+ let h = hatchIntensity(input.position.xy, patternId, spacing, angle, width, angle2);
208
+ // Per-polygon colour (a material-layer slab fills with its own
209
+ // IfcMaterial RGBA) overrides the uniform cap fill when present.
210
+ // Polygons without a colour carry the sentinel alpha -1 and fall back
211
+ // to the user's cap style, byte-identically. Hatch + stroke apply over
212
+ // whichever base is chosen, so the architectural hatch still works.
213
+ let useVertex = input.color.a >= 0.0;
214
+ let baseFill = select(uniforms.capFillColor, input.color, useVertex);
215
+ let rgb = mix(baseFill.rgb, uniforms.capStrokeColor.rgb, h * uniforms.capStrokeColor.a);
216
+ let a = max(baseFill.a, h * uniforms.capStrokeColor.a);
217
+
218
+ var out: FragOut;
219
+ out.color = vec4<f32>(rgb, a);
220
+ out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);
221
+ return out;
222
+ }
223
+ `;
224
+ /**
225
+ * Line shader for the section-cut outline and the standalone world-space line
226
+ * overlays (annotation / alignment / grid / DXF / clash box).
227
+ */
228
+ export const SECTION_2D_OVERLAY_LINE_WGSL = /* wgsl */ `
229
+ // Mirrors the fill shader's uniform layout so both pipelines can share
230
+ // one buffer; the line shader only reads viewProj, planeOffset and the
231
+ // appended lineColor (byte offset 144). capFillColor/… are unused here but
232
+ // declared for correct field offsets.
233
+ struct Uniforms {
234
+ viewProj: mat4x4<f32>,
235
+ planeOffset: vec4<f32>,
236
+ capFillColor: vec4<f32>,
237
+ capStrokeColor: vec4<f32>,
238
+ params: vec4<f32>,
239
+ params2: vec4<f32>,
240
+ lineColor: vec4<f32>,
241
+ }
242
+ @binding(0) @group(0) var<uniform> uniforms: Uniforms;
243
+
244
+ struct VertexInput {
245
+ @location(0) position: vec3<f32>,
246
+ }
247
+
248
+ struct VertexOutput {
249
+ @builtin(position) position: vec4<f32>,
250
+ }
251
+
252
+ @vertex
253
+ fn vs_main(input: VertexInput) -> VertexOutput {
254
+ var output: VertexOutput;
255
+ let offsetPos = input.position + uniforms.planeOffset.xyz;
256
+ let clip = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
257
+ // Reverse-Z decal nudge for lines coplanar with model faces
258
+ // (issue #812). WebGPU forbids depthStencil.depthBias on non-
259
+ // triangle topologies, so we do the equivalent in clip space:
260
+ // adding a small positive multiple of clip.w raises NDC z by a
261
+ // constant after the w-divide, which under reverse-Z means
262
+ // "slightly closer" — enough to beat MSAA jitter on annotation
263
+ // lines that ride exactly on a wall/floor.
264
+ output.position = vec4<f32>(clip.x, clip.y, clip.z + 5e-5 * clip.w, clip.w);
265
+ return output;
266
+ }
267
+
268
+ struct FragOutLine {
269
+ @location(0) color: vec4<f32>,
270
+ @location(1) objectId: vec4<f32>,
271
+ }
272
+
273
+ @fragment
274
+ fn fs_main(input: VertexOutput) -> FragOutLine {
275
+ var out: FragOutLine;
276
+ out.color = uniforms.lineColor; // consumer-themeable (defaults black)
277
+ out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);
278
+ return out;
279
+ }
280
+ `;
281
+ //# sourceMappingURL=section-2d-overlay.wgsl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-2d-overlay.wgsl.js","sourceRoot":"","sources":["../../src/shaders/section-2d-overlay.wgsl.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC;IACX,+CAA+C;IAC/C,WAAW,EAAE,EAAE;IACf,+CAA+C;IAC/C,YAAY,EAAE,EAAE;IAChB,gDAAgD;IAChD,cAAc,EAAE,EAAE;IAClB,gFAAgF;IAChF,MAAM,EAAE,EAAE;IACV,oEAAoE;IACpE,OAAO,EAAE,EAAE;IACX,mFAAmF;IACnF,SAAS,EAAE,EAAE;CACL,CAAC;AAEX,uEAAuE;AACvE,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C,6CAA6C;AAC7C,MAAM,CAAC,MAAM,wBAAwB,GAAG,yBAAyB,GAAG,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,6EAA6E;IAC7E,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,CAAC;IACZ,IAAI,EAAE,CAAC;IACP,GAAG,EAAE,CAAC;IACN,QAAQ,EAAE,CAAC;CACH,CAAC;AAEX,wDAAwD;AACxD,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,+BAA+B,IAAI,GAAG,CAAC;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3E,OAAO,IAAI,CAAC,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwH5C,CAAC;AAER;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDhD,CAAC"}
@@ -5,11 +5,9 @@
5
5
  * viewProj)` entry point that the caller invokes from inside an existing
6
6
  * RGBA-blended render pass.
7
7
  *
8
- * Triangulation: simple ear-clipping for polygon-with-optional-holes,
9
- * inlined below to avoid adding a dependency. Good enough for typical IFC
10
- * fill regions (rooms, hatched zones) which are usually convex or near-convex.
11
- * Pathological concave shapes with many holes may show tessellation glitches —
12
- * an `earcut` upgrade is straightforward when it matters.
8
+ * Triangulation lives in `fill-triangulate.ts` and is shared with the section
9
+ * cut caps: rings are nested by the even-odd rule, holes are bridged in, and
10
+ * the result is ear-clipped.
13
11
  */
14
12
  import { SymbolicTextAtlas } from './symbolic-text-atlas.js';
15
13
  export interface SymbolicFillInput {
@@ -63,12 +61,14 @@ export declare class SymbolicFillPipeline {
63
61
  constructor(device: GPUDevice, presentationFormat: GPUTextureFormat, sampleCount?: number);
64
62
  private init;
65
63
  /**
66
- * Upload a list of fill regions. Each region is triangulated (ear-clipping,
67
- * holes-aware) into a single shared vertex buffer.
64
+ * Upload a list of fill regions. Each region is triangulated by the shared
65
+ * even-odd triangulator (`fill-triangulate.ts`) into a single vertex buffer.
68
66
  *
69
- * Pass an empty array to clear. Triangulation skips degenerate rings
70
- * (< 3 vertices) and silently drops any hole that can't be merged into the
71
- * outer ring (rare; usually overlapping rings in malformed IFC).
67
+ * Pass an empty array to clear. Rings with fewer than 3 vertices are
68
+ * dropped, and so is any ring set that yields no triangle at all (a fully
69
+ * collinear bound). Inner bounds themselves are never dropped: since #2516
70
+ * the bridge always finds an anchor, so a hole cannot fall out of the
71
+ * result the way it used to when it "couldn't be merged".
72
72
  */
73
73
  upload(fills: readonly SymbolicFillInput[]): void;
74
74
  hasGeometry(): boolean;
@@ -107,33 +107,4 @@ export declare class SymbolicTextPipeline {
107
107
  render(pass: GPURenderPassEncoder, viewProj: Float32Array, viewportPxWidth: number, viewportPxHeight: number, cameraRight: readonly [number, number, number], cameraUp: readonly [number, number, number], targetGlyphPx?: number): void;
108
108
  destroy(): void;
109
109
  }
110
- /**
111
- * Stitch each hole into the outer ring with a single bridge edge so the
112
- * result is a simple polygon ear-clipping can handle. Mirrors mapbox/earcut's
113
- * `eliminateHoles` pass:
114
- *
115
- * 1. For each hole, pick its rightmost (max-x) vertex as the bridge start.
116
- * 2. Sort holes by descending bridge-start x so outer holes go in first.
117
- * 3. Walk the outer ring for the vertex on the right side of the bridge
118
- * that's "visible" from the hole — closest match wins.
119
- * 4. Splice the hole (rotated to start at its bridge vertex, and reversed
120
- * so its winding opposes the outer ring) into the outer ring at the
121
- * anchor, closing both ends back to their starts to form a zero-area
122
- * bridge edge.
123
- *
124
- * This breaks under pathological inputs (overlapping holes, holes outside
125
- * the outer ring) but those don't occur in well-formed `IfcAnnotationFillArea`
126
- * geometry — the IFC schema requires the outer bound to contain all inner
127
- * bounds.
128
- */
129
- export type Pt = {
130
- x: number;
131
- z: number;
132
- };
133
- export declare function joinHoles(outer: Pt[], holes: Pt[][]): Pt[];
134
- /** Ear-clipping triangulation of a simple polygon. Returns triangle vertex indices. */
135
- export declare function earClip(ring: ReadonlyArray<{
136
- x: number;
137
- z: number;
138
- }>): number[][];
139
110
  //# sourceMappingURL=symbolic-overlay-pipelines.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"symbolic-overlay-pipelines.d.ts","sourceRoot":"","sources":["../src/symbolic-overlay-pipelines.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"symbolic-overlay-pipelines.d.ts","sourceRoot":"","sources":["../src/symbolic-overlay-pipelines.ts"],"names":[],"mappings":"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"}
@@ -8,15 +8,14 @@
8
8
  * viewProj)` entry point that the caller invokes from inside an existing
9
9
  * RGBA-blended render pass.
10
10
  *
11
- * Triangulation: simple ear-clipping for polygon-with-optional-holes,
12
- * inlined below to avoid adding a dependency. Good enough for typical IFC
13
- * fill regions (rooms, hatched zones) which are usually convex or near-convex.
14
- * Pathological concave shapes with many holes may show tessellation glitches —
15
- * an `earcut` upgrade is straightforward when it matters.
11
+ * Triangulation lives in `fill-triangulate.ts` and is shared with the section
12
+ * cut caps: rings are nested by the even-odd rule, holes are bridged in, and
13
+ * the result is ear-clipped.
16
14
  */
17
15
  import { SymbolicTextAtlas } from './symbolic-text-atlas.js';
18
16
  import { SYMBOLIC_FILL_WGSL, SYMBOLIC_TEXT_WGSL, } from './shaders/symbolic-overlay.wgsl.js';
19
17
  import { PIPELINE_CONSTANTS } from './constants.js';
18
+ import { triangulateRings } from './fill-triangulate.js';
20
19
  const FILL_VERTEX_STRIDE_BYTES = (3 + 4) * 4; // pos.xyz + color.rgba, 4 bytes each
21
20
  const TEXT_INSTANCE_STRIDE_BYTES = (3 + 3 + 3 + 4 + 4 + 3 + 1 + 1 + 4 + 1) * 4;
22
21
  // origin.xyz + rightAxis.xyz + upAxis.xyz + uvBounds.xyzw + color.rgba
@@ -137,12 +136,14 @@ export class SymbolicFillPipeline {
137
136
  });
138
137
  }
139
138
  /**
140
- * Upload a list of fill regions. Each region is triangulated (ear-clipping,
141
- * holes-aware) into a single shared vertex buffer.
139
+ * Upload a list of fill regions. Each region is triangulated by the shared
140
+ * even-odd triangulator (`fill-triangulate.ts`) into a single vertex buffer.
142
141
  *
143
- * Pass an empty array to clear. Triangulation skips degenerate rings
144
- * (< 3 vertices) and silently drops any hole that can't be merged into the
145
- * outer ring (rare; usually overlapping rings in malformed IFC).
142
+ * Pass an empty array to clear. Rings with fewer than 3 vertices are
143
+ * dropped, and so is any ring set that yields no triangle at all (a fully
144
+ * collinear bound). Inner bounds themselves are never dropped: since #2516
145
+ * the bridge always finds an anchor, so a hole cannot fall out of the
146
+ * result the way it used to when it "couldn't be merged".
146
147
  */
147
148
  upload(fills) {
148
149
  this.init();
@@ -607,14 +608,14 @@ function parseBoxAlignment(s) {
607
608
  return { horizontal, vertical };
608
609
  }
609
610
  /**
610
- * Triangulate a polygon-with-holes via ear-clipping. Each output triangle is
611
- * appended to `stream` as 3 × (x, y, z, r, g, b, a) entries (matching the
612
- * fill pipeline's vertex layout). Holes that fully enclose nothing or have
613
- * < 3 valid vertices are silently dropped.
611
+ * Triangulate a fill region (outer bound plus inner bounds) and append every
612
+ * output triangle to `stream` as 3 x (x, y, z, r, g, b, a) entries, matching
613
+ * the fill pipeline's vertex layout.
614
614
  *
615
- * Ear-clipping is O(n²) which is fine for fill regions (typically < 100
616
- * vertices). For pathological inputs we'd want earcut proper, but adding the
617
- * npm dependency for marginal gain isn't worth it.
615
+ * `holesOffsets` splits the flat `points` buffer into rings; rings with fewer
616
+ * than 3 vertices are dropped. Which of those rings are filled and which are
617
+ * voids is decided by nesting, not by position, so an inner bound that itself
618
+ * contains an island fills that island (see `fill-triangulate.ts`).
618
619
  */
619
620
  function triangulateFillTo(stream, fill) {
620
621
  const { points, holesOffsets, worldY, color } = fill;
@@ -625,7 +626,6 @@ function triangulateFillTo(stream, fill) {
625
626
  const ringStarts = [0, ...Array.from(holesOffsets), totalVerts];
626
627
  if (ringStarts.length < 2)
627
628
  return;
628
- // Pull each ring's vertices out.
629
629
  const rings = [];
630
630
  for (let r = 0; r < ringStarts.length - 1; r++) {
631
631
  const start = ringStarts[r];
@@ -640,155 +640,12 @@ function triangulateFillTo(stream, fill) {
640
640
  }
641
641
  if (rings.length === 0)
642
642
  return;
643
- // Stitch each hole into the outer ring with a bridge edge from the hole's
644
- // rightmost vertex to the nearest outer-ring vertex (the same approach
645
- // earcut.js takes for polygon-with-holes input). Ear-clipping then runs on
646
- // the resulting simple polygon. The hole's winding is reversed first so the
647
- // combined ring's signed area stays consistent and the ear-test sign holds.
648
- const outer = rings[0];
649
- const holes = rings.slice(1);
650
- const stitched = holes.length === 0 ? outer : joinHoles(outer, holes);
651
- const triangles = earClip(stitched);
643
+ const { points: verts, triangles } = triangulateRings(rings);
652
644
  for (const tri of triangles) {
653
645
  for (const idx of tri) {
654
- const v = stitched[idx];
646
+ const v = verts[idx];
655
647
  stream.push(v.x, worldY, v.z, color[0], color[1], color[2], color[3]);
656
648
  }
657
649
  }
658
650
  }
659
- export function joinHoles(outer, holes) {
660
- if (holes.length === 0)
661
- return outer;
662
- const sorted = holes
663
- .map((h) => {
664
- let bestI = 0;
665
- for (let i = 1; i < h.length; i++) {
666
- if (h[i].x > h[bestI].x)
667
- bestI = i;
668
- }
669
- return { ring: h, startIdx: bestI, startX: h[bestI].x, startZ: h[bestI].z };
670
- })
671
- .sort((a, b) => b.startX - a.startX);
672
- let result = outer.slice();
673
- for (const { ring, startIdx, startX, startZ } of sorted) {
674
- // Find the outer-ring index with the smallest distance to the bridge
675
- // start, preferring vertices to the right (x > startX). When nothing is
676
- // to the right (hole touches the outer ring's right edge), fall back to
677
- // global nearest.
678
- let bestIdx = -1;
679
- let bestDist = Infinity;
680
- for (let i = 0; i < result.length; i++) {
681
- const p = result[i];
682
- if (p.x <= startX)
683
- continue;
684
- const d = (p.x - startX) * (p.x - startX) + (p.z - startZ) * (p.z - startZ);
685
- if (d < bestDist) {
686
- bestDist = d;
687
- bestIdx = i;
688
- }
689
- }
690
- if (bestIdx < 0) {
691
- for (let i = 0; i < result.length; i++) {
692
- const p = result[i];
693
- const d = (p.x - startX) * (p.x - startX) + (p.z - startZ) * (p.z - startZ);
694
- if (d < bestDist) {
695
- bestDist = d;
696
- bestIdx = i;
697
- }
698
- }
699
- }
700
- if (bestIdx < 0)
701
- continue;
702
- // Hole reversed so its winding opposes outer (outer is CCW after earClip
703
- // normalisation; holes should be CW for the combined ring's area to come
704
- // out right). Rotate to start at the bridge vertex.
705
- const reversed = ring.slice().reverse();
706
- const reversedStartIdx = ring.length - 1 - startIdx;
707
- const rotated = [
708
- ...reversed.slice(reversedStartIdx),
709
- ...reversed.slice(0, reversedStartIdx),
710
- ];
711
- result = [
712
- ...result.slice(0, bestIdx + 1),
713
- ...rotated,
714
- rotated[0],
715
- result[bestIdx],
716
- ...result.slice(bestIdx + 1),
717
- ];
718
- }
719
- return result;
720
- }
721
- /** Ear-clipping triangulation of a simple polygon. Returns triangle vertex indices. */
722
- export function earClip(ring) {
723
- const n = ring.length;
724
- if (n < 3)
725
- return [];
726
- if (n === 3)
727
- return [[0, 1, 2]];
728
- // Working list of indices.
729
- const indices = [];
730
- // Determine winding: positive shoelace = CCW; otherwise reverse so the
731
- // ear-test below uses a consistent sign.
732
- let area = 0;
733
- for (let i = 0; i < n; i++) {
734
- const a = ring[i];
735
- const b = ring[(i + 1) % n];
736
- area += (a.x * b.z) - (b.x * a.z);
737
- }
738
- const ccw = area > 0;
739
- for (let i = 0; i < n; i++) {
740
- indices.push(ccw ? i : n - 1 - i);
741
- }
742
- const triangles = [];
743
- let safety = indices.length * indices.length;
744
- while (indices.length > 3 && safety-- > 0) {
745
- let found = false;
746
- for (let i = 0; i < indices.length; i++) {
747
- const ia = indices[(i + indices.length - 1) % indices.length];
748
- const ib = indices[i];
749
- const ic = indices[(i + 1) % indices.length];
750
- const a = ring[ia];
751
- const b = ring[ib];
752
- const c = ring[ic];
753
- // Convex check (cross product of (b-a) × (c-b) > 0 for CCW).
754
- const cross = (b.x - a.x) * (c.z - b.z) - (b.z - a.z) * (c.x - b.x);
755
- if (cross <= 0)
756
- continue;
757
- // No other vertex inside triangle (a, b, c).
758
- let containsOther = false;
759
- for (let j = 0; j < indices.length; j++) {
760
- const ij = indices[j];
761
- if (ij === ia || ij === ib || ij === ic)
762
- continue;
763
- if (pointInTriangle(ring[ij], a, b, c)) {
764
- containsOther = true;
765
- break;
766
- }
767
- }
768
- if (containsOther)
769
- continue;
770
- triangles.push([ia, ib, ic]);
771
- indices.splice(i, 1);
772
- found = true;
773
- break;
774
- }
775
- if (!found)
776
- break; // degenerate polygon — emit what we have
777
- }
778
- if (indices.length === 3) {
779
- triangles.push([indices[0], indices[1], indices[2]]);
780
- }
781
- return triangles;
782
- }
783
- function pointInTriangle(p, a, b, c) {
784
- const s1 = sign(p, a, b);
785
- const s2 = sign(p, b, c);
786
- const s3 = sign(p, c, a);
787
- const hasNeg = s1 < 0 || s2 < 0 || s3 < 0;
788
- const hasPos = s1 > 0 || s2 > 0 || s3 > 0;
789
- return !(hasNeg && hasPos);
790
- }
791
- function sign(p1, p2, p3) {
792
- return (p1.x - p3.x) * (p2.z - p3.z) - (p2.x - p3.x) * (p1.z - p3.z);
793
- }
794
651
  //# sourceMappingURL=symbolic-overlay-pipelines.js.map