@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.
- package/dist/camera-animation.d.ts +20 -32
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +71 -358
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +11 -51
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +140 -12
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera-first-person.d.ts +52 -0
- package/dist/camera-first-person.d.ts.map +1 -0
- package/dist/camera-first-person.js +119 -0
- package/dist/camera-first-person.js.map +1 -0
- package/dist/camera-framing.d.ts +86 -0
- package/dist/camera-framing.d.ts.map +1 -0
- package/dist/camera-framing.js +230 -0
- package/dist/camera-framing.js.map +1 -0
- package/dist/camera-guards.d.ts +148 -0
- package/dist/camera-guards.d.ts.map +1 -0
- package/dist/camera-guards.js +150 -0
- package/dist/camera-guards.js.map +1 -0
- package/dist/camera-matrices.d.ts +14 -0
- package/dist/camera-matrices.d.ts.map +1 -0
- package/dist/camera-matrices.js +146 -0
- package/dist/camera-matrices.js.map +1 -0
- package/dist/camera-preset-view.d.ts +56 -0
- package/dist/camera-preset-view.d.ts.map +1 -0
- package/dist/camera-preset-view.js +180 -0
- package/dist/camera-preset-view.js.map +1 -0
- package/dist/camera-projection.d.ts +1 -1
- package/dist/camera-projection.d.ts.map +1 -1
- package/dist/camera-projection.js +83 -30
- package/dist/camera-projection.js.map +1 -1
- package/dist/camera-state.d.ts +62 -0
- package/dist/camera-state.d.ts.map +1 -0
- package/dist/camera-state.js +5 -0
- package/dist/camera-state.js.map +1 -0
- package/dist/camera.d.ts +22 -11
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +75 -120
- package/dist/camera.js.map +1 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +23 -1
- package/dist/environment.js.map +1 -1
- package/dist/fill-bridge-anchor.d.ts +32 -0
- package/dist/fill-bridge-anchor.d.ts.map +1 -0
- package/dist/fill-bridge-anchor.js +82 -0
- package/dist/fill-bridge-anchor.js.map +1 -0
- package/dist/fill-predicates.d.ts +44 -0
- package/dist/fill-predicates.d.ts.map +1 -0
- package/dist/fill-predicates.js +65 -0
- package/dist/fill-predicates.js.map +1 -0
- package/dist/fill-triangulate.d.ts +102 -0
- package/dist/fill-triangulate.d.ts.map +1 -0
- package/dist/fill-triangulate.js +333 -0
- package/dist/fill-triangulate.js.map +1 -0
- package/dist/index.d.ts +154 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +294 -45
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +49 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +122 -67
- package/dist/math.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +20 -25
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-geometry.d.ts +32 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +66 -0
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene.d.ts +25 -0
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +91 -109
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-lift.d.ts +109 -0
- package/dist/section-2d-lift.d.ts.map +1 -0
- package/dist/section-2d-lift.js +168 -0
- package/dist/section-2d-lift.js.map +1 -0
- package/dist/section-2d-line-buffer.d.ts +58 -0
- package/dist/section-2d-line-buffer.d.ts.map +1 -0
- package/dist/section-2d-line-buffer.js +109 -0
- package/dist/section-2d-line-buffer.js.map +1 -0
- package/dist/section-2d-overlay.d.ts +28 -77
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +161 -593
- package/dist/section-2d-overlay.js.map +1 -1
- package/dist/section-plane-basis.d.ts +22 -3
- package/dist/section-plane-basis.d.ts.map +1 -1
- package/dist/section-plane-basis.js +51 -10
- package/dist/section-plane-basis.js.map +1 -1
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js +10 -1
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +102 -0
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -0
- package/dist/shaders/section-2d-overlay.wgsl.js +281 -0
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -0
- package/dist/symbolic-overlay-pipelines.d.ts +10 -39
- package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
- package/dist/symbolic-overlay-pipelines.js +20 -163
- package/dist/symbolic-overlay-pipelines.js.map +1 -1
- package/package.json +3 -2
|
@@ -7,41 +7,23 @@
|
|
|
7
7
|
* Renders 2D section drawings (cut polygons, outlines, hatching) as a 3D overlay
|
|
8
8
|
* on the section plane in the WebGPU viewport. This provides an integrated view
|
|
9
9
|
* where the architectural drawing appears directly on the section cut surface.
|
|
10
|
+
*
|
|
11
|
+
* SIZE (issue #2456): this module is deliberately over the ~400-line house rule.
|
|
12
|
+
* Everything that could leave without dragging a GPU resource across a module
|
|
13
|
+
* boundary has left — the WGSL to `shaders/section-2d-overlay.wgsl.ts`, the
|
|
14
|
+
* 2D→3D lift and cap triangulation to `section-2d-lift.ts`, the per-family
|
|
15
|
+
* vertex buffer to `section-2d-line-buffer.ts`. What is left is one nullable,
|
|
16
|
+
* `init()`-created / `dispose()`-destroyed GPU object (two pipelines, one
|
|
17
|
+
* bind-group layout, one bind group, one uniform buffer holding a 160-byte
|
|
18
|
+
* record per draw site) plus
|
|
19
|
+
* the published `upload*`/`clear*`/`has*`/`draw*` API over it. Splitting that
|
|
20
|
+
* further means giving those shared resources a second owner, which is the cut
|
|
21
|
+
* #2456 explicitly refuses. Do not "fix" the line count by doing it.
|
|
10
22
|
*/
|
|
11
23
|
import { PIPELINE_CONSTANTS } from './constants.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// PARITY-ALLOW (#913): this is the **2D drafting** palette and is deliberately
|
|
16
|
-
// independent of the canonical 3D styling table
|
|
17
|
-
// (`ifc_lite_processing::style::default_color_for_type`). 2D plan/section fills
|
|
18
|
-
// follow drawing conventions (heavier alpha, line-weight-driven greys), not the
|
|
19
|
-
// 3D material appearance, so it is the one sanctioned second colour table. Do
|
|
20
|
-
// not "sync" it to the 3D defaults; do not add a third table anywhere else.
|
|
21
|
-
const IFC_TYPE_FILL_COLORS = {
|
|
22
|
-
IfcWall: [0.69, 0.69, 0.69, 0.95],
|
|
23
|
-
IfcWallStandardCase: [0.69, 0.69, 0.69, 0.95],
|
|
24
|
-
IfcColumn: [0.56, 0.56, 0.56, 0.95],
|
|
25
|
-
IfcBeam: [0.56, 0.56, 0.56, 0.95],
|
|
26
|
-
IfcSlab: [0.78, 0.78, 0.78, 0.95],
|
|
27
|
-
IfcRoof: [0.82, 0.82, 0.82, 0.95],
|
|
28
|
-
IfcFooting: [0.50, 0.50, 0.50, 0.95],
|
|
29
|
-
IfcPile: [0.44, 0.44, 0.44, 0.95],
|
|
30
|
-
IfcWindow: [0.91, 0.96, 0.99, 0.7],
|
|
31
|
-
IfcDoor: [0.96, 0.90, 0.83, 0.95],
|
|
32
|
-
IfcStair: [0.85, 0.85, 0.85, 0.95],
|
|
33
|
-
IfcStairFlight: [0.85, 0.85, 0.85, 0.95],
|
|
34
|
-
IfcRailing: [0.75, 0.75, 0.75, 0.95],
|
|
35
|
-
IfcPipeSegment: [0.63, 0.82, 1.0, 0.95],
|
|
36
|
-
IfcDuctSegment: [0.75, 1.0, 0.75, 0.95],
|
|
37
|
-
IfcFurnishingElement: [1.0, 0.88, 0.75, 0.95],
|
|
38
|
-
IfcSpace: [0.94, 0.94, 0.94, 0.5],
|
|
39
|
-
IfcSpatialZone: [0.88, 0.80, 0.96, 0.5],
|
|
40
|
-
default: [0.82, 0.82, 0.82, 0.95],
|
|
41
|
-
};
|
|
42
|
-
function getFillColor(ifcType) {
|
|
43
|
-
return IFC_TYPE_FILL_COLORS[ifcType] || IFC_TYPE_FILL_COLORS.default;
|
|
44
|
-
}
|
|
24
|
+
import { SECTION_2D_CAP_FILL_WGSL, SECTION_2D_OVERLAY_LINE_WGSL, SECTION_2D_UNIFORM_BYTES, SECTION_2D_UNIFORM_FLOATS, SECTION_2D_UNIFORM_SLOTS, SECTION_2D_UNIFORM_SLOT_COUNT, SECTION_2D_UNIFORM_SLOT_INDEX, sectionUniformSlotStride, } from './shaders/section-2d-overlay.wgsl.js';
|
|
25
|
+
import { buildCapFillGeometry, buildDrawingOutlineVertices, createSectionLift, } from './section-2d-lift.js';
|
|
26
|
+
import { WorldLineBuffer, } from './section-2d-line-buffer.js';
|
|
45
27
|
export class Section2DOverlayRenderer {
|
|
46
28
|
device;
|
|
47
29
|
fillPipeline = null;
|
|
@@ -49,6 +31,8 @@ export class Section2DOverlayRenderer {
|
|
|
49
31
|
bindGroupLayout = null;
|
|
50
32
|
uniformBuffer = null;
|
|
51
33
|
bindGroup = null;
|
|
34
|
+
/** Byte stride between the uniform slots in {@link uniformBuffer}. Set by `init()`. */
|
|
35
|
+
uniformStride = 0;
|
|
52
36
|
format;
|
|
53
37
|
sampleCount;
|
|
54
38
|
initialized = false;
|
|
@@ -57,7 +41,9 @@ export class Section2DOverlayRenderer {
|
|
|
57
41
|
// opaque black for backwards compatibility; a consumer can theme it (e.g. light
|
|
58
42
|
// lines on a dark canvas) via setOverlayLineColor().
|
|
59
43
|
overlayLineColor = [0, 0, 0, 1];
|
|
60
|
-
// Cached geometry buffers
|
|
44
|
+
// Cached section-cut geometry buffers. Unlike the world-space overlays below
|
|
45
|
+
// these ride the section plane and the fill half is indexed, so they are not
|
|
46
|
+
// WorldLineBuffers.
|
|
61
47
|
fillVertexBuffer = null;
|
|
62
48
|
fillIndexBuffer = null;
|
|
63
49
|
fillIndexCount = 0;
|
|
@@ -68,33 +54,28 @@ export class Section2DOverlayRenderer {
|
|
|
68
54
|
// does not depend on a section plane being active. Used by the
|
|
69
55
|
// "Show IFC Annotations" toggle so that authored 2D drawing curves
|
|
70
56
|
// (IfcAnnotation polylines/arcs) are visible in any view.
|
|
71
|
-
|
|
72
|
-
annotationLineVertexCount = 0;
|
|
57
|
+
annotationLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.annotation);
|
|
73
58
|
// Standalone 3D alignment centerline overlay. Independent buffer from the
|
|
74
59
|
// annotation lines (separate visibility toggle) but reuses the same line
|
|
75
60
|
// pipeline. IfcAlignment renders as a thin line here — not a ribbon mesh —
|
|
76
61
|
// to match IfcGrid axes / IfcAnnotation curves.
|
|
77
|
-
|
|
78
|
-
alignmentLineVertexCount = 0;
|
|
62
|
+
alignmentLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.alignment);
|
|
79
63
|
// Standalone 3D structural-grid (IfcGridAxis) overlay. Independent buffer so
|
|
80
64
|
// grid visibility is independent of the annotation/alignment overlays, but
|
|
81
65
|
// reuses the same line pipeline (issue #967).
|
|
82
|
-
|
|
83
|
-
gridLineVertexCount = 0;
|
|
66
|
+
gridLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.grid);
|
|
84
67
|
// Standalone 3D DXF reference-layer overlay (issue #2043, follow-up to
|
|
85
68
|
// #1782/#1929's 2D-only DXF underlay). Independent buffer so 3D DXF
|
|
86
69
|
// visibility is independent of the 2D underlay and the other overlays
|
|
87
70
|
// above, but reuses the same line pipeline + shared overlay colour —
|
|
88
71
|
// mirrors the grid overlay exactly. Line paths only (walls/boundaries);
|
|
89
72
|
// DXF fills/text are not lifted to 3D in this iteration.
|
|
90
|
-
|
|
91
|
-
dxfLineVertexCount = 0;
|
|
73
|
+
dxfLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.dxf);
|
|
92
74
|
// Standalone 3D clash-overlap-box overlay (#1277): the wireframe AABB of a
|
|
93
75
|
// focused clash, drawn in its OWN distinct colour (not the shared overlay
|
|
94
76
|
// line colour) so the overlap region reads as a third colour next to the two
|
|
95
77
|
// glowing clash elements.
|
|
96
|
-
|
|
97
|
-
clashBoxLineVertexCount = 0;
|
|
78
|
+
clashBoxLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.clashBox);
|
|
98
79
|
clashBoxLineColor = [1, 0, 1, 1];
|
|
99
80
|
constructor(device, format, sampleCount = 4) {
|
|
100
81
|
this.device = device;
|
|
@@ -105,198 +86,27 @@ export class Section2DOverlayRenderer {
|
|
|
105
86
|
if (this.initialized)
|
|
106
87
|
return;
|
|
107
88
|
// Create bind group layout
|
|
89
|
+
// `hasDynamicOffset`: one bind group serves every draw site, each reading
|
|
90
|
+
// its own 160-byte slot at a per-draw offset. See
|
|
91
|
+
// SECTION_2D_UNIFORM_SLOT_INDEX for why the slots exist at all.
|
|
108
92
|
this.bindGroupLayout = this.device.createBindGroupLayout({
|
|
109
93
|
entries: [
|
|
110
94
|
{
|
|
111
95
|
binding: 0,
|
|
112
96
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
113
|
-
buffer: {
|
|
97
|
+
buffer: {
|
|
98
|
+
type: 'uniform',
|
|
99
|
+
hasDynamicOffset: true,
|
|
100
|
+
minBindingSize: SECTION_2D_UNIFORM_BYTES,
|
|
101
|
+
},
|
|
114
102
|
},
|
|
115
103
|
],
|
|
116
104
|
});
|
|
117
105
|
const pipelineLayout = this.device.createPipelineLayout({
|
|
118
106
|
bindGroupLayouts: [this.bindGroupLayout],
|
|
119
107
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// polygons produced by SectionCutter. Per-vertex colour now DRIVES the
|
|
123
|
-
// fill when a polygon opts in (alpha ≥ 0): a material-layer wall/slab
|
|
124
|
-
// fills each layer with its own IfcMaterial colour, matching the 3D
|
|
125
|
-
// build-up. Polygons that pass the sentinel alpha −1 fall back to the
|
|
126
|
-
// uniform cap style, so single-material cuts read as one architectural
|
|
127
|
-
// section exactly as before. Hatch + stroke apply over either base.
|
|
128
|
-
const fillShader = this.device.createShaderModule({
|
|
129
|
-
code: `
|
|
130
|
-
struct Uniforms {
|
|
131
|
-
viewProj: mat4x4<f32>,
|
|
132
|
-
planeOffset: vec4<f32>, // Small offset to render slightly in front of section plane
|
|
133
|
-
capFillColor: vec4<f32>,
|
|
134
|
-
capStrokeColor: vec4<f32>,
|
|
135
|
-
// x=patternId, y=spacingPx, z=angleRad, w=widthPx
|
|
136
|
-
params: vec4<f32>,
|
|
137
|
-
// x=secondaryAngleRad, y,z,w reserved
|
|
138
|
-
params2: vec4<f32>,
|
|
139
|
-
}
|
|
140
|
-
@binding(0) @group(0) var<uniform> uniforms: Uniforms;
|
|
141
|
-
|
|
142
|
-
struct VertexInput {
|
|
143
|
-
@location(0) position: vec3<f32>,
|
|
144
|
-
@location(1) color: vec4<f32>,
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
struct VertexOutput {
|
|
148
|
-
@builtin(position) position: vec4<f32>,
|
|
149
|
-
@location(0) color: vec4<f32>,
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
@vertex
|
|
153
|
-
fn vs_main(input: VertexInput) -> VertexOutput {
|
|
154
|
-
var output: VertexOutput;
|
|
155
|
-
let offsetPos = input.position + uniforms.planeOffset.xyz;
|
|
156
|
-
output.position = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
|
|
157
|
-
output.color = input.color;
|
|
158
|
-
return output;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Screen-space hatch pattern helpers (ported from section-cap.wgsl).
|
|
162
|
-
fn lineMask(u: f32, s: f32, w: f32) -> f32 {
|
|
163
|
-
let f = fract(u / s) * s;
|
|
164
|
-
let d = min(f, s - f);
|
|
165
|
-
return 1.0 - smoothstep(w * 0.5, w * 0.5 + 1.0, d);
|
|
166
|
-
}
|
|
167
|
-
fn rotate(p: vec2<f32>, a: f32) -> vec2<f32> {
|
|
168
|
-
let c = cos(a);
|
|
169
|
-
let s = sin(a);
|
|
170
|
-
return vec2<f32>(c * p.x - s * p.y, s * p.x + c * p.y);
|
|
171
|
-
}
|
|
172
|
-
fn hatchIntensity(fragCoord: vec2<f32>, patternId: u32, spacing: f32, angle: f32, width: f32, angle2: f32) -> f32 {
|
|
173
|
-
let p = fragCoord;
|
|
174
|
-
if (patternId == 0u) { return 0.0; } // solid
|
|
175
|
-
if (patternId == 1u) { // diagonal
|
|
176
|
-
let r = rotate(p, angle);
|
|
177
|
-
return lineMask(r.x, spacing, width);
|
|
178
|
-
}
|
|
179
|
-
if (patternId == 2u) { // cross-hatch
|
|
180
|
-
let r = rotate(p, angle);
|
|
181
|
-
let r2 = rotate(p, angle2);
|
|
182
|
-
return max(lineMask(r.x, spacing, width), lineMask(r2.x, spacing, width));
|
|
183
|
-
}
|
|
184
|
-
if (patternId == 3u) { return lineMask(p.y, spacing, width); } // horizontal
|
|
185
|
-
if (patternId == 4u) { return lineMask(p.x, spacing, width); } // vertical
|
|
186
|
-
if (patternId == 5u) {
|
|
187
|
-
// Concrete (ISO 128-50): clean regular dot grid. The previous
|
|
188
|
-
// version layered dashes on top which looked noisy and broken.
|
|
189
|
-
// Dots sit at every grid intersection; radius scales with
|
|
190
|
-
// stroke width so the user's width slider works consistently.
|
|
191
|
-
let gx = p.x - round(p.x / spacing) * spacing;
|
|
192
|
-
let gy = p.y - round(p.y / spacing) * spacing;
|
|
193
|
-
let d = sqrt(gx * gx + gy * gy);
|
|
194
|
-
let radius = max(1.0, width * 1.2);
|
|
195
|
-
return 1.0 - smoothstep(radius, radius + 1.0, d);
|
|
196
|
-
}
|
|
197
|
-
if (patternId == 6u) { // brick
|
|
198
|
-
let bandH = spacing;
|
|
199
|
-
let band = floor(p.y / bandH);
|
|
200
|
-
let offset = select(0.0, bandH, (u32(band) & 1u) == 1u);
|
|
201
|
-
let horiz = lineMask(p.y, bandH, width);
|
|
202
|
-
let vertPos = p.x + offset * 0.5;
|
|
203
|
-
let vert = step(fract(vertPos / (bandH * 2.0)), 0.02);
|
|
204
|
-
return max(horiz, vert);
|
|
205
|
-
}
|
|
206
|
-
if (patternId == 7u) { // insulation
|
|
207
|
-
let y = spacing * 0.5 * sin(p.x * 6.2831853 / spacing) + p.y;
|
|
208
|
-
return lineMask(y, spacing, width);
|
|
209
|
-
}
|
|
210
|
-
return 0.0;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
struct FragOut {
|
|
214
|
-
@location(0) color: vec4<f32>,
|
|
215
|
-
@location(1) objectId: vec4<f32>,
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
@fragment
|
|
219
|
-
fn fs_main(input: VertexOutput) -> FragOut {
|
|
220
|
-
let patternId = u32(uniforms.params.x + 0.5);
|
|
221
|
-
let spacing = max(2.0, uniforms.params.y);
|
|
222
|
-
let angle = uniforms.params.z;
|
|
223
|
-
let width = max(1.0, uniforms.params.w);
|
|
224
|
-
let angle2 = uniforms.params2.x;
|
|
225
|
-
|
|
226
|
-
let h = hatchIntensity(input.position.xy, patternId, spacing, angle, width, angle2);
|
|
227
|
-
// Per-polygon colour (a material-layer slab fills with its own
|
|
228
|
-
// IfcMaterial RGBA) overrides the uniform cap fill when present.
|
|
229
|
-
// Polygons without a colour carry the sentinel alpha −1 and fall back
|
|
230
|
-
// to the user's cap style, byte-identically. Hatch + stroke apply over
|
|
231
|
-
// whichever base is chosen, so the architectural hatch still works.
|
|
232
|
-
let useVertex = input.color.a >= 0.0;
|
|
233
|
-
let baseFill = select(uniforms.capFillColor, input.color, useVertex);
|
|
234
|
-
let rgb = mix(baseFill.rgb, uniforms.capStrokeColor.rgb, h * uniforms.capStrokeColor.a);
|
|
235
|
-
let a = max(baseFill.a, h * uniforms.capStrokeColor.a);
|
|
236
|
-
|
|
237
|
-
var out: FragOut;
|
|
238
|
-
out.color = vec4<f32>(rgb, a);
|
|
239
|
-
out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
240
|
-
return out;
|
|
241
|
-
}
|
|
242
|
-
`,
|
|
243
|
-
});
|
|
244
|
-
// Shader for lines (uniform color)
|
|
245
|
-
const lineShader = this.device.createShaderModule({
|
|
246
|
-
code: `
|
|
247
|
-
// Mirrors the fill shader's uniform layout so both pipelines can share
|
|
248
|
-
// one buffer; the line shader only reads viewProj, planeOffset and the
|
|
249
|
-
// appended lineColor (byte offset 144). capFillColor/… are unused here but
|
|
250
|
-
// declared for correct field offsets.
|
|
251
|
-
struct Uniforms {
|
|
252
|
-
viewProj: mat4x4<f32>,
|
|
253
|
-
planeOffset: vec4<f32>,
|
|
254
|
-
capFillColor: vec4<f32>,
|
|
255
|
-
capStrokeColor: vec4<f32>,
|
|
256
|
-
params: vec4<f32>,
|
|
257
|
-
params2: vec4<f32>,
|
|
258
|
-
lineColor: vec4<f32>,
|
|
259
|
-
}
|
|
260
|
-
@binding(0) @group(0) var<uniform> uniforms: Uniforms;
|
|
261
|
-
|
|
262
|
-
struct VertexInput {
|
|
263
|
-
@location(0) position: vec3<f32>,
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
struct VertexOutput {
|
|
267
|
-
@builtin(position) position: vec4<f32>,
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
@vertex
|
|
271
|
-
fn vs_main(input: VertexInput) -> VertexOutput {
|
|
272
|
-
var output: VertexOutput;
|
|
273
|
-
let offsetPos = input.position + uniforms.planeOffset.xyz;
|
|
274
|
-
let clip = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
|
|
275
|
-
// Reverse-Z decal nudge for lines coplanar with model faces
|
|
276
|
-
// (issue #812). WebGPU forbids depthStencil.depthBias on non-
|
|
277
|
-
// triangle topologies, so we do the equivalent in clip space:
|
|
278
|
-
// adding a small positive multiple of clip.w raises NDC z by a
|
|
279
|
-
// constant after the w-divide, which under reverse-Z means
|
|
280
|
-
// "slightly closer" — enough to beat MSAA jitter on annotation
|
|
281
|
-
// lines that ride exactly on a wall/floor.
|
|
282
|
-
output.position = vec4<f32>(clip.x, clip.y, clip.z + 5e-5 * clip.w, clip.w);
|
|
283
|
-
return output;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
struct FragOutLine {
|
|
287
|
-
@location(0) color: vec4<f32>,
|
|
288
|
-
@location(1) objectId: vec4<f32>,
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
@fragment
|
|
292
|
-
fn fs_main(input: VertexOutput) -> FragOutLine {
|
|
293
|
-
var out: FragOutLine;
|
|
294
|
-
out.color = uniforms.lineColor; // consumer-themeable (defaults black)
|
|
295
|
-
out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
296
|
-
return out;
|
|
297
|
-
}
|
|
298
|
-
`,
|
|
299
|
-
});
|
|
108
|
+
const fillShader = this.device.createShaderModule({ code: SECTION_2D_CAP_FILL_WGSL });
|
|
109
|
+
const lineShader = this.device.createShaderModule({ code: SECTION_2D_OVERLAY_LINE_WGSL });
|
|
300
110
|
// Pipeline for filled polygons
|
|
301
111
|
this.fillPipeline = this.device.createRenderPipeline({
|
|
302
112
|
layout: pipelineLayout,
|
|
@@ -402,73 +212,48 @@ export class Section2DOverlayRenderer {
|
|
|
402
212
|
count: this.sampleCount,
|
|
403
213
|
},
|
|
404
214
|
});
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
// line shader reads viewProj/planeOffset + the appended lineColor (offset
|
|
415
|
-
// 144 B), so the two pipelines share this one buffer without aliasing.
|
|
215
|
+
// One 160-byte uniform buffer shared by BOTH pipelines: the fill fragment
|
|
216
|
+
// shader reads up to params2 (144 B), the line shader reads
|
|
217
|
+
// viewProj/planeOffset plus the appended lineColor at byte offset 144, so
|
|
218
|
+
// they do not alias. Field offsets live in SECTION_2D_UNIFORM_SLOTS next to
|
|
219
|
+
// the WGSL that defines them.
|
|
220
|
+
// …once per draw site (SECTION_2D_UNIFORM_SLOT_COUNT of them), spaced by
|
|
221
|
+
// the device's dynamic-offset alignment. Still one buffer under one owner;
|
|
222
|
+
// what changed is that the six draws no longer overwrite each other.
|
|
223
|
+
this.uniformStride = sectionUniformSlotStride(this.device);
|
|
416
224
|
this.uniformBuffer = this.device.createBuffer({
|
|
417
|
-
size:
|
|
225
|
+
size: this.uniformStride * SECTION_2D_UNIFORM_SLOT_COUNT,
|
|
418
226
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
419
227
|
});
|
|
420
|
-
// Create bind group
|
|
228
|
+
// Create bind group. `size` pins the binding to ONE record — without it
|
|
229
|
+
// the binding would span the whole buffer and the dynamic offset would be
|
|
230
|
+
// rejected for every slot but the first.
|
|
421
231
|
this.bindGroup = this.device.createBindGroup({
|
|
422
232
|
layout: this.bindGroupLayout,
|
|
423
233
|
entries: [
|
|
424
|
-
{
|
|
234
|
+
{
|
|
235
|
+
binding: 0,
|
|
236
|
+
resource: { buffer: this.uniformBuffer, offset: 0, size: SECTION_2D_UNIFORM_BYTES },
|
|
237
|
+
},
|
|
425
238
|
],
|
|
426
239
|
});
|
|
427
240
|
this.initialized = true;
|
|
428
241
|
}
|
|
429
242
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* - Y axis (down): 2D (x, y) = 3D (x, z) - looking down at XZ plane
|
|
434
|
-
* - Z axis (front): 2D (x, y) = 3D (x, y) - looking along Z at XY plane
|
|
435
|
-
* - X axis (side): 2D (x, y) = 3D (z, y) - looking along X at ZY plane
|
|
436
|
-
* When flipped, the 2D x coordinate is negated.
|
|
437
|
-
*
|
|
438
|
-
* Custom-plane path (issue #243): when `customPlane` is supplied, the
|
|
439
|
-
* 3D point is `origin + tangent*x2d + bitangent*y2d`. The same basis
|
|
440
|
-
* is used by `SectionCutter` to project triangle-plane intersections
|
|
441
|
-
* to 2D, so the round-trip is exact and the cap polygons land
|
|
442
|
-
* precisely on the user's tilted plane.
|
|
243
|
+
* The shared line-pipeline resources a WorldLineBuffer borrows for a draw.
|
|
244
|
+
* Returns null before `init()` has produced them (or after `dispose()`), so
|
|
245
|
+
* every `draw*Lines3D` bails on the same condition it always did.
|
|
443
246
|
*/
|
|
444
|
-
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
o[0] + t[0] * x2d + b[0] * y2d,
|
|
455
|
-
o[1] + t[1] * x2d + b[1] * y2d,
|
|
456
|
-
o[2] + t[2] * x2d + b[2] * y2d,
|
|
457
|
-
];
|
|
458
|
-
}
|
|
459
|
-
// Handle flipped - the 2D x coordinate was negated during projection
|
|
460
|
-
const x = flipped ? -x2d : x2d;
|
|
461
|
-
switch (axis) {
|
|
462
|
-
case 'down': // Y axis - horizontal cut (floor plan)
|
|
463
|
-
// 2D.x = 3D.x, 2D.y = 3D.z -> 3D (x, planeY, y)
|
|
464
|
-
return [x, planePosition, y2d];
|
|
465
|
-
case 'front': // Z axis - vertical cut (section view)
|
|
466
|
-
// 2D.x = 3D.x, 2D.y = 3D.y -> 3D (x, y, planeZ)
|
|
467
|
-
return [x, y2d, planePosition];
|
|
468
|
-
case 'side': // X axis - vertical cut (side elevation)
|
|
469
|
-
// 2D.x = 3D.z, 2D.y = 3D.y -> 3D (planeX, y, x)
|
|
470
|
-
return [planePosition, y2d, x];
|
|
471
|
-
}
|
|
247
|
+
lineResources() {
|
|
248
|
+
if (!this.linePipeline || !this.uniformBuffer || !this.bindGroup)
|
|
249
|
+
return null;
|
|
250
|
+
return {
|
|
251
|
+
device: this.device,
|
|
252
|
+
pipeline: this.linePipeline,
|
|
253
|
+
bindGroup: this.bindGroup,
|
|
254
|
+
uniformBuffer: this.uniformBuffer,
|
|
255
|
+
uniformStride: this.uniformStride,
|
|
256
|
+
};
|
|
472
257
|
}
|
|
473
258
|
/**
|
|
474
259
|
* Upload 2D drawing data to GPU buffers.
|
|
@@ -485,113 +270,30 @@ export class Section2DOverlayRenderer {
|
|
|
485
270
|
*/
|
|
486
271
|
uploadDrawing(polygons, lines, axis, planePosition, flipped = false, customPlane) {
|
|
487
272
|
this.init();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
if (this.fillIndexBuffer) {
|
|
494
|
-
this.fillIndexBuffer.destroy();
|
|
495
|
-
this.fillIndexBuffer = null;
|
|
496
|
-
}
|
|
497
|
-
if (this.lineVertexBuffer) {
|
|
498
|
-
this.lineVertexBuffer.destroy();
|
|
499
|
-
this.lineVertexBuffer = null;
|
|
500
|
-
}
|
|
501
|
-
this.fillIndexCount = 0;
|
|
502
|
-
this.lineVertexCount = 0;
|
|
503
|
-
// Build fill geometry (triangulated polygons)
|
|
504
|
-
const fillVertices = [];
|
|
505
|
-
const fillIndices = [];
|
|
506
|
-
let vertexOffset = 0;
|
|
507
|
-
for (const polygon of polygons) {
|
|
508
|
-
const outer = polygon.polygon.outer;
|
|
509
|
-
if (outer.length < 3)
|
|
510
|
-
continue;
|
|
511
|
-
// Per-polygon fill colour. A material-layer wall/slab delivers one polygon
|
|
512
|
-
// per layer, each carrying its IfcMaterial RGBA (window frame/glass parts
|
|
513
|
-
// likewise). Polygons WITHOUT a colour use the sentinel alpha −1 so the
|
|
514
|
-
// fill shader falls back to the uniform cap style (architectural fill +
|
|
515
|
-
// hatch) byte-identically — see fs_main.
|
|
516
|
-
const color = polygon.color ?? [0, 0, 0, -1];
|
|
517
|
-
// Hole-aware ear-clipping (reused from the IfcAnnotationFillArea fill path)
|
|
518
|
-
// replaces the old convex fan. The fan ignored holes and inverted on the
|
|
519
|
-
// CONCAVE cross-sections that arbitrary IFC profiles (and material-layer
|
|
520
|
-
// slabs) cut into, leaving the cut face uncovered — it read as a hollow
|
|
521
|
-
// shell. Section 2D points are (x, y); the triangulator works in (x, z),
|
|
522
|
-
// so y maps to z.
|
|
523
|
-
const outerRing = outer.map((p) => ({ x: p.x, z: p.y }));
|
|
524
|
-
const holeRings = polygon.polygon.holes
|
|
525
|
-
.filter((h) => h.length >= 3)
|
|
526
|
-
.map((h) => h.map((p) => ({ x: p.x, z: p.y })));
|
|
527
|
-
const stitched = holeRings.length > 0 ? joinHoles(outerRing, holeRings) : outerRing;
|
|
528
|
-
const tris = earClip(stitched);
|
|
529
|
-
if (tris.length === 0)
|
|
530
|
-
continue;
|
|
531
|
-
const baseVertex = vertexOffset;
|
|
532
|
-
for (const pt of stitched) {
|
|
533
|
-
const [x3d, y3d, z3d] = this.transform2Dto3D(pt.x, pt.z, axis, planePosition, flipped, customPlane);
|
|
534
|
-
fillVertices.push(x3d, y3d, z3d, color[0], color[1], color[2], color[3]);
|
|
535
|
-
vertexOffset++;
|
|
536
|
-
}
|
|
537
|
-
for (const [a, b, c] of tris) {
|
|
538
|
-
fillIndices.push(baseVertex + a, baseVertex + b, baseVertex + c);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
// Create fill buffers
|
|
542
|
-
if (fillVertices.length > 0) {
|
|
543
|
-
const fillVertexData = new Float32Array(fillVertices);
|
|
273
|
+
this.clearGeometry();
|
|
274
|
+
const lift = createSectionLift(axis, planePosition, flipped, customPlane);
|
|
275
|
+
const fill = buildCapFillGeometry(polygons, lift);
|
|
276
|
+
if (fill) {
|
|
544
277
|
this.fillVertexBuffer = this.device.createBuffer({
|
|
545
|
-
size:
|
|
278
|
+
size: fill.vertices.byteLength,
|
|
546
279
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
547
280
|
});
|
|
548
|
-
this.device.queue.writeBuffer(this.fillVertexBuffer, 0,
|
|
549
|
-
const fillIndexData = new Uint32Array(fillIndices);
|
|
281
|
+
this.device.queue.writeBuffer(this.fillVertexBuffer, 0, fill.vertices);
|
|
550
282
|
this.fillIndexBuffer = this.device.createBuffer({
|
|
551
|
-
size:
|
|
283
|
+
size: fill.indices.byteLength,
|
|
552
284
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
553
285
|
});
|
|
554
|
-
this.device.queue.writeBuffer(this.fillIndexBuffer, 0,
|
|
555
|
-
this.fillIndexCount =
|
|
286
|
+
this.device.queue.writeBuffer(this.fillIndexBuffer, 0, fill.indices);
|
|
287
|
+
this.fillIndexCount = fill.indices.length;
|
|
556
288
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
// Polygon outlines
|
|
560
|
-
for (const polygon of polygons) {
|
|
561
|
-
const outer = polygon.polygon.outer;
|
|
562
|
-
for (let i = 0; i < outer.length; i++) {
|
|
563
|
-
const p1 = outer[i];
|
|
564
|
-
const p2 = outer[(i + 1) % outer.length];
|
|
565
|
-
const [x1, y1, z1] = this.transform2Dto3D(p1.x, p1.y, axis, planePosition, flipped, customPlane);
|
|
566
|
-
const [x2, y2, z2] = this.transform2Dto3D(p2.x, p2.y, axis, planePosition, flipped, customPlane);
|
|
567
|
-
lineVertices.push(x1, y1, z1, x2, y2, z2);
|
|
568
|
-
}
|
|
569
|
-
// Hole outlines
|
|
570
|
-
for (const hole of polygon.polygon.holes) {
|
|
571
|
-
for (let i = 0; i < hole.length; i++) {
|
|
572
|
-
const p1 = hole[i];
|
|
573
|
-
const p2 = hole[(i + 1) % hole.length];
|
|
574
|
-
const [x1, y1, z1] = this.transform2Dto3D(p1.x, p1.y, axis, planePosition, flipped, customPlane);
|
|
575
|
-
const [x2, y2, z2] = this.transform2Dto3D(p2.x, p2.y, axis, planePosition, flipped, customPlane);
|
|
576
|
-
lineVertices.push(x1, y1, z1, x2, y2, z2);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
// Additional drawing lines (hatching, etc.)
|
|
581
|
-
for (const line of lines) {
|
|
582
|
-
const [x1, y1, z1] = this.transform2Dto3D(line.line.start.x, line.line.start.y, axis, planePosition, flipped, customPlane);
|
|
583
|
-
const [x2, y2, z2] = this.transform2Dto3D(line.line.end.x, line.line.end.y, axis, planePosition, flipped, customPlane);
|
|
584
|
-
lineVertices.push(x1, y1, z1, x2, y2, z2);
|
|
585
|
-
}
|
|
586
|
-
// Create line buffer
|
|
587
|
-
if (lineVertices.length > 0) {
|
|
588
|
-
const lineVertexData = new Float32Array(lineVertices);
|
|
289
|
+
const outline = buildDrawingOutlineVertices(polygons, lines, lift);
|
|
290
|
+
if (outline) {
|
|
589
291
|
this.lineVertexBuffer = this.device.createBuffer({
|
|
590
|
-
size:
|
|
292
|
+
size: outline.byteLength,
|
|
591
293
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
592
294
|
});
|
|
593
|
-
this.device.queue.writeBuffer(this.lineVertexBuffer, 0,
|
|
594
|
-
this.lineVertexCount =
|
|
295
|
+
this.device.queue.writeBuffer(this.lineVertexBuffer, 0, outline);
|
|
296
|
+
this.lineVertexCount = outline.length / 3; // Each vertex is 3 floats
|
|
595
297
|
}
|
|
596
298
|
}
|
|
597
299
|
/**
|
|
@@ -632,29 +334,27 @@ export class Section2DOverlayRenderer {
|
|
|
632
334
|
*/
|
|
633
335
|
uploadAnnotationLines3D(vertices) {
|
|
634
336
|
this.init();
|
|
635
|
-
|
|
636
|
-
this.annotationLineVertexBuffer.destroy();
|
|
637
|
-
this.annotationLineVertexBuffer = null;
|
|
638
|
-
}
|
|
639
|
-
this.annotationLineVertexCount = 0;
|
|
640
|
-
if (vertices.length < 6)
|
|
641
|
-
return;
|
|
642
|
-
this.annotationLineVertexBuffer = this.device.createBuffer({
|
|
643
|
-
size: vertices.byteLength,
|
|
644
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
645
|
-
});
|
|
646
|
-
this.device.queue.writeBuffer(this.annotationLineVertexBuffer, 0, vertices);
|
|
647
|
-
this.annotationLineVertexCount = vertices.length / 3;
|
|
337
|
+
this.annotationLines.upload(this.device, vertices);
|
|
648
338
|
}
|
|
649
339
|
clearAnnotationLines3D() {
|
|
650
|
-
|
|
651
|
-
this.annotationLineVertexBuffer.destroy();
|
|
652
|
-
this.annotationLineVertexBuffer = null;
|
|
653
|
-
}
|
|
654
|
-
this.annotationLineVertexCount = 0;
|
|
340
|
+
this.annotationLines.clear();
|
|
655
341
|
}
|
|
656
342
|
hasAnnotationLines3D() {
|
|
657
|
-
return this.
|
|
343
|
+
return this.annotationLines.has();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Draw the standalone annotation line overlay. Uses the same line pipeline
|
|
347
|
+
* as the section cut outlines (vertex format: 3 floats per vertex, line-list
|
|
348
|
+
* topology) but reads from a separate vertex buffer. The pipeline's
|
|
349
|
+
* `planeOffset` uniform is zeroed so vertices render at their authored
|
|
350
|
+
* world position.
|
|
351
|
+
*/
|
|
352
|
+
drawAnnotationLines3D(pass, viewProj) {
|
|
353
|
+
this.init();
|
|
354
|
+
const resources = this.lineResources();
|
|
355
|
+
if (!resources)
|
|
356
|
+
return;
|
|
357
|
+
this.annotationLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
658
358
|
}
|
|
659
359
|
/**
|
|
660
360
|
* Upload a flat Float32Array of 3D line-list vertices for the alignment
|
|
@@ -664,55 +364,13 @@ export class Section2DOverlayRenderer {
|
|
|
664
364
|
*/
|
|
665
365
|
uploadAlignmentLines3D(vertices) {
|
|
666
366
|
this.init();
|
|
667
|
-
|
|
668
|
-
this.alignmentLineVertexBuffer.destroy();
|
|
669
|
-
this.alignmentLineVertexBuffer = null;
|
|
670
|
-
}
|
|
671
|
-
this.alignmentLineVertexCount = 0;
|
|
672
|
-
if (vertices.length < 6)
|
|
673
|
-
return;
|
|
674
|
-
this.alignmentLineVertexBuffer = this.device.createBuffer({
|
|
675
|
-
size: vertices.byteLength,
|
|
676
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
677
|
-
});
|
|
678
|
-
this.device.queue.writeBuffer(this.alignmentLineVertexBuffer, 0, vertices);
|
|
679
|
-
this.alignmentLineVertexCount = vertices.length / 3;
|
|
367
|
+
this.alignmentLines.upload(this.device, vertices);
|
|
680
368
|
}
|
|
681
369
|
clearAlignmentLines3D() {
|
|
682
|
-
|
|
683
|
-
this.alignmentLineVertexBuffer.destroy();
|
|
684
|
-
this.alignmentLineVertexBuffer = null;
|
|
685
|
-
}
|
|
686
|
-
this.alignmentLineVertexCount = 0;
|
|
370
|
+
this.alignmentLines.clear();
|
|
687
371
|
}
|
|
688
372
|
hasAlignmentLines3D() {
|
|
689
|
-
return this.
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* Draw the standalone annotation line overlay. Uses the same line pipeline
|
|
693
|
-
* as the section cut outlines (vertex format: 3 floats per vertex, line-list
|
|
694
|
-
* topology) but reads from a separate vertex buffer. The pipeline's
|
|
695
|
-
* `planeOffset` uniform is zeroed so vertices render at their authored
|
|
696
|
-
* world position.
|
|
697
|
-
*/
|
|
698
|
-
drawAnnotationLines3D(pass, viewProj) {
|
|
699
|
-
this.init();
|
|
700
|
-
if (!this.linePipeline || !this.uniformBuffer || !this.bindGroup)
|
|
701
|
-
return;
|
|
702
|
-
if (!this.annotationLineVertexBuffer || this.annotationLineVertexCount === 0)
|
|
703
|
-
return;
|
|
704
|
-
// Reuse the existing fill uniform buffer slot 0 (viewProj + planeOffset).
|
|
705
|
-
// The line shader only reads those two fields, so the fill/cap-style
|
|
706
|
-
// tail of the uniforms is harmless leftover data.
|
|
707
|
-
const uniforms = new Float32Array(40);
|
|
708
|
-
uniforms.set(viewProj, 0);
|
|
709
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
710
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
711
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
712
|
-
pass.setPipeline(this.linePipeline);
|
|
713
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
714
|
-
pass.setVertexBuffer(0, this.annotationLineVertexBuffer);
|
|
715
|
-
pass.draw(this.annotationLineVertexCount);
|
|
373
|
+
return this.alignmentLines.has();
|
|
716
374
|
}
|
|
717
375
|
/**
|
|
718
376
|
* Draw the alignment centerline overlay. Identical pipeline/uniform setup as
|
|
@@ -720,19 +378,10 @@ export class Section2DOverlayRenderer {
|
|
|
720
378
|
*/
|
|
721
379
|
drawAlignmentLines3D(pass, viewProj) {
|
|
722
380
|
this.init();
|
|
723
|
-
|
|
381
|
+
const resources = this.lineResources();
|
|
382
|
+
if (!resources)
|
|
724
383
|
return;
|
|
725
|
-
|
|
726
|
-
return;
|
|
727
|
-
const uniforms = new Float32Array(40);
|
|
728
|
-
uniforms.set(viewProj, 0);
|
|
729
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
730
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
731
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
732
|
-
pass.setPipeline(this.linePipeline);
|
|
733
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
734
|
-
pass.setVertexBuffer(0, this.alignmentLineVertexBuffer);
|
|
735
|
-
pass.draw(this.alignmentLineVertexCount);
|
|
384
|
+
this.alignmentLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
736
385
|
}
|
|
737
386
|
/**
|
|
738
387
|
* Upload structural-grid (IfcGridAxis) segments as a flat
|
|
@@ -742,29 +391,13 @@ export class Section2DOverlayRenderer {
|
|
|
742
391
|
*/
|
|
743
392
|
uploadGridLines3D(vertices) {
|
|
744
393
|
this.init();
|
|
745
|
-
|
|
746
|
-
this.gridLineVertexBuffer.destroy();
|
|
747
|
-
this.gridLineVertexBuffer = null;
|
|
748
|
-
}
|
|
749
|
-
this.gridLineVertexCount = 0;
|
|
750
|
-
if (vertices.length < 6)
|
|
751
|
-
return;
|
|
752
|
-
this.gridLineVertexBuffer = this.device.createBuffer({
|
|
753
|
-
size: vertices.byteLength,
|
|
754
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
755
|
-
});
|
|
756
|
-
this.device.queue.writeBuffer(this.gridLineVertexBuffer, 0, vertices);
|
|
757
|
-
this.gridLineVertexCount = vertices.length / 3;
|
|
394
|
+
this.gridLines.upload(this.device, vertices);
|
|
758
395
|
}
|
|
759
396
|
clearGridLines3D() {
|
|
760
|
-
|
|
761
|
-
this.gridLineVertexBuffer.destroy();
|
|
762
|
-
this.gridLineVertexBuffer = null;
|
|
763
|
-
}
|
|
764
|
-
this.gridLineVertexCount = 0;
|
|
397
|
+
this.gridLines.clear();
|
|
765
398
|
}
|
|
766
399
|
hasGridLines3D() {
|
|
767
|
-
return this.
|
|
400
|
+
return this.gridLines.has();
|
|
768
401
|
}
|
|
769
402
|
/**
|
|
770
403
|
* Draw the structural-grid overlay. Identical pipeline/uniform setup as
|
|
@@ -772,19 +405,10 @@ export class Section2DOverlayRenderer {
|
|
|
772
405
|
*/
|
|
773
406
|
drawGridLines3D(pass, viewProj) {
|
|
774
407
|
this.init();
|
|
775
|
-
|
|
408
|
+
const resources = this.lineResources();
|
|
409
|
+
if (!resources)
|
|
776
410
|
return;
|
|
777
|
-
|
|
778
|
-
return;
|
|
779
|
-
const uniforms = new Float32Array(40);
|
|
780
|
-
uniforms.set(viewProj, 0);
|
|
781
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
782
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
783
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
784
|
-
pass.setPipeline(this.linePipeline);
|
|
785
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
786
|
-
pass.setVertexBuffer(0, this.gridLineVertexBuffer);
|
|
787
|
-
pass.draw(this.gridLineVertexCount);
|
|
411
|
+
this.gridLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
788
412
|
}
|
|
789
413
|
/**
|
|
790
414
|
* Upload the DXF reference-layer's line paths as a flat `[x,y,z, x,y,z, …]`
|
|
@@ -795,29 +419,13 @@ export class Section2DOverlayRenderer {
|
|
|
795
419
|
*/
|
|
796
420
|
uploadDxfLines3D(vertices) {
|
|
797
421
|
this.init();
|
|
798
|
-
|
|
799
|
-
this.dxfLineVertexBuffer.destroy();
|
|
800
|
-
this.dxfLineVertexBuffer = null;
|
|
801
|
-
}
|
|
802
|
-
this.dxfLineVertexCount = 0;
|
|
803
|
-
if (vertices.length < 6)
|
|
804
|
-
return;
|
|
805
|
-
this.dxfLineVertexBuffer = this.device.createBuffer({
|
|
806
|
-
size: vertices.byteLength,
|
|
807
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
808
|
-
});
|
|
809
|
-
this.device.queue.writeBuffer(this.dxfLineVertexBuffer, 0, vertices);
|
|
810
|
-
this.dxfLineVertexCount = vertices.length / 3;
|
|
422
|
+
this.dxfLines.upload(this.device, vertices);
|
|
811
423
|
}
|
|
812
424
|
clearDxfLines3D() {
|
|
813
|
-
|
|
814
|
-
this.dxfLineVertexBuffer.destroy();
|
|
815
|
-
this.dxfLineVertexBuffer = null;
|
|
816
|
-
}
|
|
817
|
-
this.dxfLineVertexCount = 0;
|
|
425
|
+
this.dxfLines.clear();
|
|
818
426
|
}
|
|
819
427
|
hasDxfLines3D() {
|
|
820
|
-
return this.
|
|
428
|
+
return this.dxfLines.has();
|
|
821
429
|
}
|
|
822
430
|
/**
|
|
823
431
|
* Draw the 3D DXF reference-layer overlay. Identical pipeline/uniform
|
|
@@ -825,19 +433,10 @@ export class Section2DOverlayRenderer {
|
|
|
825
433
|
*/
|
|
826
434
|
drawDxfLines3D(pass, viewProj) {
|
|
827
435
|
this.init();
|
|
828
|
-
|
|
436
|
+
const resources = this.lineResources();
|
|
437
|
+
if (!resources)
|
|
829
438
|
return;
|
|
830
|
-
|
|
831
|
-
return;
|
|
832
|
-
const uniforms = new Float32Array(40);
|
|
833
|
-
uniforms.set(viewProj, 0);
|
|
834
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
835
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
836
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
837
|
-
pass.setPipeline(this.linePipeline);
|
|
838
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
839
|
-
pass.setVertexBuffer(0, this.dxfLineVertexBuffer);
|
|
840
|
-
pass.draw(this.dxfLineVertexCount);
|
|
439
|
+
this.dxfLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
841
440
|
}
|
|
842
441
|
/** Colour for the clash-overlap box (its own, not the shared overlay colour). */
|
|
843
442
|
setClashBoxLineColor(color) {
|
|
@@ -850,46 +449,21 @@ export class Section2DOverlayRenderer {
|
|
|
850
449
|
*/
|
|
851
450
|
uploadClashBoxLines3D(vertices) {
|
|
852
451
|
this.init();
|
|
853
|
-
|
|
854
|
-
this.clashBoxLineVertexBuffer.destroy();
|
|
855
|
-
this.clashBoxLineVertexBuffer = null;
|
|
856
|
-
}
|
|
857
|
-
this.clashBoxLineVertexCount = 0;
|
|
858
|
-
if (vertices.length < 6)
|
|
859
|
-
return;
|
|
860
|
-
this.clashBoxLineVertexBuffer = this.device.createBuffer({
|
|
861
|
-
size: vertices.byteLength,
|
|
862
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
863
|
-
});
|
|
864
|
-
this.device.queue.writeBuffer(this.clashBoxLineVertexBuffer, 0, vertices);
|
|
865
|
-
this.clashBoxLineVertexCount = vertices.length / 3;
|
|
452
|
+
this.clashBoxLines.upload(this.device, vertices);
|
|
866
453
|
}
|
|
867
454
|
clearClashBoxLines3D() {
|
|
868
|
-
|
|
869
|
-
this.clashBoxLineVertexBuffer.destroy();
|
|
870
|
-
this.clashBoxLineVertexBuffer = null;
|
|
871
|
-
}
|
|
872
|
-
this.clashBoxLineVertexCount = 0;
|
|
455
|
+
this.clashBoxLines.clear();
|
|
873
456
|
}
|
|
874
457
|
hasClashBoxLines3D() {
|
|
875
|
-
return this.
|
|
458
|
+
return this.clashBoxLines.has();
|
|
876
459
|
}
|
|
877
460
|
/** Draw the clash-overlap box in its own colour. Same line pipeline. (#1277) */
|
|
878
461
|
drawClashBoxLines3D(pass, viewProj) {
|
|
879
462
|
this.init();
|
|
880
|
-
|
|
463
|
+
const resources = this.lineResources();
|
|
464
|
+
if (!resources)
|
|
881
465
|
return;
|
|
882
|
-
|
|
883
|
-
return;
|
|
884
|
-
const uniforms = new Float32Array(40);
|
|
885
|
-
uniforms.set(viewProj, 0);
|
|
886
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
887
|
-
uniforms.set(this.clashBoxLineColor, 36); // lineColor (byte offset 144)
|
|
888
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
889
|
-
pass.setPipeline(this.linePipeline);
|
|
890
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
891
|
-
pass.setVertexBuffer(0, this.clashBoxLineVertexBuffer);
|
|
892
|
-
pass.draw(this.clashBoxLineVertexCount);
|
|
466
|
+
this.clashBoxLines.draw(pass, resources, viewProj, this.clashBoxLineColor);
|
|
893
467
|
}
|
|
894
468
|
/**
|
|
895
469
|
* Check if there is geometry to draw
|
|
@@ -908,7 +482,7 @@ export class Section2DOverlayRenderer {
|
|
|
908
482
|
if (!this.hasGeometry()) {
|
|
909
483
|
return;
|
|
910
484
|
}
|
|
911
|
-
const {
|
|
485
|
+
const { viewProj } = options;
|
|
912
486
|
// No offset — cap renders exactly on the section plane. The previous
|
|
913
487
|
// 0.3m bias was there to keep the outline lines clear of below-plane
|
|
914
488
|
// geometry, but it made the cap visually drift off the slider plane
|
|
@@ -917,55 +491,43 @@ export class Section2DOverlayRenderer {
|
|
|
917
491
|
// coincident below-plane top faces is not an issue; the stencil gate
|
|
918
492
|
// keeps the fill restricted to the actual cap polygons.
|
|
919
493
|
const offset = [0, 0, 0];
|
|
920
|
-
// Update uniforms.
|
|
921
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
uniforms.
|
|
930
|
-
uniforms.set(this.overlayLineColor, 36); // section-cut outline colour
|
|
931
|
-
uniforms[16] = offset[0];
|
|
932
|
-
uniforms[17] = offset[1];
|
|
933
|
-
uniforms[18] = offset[2];
|
|
934
|
-
uniforms[19] = 0;
|
|
494
|
+
// Update uniforms. Field offsets come from SECTION_2D_UNIFORM_SLOTS, which
|
|
495
|
+
// sits next to the WGSL struct it describes.
|
|
496
|
+
const S = SECTION_2D_UNIFORM_SLOTS;
|
|
497
|
+
const uniforms = new Float32Array(SECTION_2D_UNIFORM_FLOATS);
|
|
498
|
+
uniforms.set(viewProj, S.viewProj);
|
|
499
|
+
uniforms.set(this.overlayLineColor, S.lineColor); // section-cut outline colour
|
|
500
|
+
uniforms[S.planeOffset + 0] = offset[0];
|
|
501
|
+
uniforms[S.planeOffset + 1] = offset[1];
|
|
502
|
+
uniforms[S.planeOffset + 2] = offset[2];
|
|
503
|
+
uniforms[S.planeOffset + 3] = 0;
|
|
935
504
|
const cs = options.capStyle;
|
|
936
505
|
if (cs) {
|
|
937
|
-
uniforms
|
|
938
|
-
uniforms
|
|
939
|
-
uniforms[
|
|
940
|
-
uniforms[
|
|
941
|
-
uniforms[
|
|
942
|
-
uniforms[
|
|
943
|
-
uniforms[
|
|
944
|
-
uniforms[27] = cs.strokeColor[3];
|
|
945
|
-
uniforms[28] = cs.patternId;
|
|
946
|
-
uniforms[29] = cs.spacingPx;
|
|
947
|
-
uniforms[30] = cs.angleRad;
|
|
948
|
-
uniforms[31] = cs.widthPx;
|
|
949
|
-
uniforms[32] = cs.secondaryAngleRad;
|
|
506
|
+
uniforms.set(cs.fillColor, S.capFillColor);
|
|
507
|
+
uniforms.set(cs.strokeColor, S.capStrokeColor);
|
|
508
|
+
uniforms[S.params + 0] = cs.patternId;
|
|
509
|
+
uniforms[S.params + 1] = cs.spacingPx;
|
|
510
|
+
uniforms[S.params + 2] = cs.angleRad;
|
|
511
|
+
uniforms[S.params + 3] = cs.widthPx;
|
|
512
|
+
uniforms[S.params2 + 0] = cs.secondaryAngleRad;
|
|
950
513
|
}
|
|
951
514
|
else {
|
|
952
515
|
// Sensible defaults when caller omits style (e.g. legacy lines-only
|
|
953
516
|
// use): solid fill using a warm-paper colour, no hatch.
|
|
954
|
-
uniforms[
|
|
955
|
-
uniforms[
|
|
956
|
-
uniforms[
|
|
957
|
-
uniforms[
|
|
958
|
-
uniforms[
|
|
959
|
-
uniforms[
|
|
960
|
-
uniforms[
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
517
|
+
uniforms.set([0.92, 0.88, 0.78, 1], S.capFillColor);
|
|
518
|
+
uniforms.set([0.10, 0.10, 0.10, 1], S.capStrokeColor);
|
|
519
|
+
uniforms[S.params + 0] = 0; // solid pattern
|
|
520
|
+
uniforms[S.params + 1] = 8;
|
|
521
|
+
uniforms[S.params + 2] = Math.PI / 4;
|
|
522
|
+
uniforms[S.params + 3] = 1;
|
|
523
|
+
uniforms[S.params2 + 0] = -Math.PI / 4;
|
|
524
|
+
}
|
|
525
|
+
// The section cut owns slot 0. Both draws below read it, which is correct:
|
|
526
|
+
// the fill and the outline are one drawing with one plane offset and one
|
|
527
|
+
// style. What they must NOT share is the record the world-space line
|
|
528
|
+
// families write, whose zeroed cap-style tail would otherwise arrive here.
|
|
529
|
+
const capOffset = SECTION_2D_UNIFORM_SLOT_INDEX.sectionCut * this.uniformStride;
|
|
530
|
+
this.device.queue.writeBuffer(this.uniformBuffer, capOffset, uniforms);
|
|
969
531
|
// Filled polygons = the 3D section cap. Render them ONLY when the
|
|
970
532
|
// caller opts in (`showFills: true` + a capStyle). This replaces the
|
|
971
533
|
// old stencil-parity cap, which leaked hatch into empty sky on non-
|
|
@@ -978,7 +540,7 @@ export class Section2DOverlayRenderer {
|
|
|
978
540
|
this.fillIndexBuffer &&
|
|
979
541
|
this.fillIndexCount > 0) {
|
|
980
542
|
pass.setPipeline(this.fillPipeline);
|
|
981
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
543
|
+
pass.setBindGroup(0, this.bindGroup, [capOffset]);
|
|
982
544
|
pass.setVertexBuffer(0, this.fillVertexBuffer);
|
|
983
545
|
pass.setIndexBuffer(this.fillIndexBuffer, 'uint32');
|
|
984
546
|
pass.drawIndexed(this.fillIndexCount);
|
|
@@ -990,13 +552,18 @@ export class Section2DOverlayRenderer {
|
|
|
990
552
|
this.lineVertexBuffer &&
|
|
991
553
|
this.lineVertexCount > 0) {
|
|
992
554
|
pass.setPipeline(this.linePipeline);
|
|
993
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
555
|
+
pass.setBindGroup(0, this.bindGroup, [capOffset]);
|
|
994
556
|
pass.setVertexBuffer(0, this.lineVertexBuffer);
|
|
995
557
|
pass.draw(this.lineVertexCount);
|
|
996
558
|
}
|
|
997
559
|
}
|
|
998
560
|
/**
|
|
999
|
-
* Dispose of GPU resources
|
|
561
|
+
* Dispose of GPU resources.
|
|
562
|
+
*
|
|
563
|
+
* Every family's buffer must be released here. The clash box (#1277) was the
|
|
564
|
+
* sixth family added and was missing from this list, leaking its vertex
|
|
565
|
+
* buffer on every teardown — `section-2d-overlay-lifecycle.test.ts` now counts
|
|
566
|
+
* destroys against uploads so a seventh family cannot repeat it.
|
|
1000
567
|
*/
|
|
1001
568
|
dispose() {
|
|
1002
569
|
this.clearGeometry();
|
|
@@ -1004,6 +571,7 @@ export class Section2DOverlayRenderer {
|
|
|
1004
571
|
this.clearAlignmentLines3D();
|
|
1005
572
|
this.clearGridLines3D();
|
|
1006
573
|
this.clearDxfLines3D();
|
|
574
|
+
this.clearClashBoxLines3D();
|
|
1007
575
|
if (this.uniformBuffer) {
|
|
1008
576
|
this.uniformBuffer.destroy();
|
|
1009
577
|
this.uniformBuffer = null;
|