@ifc-lite/renderer 1.41.1 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-fit-policy.d.ts.map +1 -1
- package/dist/camera-fit-policy.js +18 -7
- package/dist/camera-fit-policy.js.map +1 -1
- 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 +110 -85
- package/dist/camera.js.map +1 -1
- package/dist/deviation/deviation-pipeline.d.ts.map +1 -1
- package/dist/deviation/deviation-pipeline.js +21 -4
- package/dist/deviation/deviation-pipeline.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/index.d.ts +333 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +753 -704
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +59 -2
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +173 -23
- package/dist/math.js.map +1 -1
- package/dist/model-bounds-tracker.d.ts +96 -0
- package/dist/model-bounds-tracker.d.ts.map +1 -0
- package/dist/model-bounds-tracker.js +147 -0
- package/dist/model-bounds-tracker.js.map +1 -0
- package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-node.js +24 -4
- package/dist/pointcloud/point-cloud-node.js.map +1 -1
- package/dist/render-degradation.d.ts +77 -0
- package/dist/render-degradation.d.ts.map +1 -0
- package/dist/render-degradation.js +52 -0
- package/dist/render-degradation.js.map +1 -0
- package/dist/render-section-draw.d.ts +51 -0
- package/dist/render-section-draw.d.ts.map +1 -0
- package/dist/render-section-draw.js +66 -0
- package/dist/render-section-draw.js.map +1 -0
- package/dist/render-section-plane.d.ts +128 -0
- package/dist/render-section-plane.d.ts.map +1 -0
- package/dist/render-section-plane.js +298 -0
- package/dist/render-section-plane.js.map +1 -0
- package/dist/renderer-overlays.d.ts +144 -0
- package/dist/renderer-overlays.d.ts.map +1 -0
- package/dist/renderer-overlays.js +257 -0
- package/dist/renderer-overlays.js.map +1 -0
- package/dist/renderer-symbolic-overlays.d.ts +52 -0
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -0
- package/dist/renderer-symbolic-overlays.js +115 -0
- package/dist/renderer-symbolic-overlays.js.map +1 -0
- 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 +86 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +391 -201
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-lift.d.ts +105 -0
- package/dist/section-2d-lift.d.ts.map +1 -0
- package/dist/section-2d-lift.js +162 -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 +43 -75
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +189 -559
- 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/visual-enhancement.d.ts +33 -0
- package/dist/visual-enhancement.d.ts.map +1 -0
- package/dist/visual-enhancement.js +46 -0
- package/dist/visual-enhancement.js.map +1 -0
- package/package.json +4 -3
|
@@ -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,25 +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
|
-
|
|
66
|
+
gridLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.grid);
|
|
67
|
+
// Standalone 3D DXF reference-layer overlay (issue #2043, follow-up to
|
|
68
|
+
// #1782/#1929's 2D-only DXF underlay). Independent buffer so 3D DXF
|
|
69
|
+
// visibility is independent of the 2D underlay and the other overlays
|
|
70
|
+
// above, but reuses the same line pipeline + shared overlay colour —
|
|
71
|
+
// mirrors the grid overlay exactly. Line paths only (walls/boundaries);
|
|
72
|
+
// DXF fills/text are not lifted to 3D in this iteration.
|
|
73
|
+
dxfLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.dxf);
|
|
84
74
|
// Standalone 3D clash-overlap-box overlay (#1277): the wireframe AABB of a
|
|
85
75
|
// focused clash, drawn in its OWN distinct colour (not the shared overlay
|
|
86
76
|
// line colour) so the overlap region reads as a third colour next to the two
|
|
87
77
|
// glowing clash elements.
|
|
88
|
-
|
|
89
|
-
clashBoxLineVertexCount = 0;
|
|
78
|
+
clashBoxLines = new WorldLineBuffer(SECTION_2D_UNIFORM_SLOT_INDEX.clashBox);
|
|
90
79
|
clashBoxLineColor = [1, 0, 1, 1];
|
|
91
80
|
constructor(device, format, sampleCount = 4) {
|
|
92
81
|
this.device = device;
|
|
@@ -97,198 +86,27 @@ export class Section2DOverlayRenderer {
|
|
|
97
86
|
if (this.initialized)
|
|
98
87
|
return;
|
|
99
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.
|
|
100
92
|
this.bindGroupLayout = this.device.createBindGroupLayout({
|
|
101
93
|
entries: [
|
|
102
94
|
{
|
|
103
95
|
binding: 0,
|
|
104
96
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
105
|
-
buffer: {
|
|
97
|
+
buffer: {
|
|
98
|
+
type: 'uniform',
|
|
99
|
+
hasDynamicOffset: true,
|
|
100
|
+
minBindingSize: SECTION_2D_UNIFORM_BYTES,
|
|
101
|
+
},
|
|
106
102
|
},
|
|
107
103
|
],
|
|
108
104
|
});
|
|
109
105
|
const pipelineLayout = this.device.createPipelineLayout({
|
|
110
106
|
bindGroupLayouts: [this.bindGroupLayout],
|
|
111
107
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// polygons produced by SectionCutter. Per-vertex colour now DRIVES the
|
|
115
|
-
// fill when a polygon opts in (alpha ≥ 0): a material-layer wall/slab
|
|
116
|
-
// fills each layer with its own IfcMaterial colour, matching the 3D
|
|
117
|
-
// build-up. Polygons that pass the sentinel alpha −1 fall back to the
|
|
118
|
-
// uniform cap style, so single-material cuts read as one architectural
|
|
119
|
-
// section exactly as before. Hatch + stroke apply over either base.
|
|
120
|
-
const fillShader = this.device.createShaderModule({
|
|
121
|
-
code: `
|
|
122
|
-
struct Uniforms {
|
|
123
|
-
viewProj: mat4x4<f32>,
|
|
124
|
-
planeOffset: vec4<f32>, // Small offset to render slightly in front of section plane
|
|
125
|
-
capFillColor: vec4<f32>,
|
|
126
|
-
capStrokeColor: vec4<f32>,
|
|
127
|
-
// x=patternId, y=spacingPx, z=angleRad, w=widthPx
|
|
128
|
-
params: vec4<f32>,
|
|
129
|
-
// x=secondaryAngleRad, y,z,w reserved
|
|
130
|
-
params2: vec4<f32>,
|
|
131
|
-
}
|
|
132
|
-
@binding(0) @group(0) var<uniform> uniforms: Uniforms;
|
|
133
|
-
|
|
134
|
-
struct VertexInput {
|
|
135
|
-
@location(0) position: vec3<f32>,
|
|
136
|
-
@location(1) color: vec4<f32>,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
struct VertexOutput {
|
|
140
|
-
@builtin(position) position: vec4<f32>,
|
|
141
|
-
@location(0) color: vec4<f32>,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@vertex
|
|
145
|
-
fn vs_main(input: VertexInput) -> VertexOutput {
|
|
146
|
-
var output: VertexOutput;
|
|
147
|
-
let offsetPos = input.position + uniforms.planeOffset.xyz;
|
|
148
|
-
output.position = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
|
|
149
|
-
output.color = input.color;
|
|
150
|
-
return output;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Screen-space hatch pattern helpers (ported from section-cap.wgsl).
|
|
154
|
-
fn lineMask(u: f32, s: f32, w: f32) -> f32 {
|
|
155
|
-
let f = fract(u / s) * s;
|
|
156
|
-
let d = min(f, s - f);
|
|
157
|
-
return 1.0 - smoothstep(w * 0.5, w * 0.5 + 1.0, d);
|
|
158
|
-
}
|
|
159
|
-
fn rotate(p: vec2<f32>, a: f32) -> vec2<f32> {
|
|
160
|
-
let c = cos(a);
|
|
161
|
-
let s = sin(a);
|
|
162
|
-
return vec2<f32>(c * p.x - s * p.y, s * p.x + c * p.y);
|
|
163
|
-
}
|
|
164
|
-
fn hatchIntensity(fragCoord: vec2<f32>, patternId: u32, spacing: f32, angle: f32, width: f32, angle2: f32) -> f32 {
|
|
165
|
-
let p = fragCoord;
|
|
166
|
-
if (patternId == 0u) { return 0.0; } // solid
|
|
167
|
-
if (patternId == 1u) { // diagonal
|
|
168
|
-
let r = rotate(p, angle);
|
|
169
|
-
return lineMask(r.x, spacing, width);
|
|
170
|
-
}
|
|
171
|
-
if (patternId == 2u) { // cross-hatch
|
|
172
|
-
let r = rotate(p, angle);
|
|
173
|
-
let r2 = rotate(p, angle2);
|
|
174
|
-
return max(lineMask(r.x, spacing, width), lineMask(r2.x, spacing, width));
|
|
175
|
-
}
|
|
176
|
-
if (patternId == 3u) { return lineMask(p.y, spacing, width); } // horizontal
|
|
177
|
-
if (patternId == 4u) { return lineMask(p.x, spacing, width); } // vertical
|
|
178
|
-
if (patternId == 5u) {
|
|
179
|
-
// Concrete (ISO 128-50): clean regular dot grid. The previous
|
|
180
|
-
// version layered dashes on top which looked noisy and broken.
|
|
181
|
-
// Dots sit at every grid intersection; radius scales with
|
|
182
|
-
// stroke width so the user's width slider works consistently.
|
|
183
|
-
let gx = p.x - round(p.x / spacing) * spacing;
|
|
184
|
-
let gy = p.y - round(p.y / spacing) * spacing;
|
|
185
|
-
let d = sqrt(gx * gx + gy * gy);
|
|
186
|
-
let radius = max(1.0, width * 1.2);
|
|
187
|
-
return 1.0 - smoothstep(radius, radius + 1.0, d);
|
|
188
|
-
}
|
|
189
|
-
if (patternId == 6u) { // brick
|
|
190
|
-
let bandH = spacing;
|
|
191
|
-
let band = floor(p.y / bandH);
|
|
192
|
-
let offset = select(0.0, bandH, (u32(band) & 1u) == 1u);
|
|
193
|
-
let horiz = lineMask(p.y, bandH, width);
|
|
194
|
-
let vertPos = p.x + offset * 0.5;
|
|
195
|
-
let vert = step(fract(vertPos / (bandH * 2.0)), 0.02);
|
|
196
|
-
return max(horiz, vert);
|
|
197
|
-
}
|
|
198
|
-
if (patternId == 7u) { // insulation
|
|
199
|
-
let y = spacing * 0.5 * sin(p.x * 6.2831853 / spacing) + p.y;
|
|
200
|
-
return lineMask(y, spacing, width);
|
|
201
|
-
}
|
|
202
|
-
return 0.0;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
struct FragOut {
|
|
206
|
-
@location(0) color: vec4<f32>,
|
|
207
|
-
@location(1) objectId: vec4<f32>,
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
@fragment
|
|
211
|
-
fn fs_main(input: VertexOutput) -> FragOut {
|
|
212
|
-
let patternId = u32(uniforms.params.x + 0.5);
|
|
213
|
-
let spacing = max(2.0, uniforms.params.y);
|
|
214
|
-
let angle = uniforms.params.z;
|
|
215
|
-
let width = max(1.0, uniforms.params.w);
|
|
216
|
-
let angle2 = uniforms.params2.x;
|
|
217
|
-
|
|
218
|
-
let h = hatchIntensity(input.position.xy, patternId, spacing, angle, width, angle2);
|
|
219
|
-
// Per-polygon colour (a material-layer slab fills with its own
|
|
220
|
-
// IfcMaterial RGBA) overrides the uniform cap fill when present.
|
|
221
|
-
// Polygons without a colour carry the sentinel alpha −1 and fall back
|
|
222
|
-
// to the user's cap style, byte-identically. Hatch + stroke apply over
|
|
223
|
-
// whichever base is chosen, so the architectural hatch still works.
|
|
224
|
-
let useVertex = input.color.a >= 0.0;
|
|
225
|
-
let baseFill = select(uniforms.capFillColor, input.color, useVertex);
|
|
226
|
-
let rgb = mix(baseFill.rgb, uniforms.capStrokeColor.rgb, h * uniforms.capStrokeColor.a);
|
|
227
|
-
let a = max(baseFill.a, h * uniforms.capStrokeColor.a);
|
|
228
|
-
|
|
229
|
-
var out: FragOut;
|
|
230
|
-
out.color = vec4<f32>(rgb, a);
|
|
231
|
-
out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
|
-
`,
|
|
235
|
-
});
|
|
236
|
-
// Shader for lines (uniform color)
|
|
237
|
-
const lineShader = this.device.createShaderModule({
|
|
238
|
-
code: `
|
|
239
|
-
// Mirrors the fill shader's uniform layout so both pipelines can share
|
|
240
|
-
// one buffer; the line shader only reads viewProj, planeOffset and the
|
|
241
|
-
// appended lineColor (byte offset 144). capFillColor/… are unused here but
|
|
242
|
-
// declared for correct field offsets.
|
|
243
|
-
struct Uniforms {
|
|
244
|
-
viewProj: mat4x4<f32>,
|
|
245
|
-
planeOffset: vec4<f32>,
|
|
246
|
-
capFillColor: vec4<f32>,
|
|
247
|
-
capStrokeColor: vec4<f32>,
|
|
248
|
-
params: vec4<f32>,
|
|
249
|
-
params2: vec4<f32>,
|
|
250
|
-
lineColor: vec4<f32>,
|
|
251
|
-
}
|
|
252
|
-
@binding(0) @group(0) var<uniform> uniforms: Uniforms;
|
|
253
|
-
|
|
254
|
-
struct VertexInput {
|
|
255
|
-
@location(0) position: vec3<f32>,
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
struct VertexOutput {
|
|
259
|
-
@builtin(position) position: vec4<f32>,
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
@vertex
|
|
263
|
-
fn vs_main(input: VertexInput) -> VertexOutput {
|
|
264
|
-
var output: VertexOutput;
|
|
265
|
-
let offsetPos = input.position + uniforms.planeOffset.xyz;
|
|
266
|
-
let clip = uniforms.viewProj * vec4<f32>(offsetPos, 1.0);
|
|
267
|
-
// Reverse-Z decal nudge for lines coplanar with model faces
|
|
268
|
-
// (issue #812). WebGPU forbids depthStencil.depthBias on non-
|
|
269
|
-
// triangle topologies, so we do the equivalent in clip space:
|
|
270
|
-
// adding a small positive multiple of clip.w raises NDC z by a
|
|
271
|
-
// constant after the w-divide, which under reverse-Z means
|
|
272
|
-
// "slightly closer" — enough to beat MSAA jitter on annotation
|
|
273
|
-
// lines that ride exactly on a wall/floor.
|
|
274
|
-
output.position = vec4<f32>(clip.x, clip.y, clip.z + 5e-5 * clip.w, clip.w);
|
|
275
|
-
return output;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
struct FragOutLine {
|
|
279
|
-
@location(0) color: vec4<f32>,
|
|
280
|
-
@location(1) objectId: vec4<f32>,
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
@fragment
|
|
284
|
-
fn fs_main(input: VertexOutput) -> FragOutLine {
|
|
285
|
-
var out: FragOutLine;
|
|
286
|
-
out.color = uniforms.lineColor; // consumer-themeable (defaults black)
|
|
287
|
-
out.objectId = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
288
|
-
return out;
|
|
289
|
-
}
|
|
290
|
-
`,
|
|
291
|
-
});
|
|
108
|
+
const fillShader = this.device.createShaderModule({ code: SECTION_2D_CAP_FILL_WGSL });
|
|
109
|
+
const lineShader = this.device.createShaderModule({ code: SECTION_2D_OVERLAY_LINE_WGSL });
|
|
292
110
|
// Pipeline for filled polygons
|
|
293
111
|
this.fillPipeline = this.device.createRenderPipeline({
|
|
294
112
|
layout: pipelineLayout,
|
|
@@ -394,73 +212,48 @@ export class Section2DOverlayRenderer {
|
|
|
394
212
|
count: this.sampleCount,
|
|
395
213
|
},
|
|
396
214
|
});
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
// line shader reads viewProj/planeOffset + the appended lineColor (offset
|
|
407
|
-
// 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);
|
|
408
224
|
this.uniformBuffer = this.device.createBuffer({
|
|
409
|
-
size:
|
|
225
|
+
size: this.uniformStride * SECTION_2D_UNIFORM_SLOT_COUNT,
|
|
410
226
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
411
227
|
});
|
|
412
|
-
// 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.
|
|
413
231
|
this.bindGroup = this.device.createBindGroup({
|
|
414
232
|
layout: this.bindGroupLayout,
|
|
415
233
|
entries: [
|
|
416
|
-
{
|
|
234
|
+
{
|
|
235
|
+
binding: 0,
|
|
236
|
+
resource: { buffer: this.uniformBuffer, offset: 0, size: SECTION_2D_UNIFORM_BYTES },
|
|
237
|
+
},
|
|
417
238
|
],
|
|
418
239
|
});
|
|
419
240
|
this.initialized = true;
|
|
420
241
|
}
|
|
421
242
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* - Y axis (down): 2D (x, y) = 3D (x, z) - looking down at XZ plane
|
|
426
|
-
* - Z axis (front): 2D (x, y) = 3D (x, y) - looking along Z at XY plane
|
|
427
|
-
* - X axis (side): 2D (x, y) = 3D (z, y) - looking along X at ZY plane
|
|
428
|
-
* When flipped, the 2D x coordinate is negated.
|
|
429
|
-
*
|
|
430
|
-
* Custom-plane path (issue #243): when `customPlane` is supplied, the
|
|
431
|
-
* 3D point is `origin + tangent*x2d + bitangent*y2d`. The same basis
|
|
432
|
-
* is used by `SectionCutter` to project triangle-plane intersections
|
|
433
|
-
* to 2D, so the round-trip is exact and the cap polygons land
|
|
434
|
-
* 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.
|
|
435
246
|
*/
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
o[0] + t[0] * x2d + b[0] * y2d,
|
|
447
|
-
o[1] + t[1] * x2d + b[1] * y2d,
|
|
448
|
-
o[2] + t[2] * x2d + b[2] * y2d,
|
|
449
|
-
];
|
|
450
|
-
}
|
|
451
|
-
// Handle flipped - the 2D x coordinate was negated during projection
|
|
452
|
-
const x = flipped ? -x2d : x2d;
|
|
453
|
-
switch (axis) {
|
|
454
|
-
case 'down': // Y axis - horizontal cut (floor plan)
|
|
455
|
-
// 2D.x = 3D.x, 2D.y = 3D.z -> 3D (x, planeY, y)
|
|
456
|
-
return [x, planePosition, y2d];
|
|
457
|
-
case 'front': // Z axis - vertical cut (section view)
|
|
458
|
-
// 2D.x = 3D.x, 2D.y = 3D.y -> 3D (x, y, planeZ)
|
|
459
|
-
return [x, y2d, planePosition];
|
|
460
|
-
case 'side': // X axis - vertical cut (side elevation)
|
|
461
|
-
// 2D.x = 3D.z, 2D.y = 3D.y -> 3D (planeX, y, x)
|
|
462
|
-
return [planePosition, y2d, x];
|
|
463
|
-
}
|
|
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
|
+
};
|
|
464
257
|
}
|
|
465
258
|
/**
|
|
466
259
|
* Upload 2D drawing data to GPU buffers.
|
|
@@ -477,113 +270,30 @@ export class Section2DOverlayRenderer {
|
|
|
477
270
|
*/
|
|
478
271
|
uploadDrawing(polygons, lines, axis, planePosition, flipped = false, customPlane) {
|
|
479
272
|
this.init();
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
if (this.fillIndexBuffer) {
|
|
486
|
-
this.fillIndexBuffer.destroy();
|
|
487
|
-
this.fillIndexBuffer = null;
|
|
488
|
-
}
|
|
489
|
-
if (this.lineVertexBuffer) {
|
|
490
|
-
this.lineVertexBuffer.destroy();
|
|
491
|
-
this.lineVertexBuffer = null;
|
|
492
|
-
}
|
|
493
|
-
this.fillIndexCount = 0;
|
|
494
|
-
this.lineVertexCount = 0;
|
|
495
|
-
// Build fill geometry (triangulated polygons)
|
|
496
|
-
const fillVertices = [];
|
|
497
|
-
const fillIndices = [];
|
|
498
|
-
let vertexOffset = 0;
|
|
499
|
-
for (const polygon of polygons) {
|
|
500
|
-
const outer = polygon.polygon.outer;
|
|
501
|
-
if (outer.length < 3)
|
|
502
|
-
continue;
|
|
503
|
-
// Per-polygon fill colour. A material-layer wall/slab delivers one polygon
|
|
504
|
-
// per layer, each carrying its IfcMaterial RGBA (window frame/glass parts
|
|
505
|
-
// likewise). Polygons WITHOUT a colour use the sentinel alpha −1 so the
|
|
506
|
-
// fill shader falls back to the uniform cap style (architectural fill +
|
|
507
|
-
// hatch) byte-identically — see fs_main.
|
|
508
|
-
const color = polygon.color ?? [0, 0, 0, -1];
|
|
509
|
-
// Hole-aware ear-clipping (reused from the IfcAnnotationFillArea fill path)
|
|
510
|
-
// replaces the old convex fan. The fan ignored holes and inverted on the
|
|
511
|
-
// CONCAVE cross-sections that arbitrary IFC profiles (and material-layer
|
|
512
|
-
// slabs) cut into, leaving the cut face uncovered — it read as a hollow
|
|
513
|
-
// shell. Section 2D points are (x, y); the triangulator works in (x, z),
|
|
514
|
-
// so y maps to z.
|
|
515
|
-
const outerRing = outer.map((p) => ({ x: p.x, z: p.y }));
|
|
516
|
-
const holeRings = polygon.polygon.holes
|
|
517
|
-
.filter((h) => h.length >= 3)
|
|
518
|
-
.map((h) => h.map((p) => ({ x: p.x, z: p.y })));
|
|
519
|
-
const stitched = holeRings.length > 0 ? joinHoles(outerRing, holeRings) : outerRing;
|
|
520
|
-
const tris = earClip(stitched);
|
|
521
|
-
if (tris.length === 0)
|
|
522
|
-
continue;
|
|
523
|
-
const baseVertex = vertexOffset;
|
|
524
|
-
for (const pt of stitched) {
|
|
525
|
-
const [x3d, y3d, z3d] = this.transform2Dto3D(pt.x, pt.z, axis, planePosition, flipped, customPlane);
|
|
526
|
-
fillVertices.push(x3d, y3d, z3d, color[0], color[1], color[2], color[3]);
|
|
527
|
-
vertexOffset++;
|
|
528
|
-
}
|
|
529
|
-
for (const [a, b, c] of tris) {
|
|
530
|
-
fillIndices.push(baseVertex + a, baseVertex + b, baseVertex + c);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// Create fill buffers
|
|
534
|
-
if (fillVertices.length > 0) {
|
|
535
|
-
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) {
|
|
536
277
|
this.fillVertexBuffer = this.device.createBuffer({
|
|
537
|
-
size:
|
|
278
|
+
size: fill.vertices.byteLength,
|
|
538
279
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
539
280
|
});
|
|
540
|
-
this.device.queue.writeBuffer(this.fillVertexBuffer, 0,
|
|
541
|
-
const fillIndexData = new Uint32Array(fillIndices);
|
|
281
|
+
this.device.queue.writeBuffer(this.fillVertexBuffer, 0, fill.vertices);
|
|
542
282
|
this.fillIndexBuffer = this.device.createBuffer({
|
|
543
|
-
size:
|
|
283
|
+
size: fill.indices.byteLength,
|
|
544
284
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
545
285
|
});
|
|
546
|
-
this.device.queue.writeBuffer(this.fillIndexBuffer, 0,
|
|
547
|
-
this.fillIndexCount =
|
|
548
|
-
}
|
|
549
|
-
// Build line geometry
|
|
550
|
-
const lineVertices = [];
|
|
551
|
-
// Polygon outlines
|
|
552
|
-
for (const polygon of polygons) {
|
|
553
|
-
const outer = polygon.polygon.outer;
|
|
554
|
-
for (let i = 0; i < outer.length; i++) {
|
|
555
|
-
const p1 = outer[i];
|
|
556
|
-
const p2 = outer[(i + 1) % outer.length];
|
|
557
|
-
const [x1, y1, z1] = this.transform2Dto3D(p1.x, p1.y, axis, planePosition, flipped, customPlane);
|
|
558
|
-
const [x2, y2, z2] = this.transform2Dto3D(p2.x, p2.y, axis, planePosition, flipped, customPlane);
|
|
559
|
-
lineVertices.push(x1, y1, z1, x2, y2, z2);
|
|
560
|
-
}
|
|
561
|
-
// Hole outlines
|
|
562
|
-
for (const hole of polygon.polygon.holes) {
|
|
563
|
-
for (let i = 0; i < hole.length; i++) {
|
|
564
|
-
const p1 = hole[i];
|
|
565
|
-
const p2 = hole[(i + 1) % hole.length];
|
|
566
|
-
const [x1, y1, z1] = this.transform2Dto3D(p1.x, p1.y, axis, planePosition, flipped, customPlane);
|
|
567
|
-
const [x2, y2, z2] = this.transform2Dto3D(p2.x, p2.y, axis, planePosition, flipped, customPlane);
|
|
568
|
-
lineVertices.push(x1, y1, z1, x2, y2, z2);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
// Additional drawing lines (hatching, etc.)
|
|
573
|
-
for (const line of lines) {
|
|
574
|
-
const [x1, y1, z1] = this.transform2Dto3D(line.line.start.x, line.line.start.y, axis, planePosition, flipped, customPlane);
|
|
575
|
-
const [x2, y2, z2] = this.transform2Dto3D(line.line.end.x, line.line.end.y, axis, planePosition, flipped, customPlane);
|
|
576
|
-
lineVertices.push(x1, y1, z1, x2, y2, z2);
|
|
286
|
+
this.device.queue.writeBuffer(this.fillIndexBuffer, 0, fill.indices);
|
|
287
|
+
this.fillIndexCount = fill.indices.length;
|
|
577
288
|
}
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
const lineVertexData = new Float32Array(lineVertices);
|
|
289
|
+
const outline = buildDrawingOutlineVertices(polygons, lines, lift);
|
|
290
|
+
if (outline) {
|
|
581
291
|
this.lineVertexBuffer = this.device.createBuffer({
|
|
582
|
-
size:
|
|
292
|
+
size: outline.byteLength,
|
|
583
293
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
584
294
|
});
|
|
585
|
-
this.device.queue.writeBuffer(this.lineVertexBuffer, 0,
|
|
586
|
-
this.lineVertexCount =
|
|
295
|
+
this.device.queue.writeBuffer(this.lineVertexBuffer, 0, outline);
|
|
296
|
+
this.lineVertexCount = outline.length / 3; // Each vertex is 3 floats
|
|
587
297
|
}
|
|
588
298
|
}
|
|
589
299
|
/**
|
|
@@ -624,29 +334,27 @@ export class Section2DOverlayRenderer {
|
|
|
624
334
|
*/
|
|
625
335
|
uploadAnnotationLines3D(vertices) {
|
|
626
336
|
this.init();
|
|
627
|
-
|
|
628
|
-
this.annotationLineVertexBuffer.destroy();
|
|
629
|
-
this.annotationLineVertexBuffer = null;
|
|
630
|
-
}
|
|
631
|
-
this.annotationLineVertexCount = 0;
|
|
632
|
-
if (vertices.length < 6)
|
|
633
|
-
return;
|
|
634
|
-
this.annotationLineVertexBuffer = this.device.createBuffer({
|
|
635
|
-
size: vertices.byteLength,
|
|
636
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
637
|
-
});
|
|
638
|
-
this.device.queue.writeBuffer(this.annotationLineVertexBuffer, 0, vertices);
|
|
639
|
-
this.annotationLineVertexCount = vertices.length / 3;
|
|
337
|
+
this.annotationLines.upload(this.device, vertices);
|
|
640
338
|
}
|
|
641
339
|
clearAnnotationLines3D() {
|
|
642
|
-
|
|
643
|
-
this.annotationLineVertexBuffer.destroy();
|
|
644
|
-
this.annotationLineVertexBuffer = null;
|
|
645
|
-
}
|
|
646
|
-
this.annotationLineVertexCount = 0;
|
|
340
|
+
this.annotationLines.clear();
|
|
647
341
|
}
|
|
648
342
|
hasAnnotationLines3D() {
|
|
649
|
-
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);
|
|
650
358
|
}
|
|
651
359
|
/**
|
|
652
360
|
* Upload a flat Float32Array of 3D line-list vertices for the alignment
|
|
@@ -656,55 +364,13 @@ export class Section2DOverlayRenderer {
|
|
|
656
364
|
*/
|
|
657
365
|
uploadAlignmentLines3D(vertices) {
|
|
658
366
|
this.init();
|
|
659
|
-
|
|
660
|
-
this.alignmentLineVertexBuffer.destroy();
|
|
661
|
-
this.alignmentLineVertexBuffer = null;
|
|
662
|
-
}
|
|
663
|
-
this.alignmentLineVertexCount = 0;
|
|
664
|
-
if (vertices.length < 6)
|
|
665
|
-
return;
|
|
666
|
-
this.alignmentLineVertexBuffer = this.device.createBuffer({
|
|
667
|
-
size: vertices.byteLength,
|
|
668
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
669
|
-
});
|
|
670
|
-
this.device.queue.writeBuffer(this.alignmentLineVertexBuffer, 0, vertices);
|
|
671
|
-
this.alignmentLineVertexCount = vertices.length / 3;
|
|
367
|
+
this.alignmentLines.upload(this.device, vertices);
|
|
672
368
|
}
|
|
673
369
|
clearAlignmentLines3D() {
|
|
674
|
-
|
|
675
|
-
this.alignmentLineVertexBuffer.destroy();
|
|
676
|
-
this.alignmentLineVertexBuffer = null;
|
|
677
|
-
}
|
|
678
|
-
this.alignmentLineVertexCount = 0;
|
|
370
|
+
this.alignmentLines.clear();
|
|
679
371
|
}
|
|
680
372
|
hasAlignmentLines3D() {
|
|
681
|
-
return this.
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* Draw the standalone annotation line overlay. Uses the same line pipeline
|
|
685
|
-
* as the section cut outlines (vertex format: 3 floats per vertex, line-list
|
|
686
|
-
* topology) but reads from a separate vertex buffer. The pipeline's
|
|
687
|
-
* `planeOffset` uniform is zeroed so vertices render at their authored
|
|
688
|
-
* world position.
|
|
689
|
-
*/
|
|
690
|
-
drawAnnotationLines3D(pass, viewProj) {
|
|
691
|
-
this.init();
|
|
692
|
-
if (!this.linePipeline || !this.uniformBuffer || !this.bindGroup)
|
|
693
|
-
return;
|
|
694
|
-
if (!this.annotationLineVertexBuffer || this.annotationLineVertexCount === 0)
|
|
695
|
-
return;
|
|
696
|
-
// Reuse the existing fill uniform buffer slot 0 (viewProj + planeOffset).
|
|
697
|
-
// The line shader only reads those two fields, so the fill/cap-style
|
|
698
|
-
// tail of the uniforms is harmless leftover data.
|
|
699
|
-
const uniforms = new Float32Array(40);
|
|
700
|
-
uniforms.set(viewProj, 0);
|
|
701
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
702
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
703
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
704
|
-
pass.setPipeline(this.linePipeline);
|
|
705
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
706
|
-
pass.setVertexBuffer(0, this.annotationLineVertexBuffer);
|
|
707
|
-
pass.draw(this.annotationLineVertexCount);
|
|
373
|
+
return this.alignmentLines.has();
|
|
708
374
|
}
|
|
709
375
|
/**
|
|
710
376
|
* Draw the alignment centerline overlay. Identical pipeline/uniform setup as
|
|
@@ -712,19 +378,10 @@ export class Section2DOverlayRenderer {
|
|
|
712
378
|
*/
|
|
713
379
|
drawAlignmentLines3D(pass, viewProj) {
|
|
714
380
|
this.init();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (!this.alignmentLineVertexBuffer || this.alignmentLineVertexCount === 0)
|
|
381
|
+
const resources = this.lineResources();
|
|
382
|
+
if (!resources)
|
|
718
383
|
return;
|
|
719
|
-
|
|
720
|
-
uniforms.set(viewProj, 0);
|
|
721
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
722
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
723
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
724
|
-
pass.setPipeline(this.linePipeline);
|
|
725
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
726
|
-
pass.setVertexBuffer(0, this.alignmentLineVertexBuffer);
|
|
727
|
-
pass.draw(this.alignmentLineVertexCount);
|
|
384
|
+
this.alignmentLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
728
385
|
}
|
|
729
386
|
/**
|
|
730
387
|
* Upload structural-grid (IfcGridAxis) segments as a flat
|
|
@@ -734,29 +391,13 @@ export class Section2DOverlayRenderer {
|
|
|
734
391
|
*/
|
|
735
392
|
uploadGridLines3D(vertices) {
|
|
736
393
|
this.init();
|
|
737
|
-
|
|
738
|
-
this.gridLineVertexBuffer.destroy();
|
|
739
|
-
this.gridLineVertexBuffer = null;
|
|
740
|
-
}
|
|
741
|
-
this.gridLineVertexCount = 0;
|
|
742
|
-
if (vertices.length < 6)
|
|
743
|
-
return;
|
|
744
|
-
this.gridLineVertexBuffer = this.device.createBuffer({
|
|
745
|
-
size: vertices.byteLength,
|
|
746
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
747
|
-
});
|
|
748
|
-
this.device.queue.writeBuffer(this.gridLineVertexBuffer, 0, vertices);
|
|
749
|
-
this.gridLineVertexCount = vertices.length / 3;
|
|
394
|
+
this.gridLines.upload(this.device, vertices);
|
|
750
395
|
}
|
|
751
396
|
clearGridLines3D() {
|
|
752
|
-
|
|
753
|
-
this.gridLineVertexBuffer.destroy();
|
|
754
|
-
this.gridLineVertexBuffer = null;
|
|
755
|
-
}
|
|
756
|
-
this.gridLineVertexCount = 0;
|
|
397
|
+
this.gridLines.clear();
|
|
757
398
|
}
|
|
758
399
|
hasGridLines3D() {
|
|
759
|
-
return this.
|
|
400
|
+
return this.gridLines.has();
|
|
760
401
|
}
|
|
761
402
|
/**
|
|
762
403
|
* Draw the structural-grid overlay. Identical pipeline/uniform setup as
|
|
@@ -764,19 +405,38 @@ export class Section2DOverlayRenderer {
|
|
|
764
405
|
*/
|
|
765
406
|
drawGridLines3D(pass, viewProj) {
|
|
766
407
|
this.init();
|
|
767
|
-
|
|
408
|
+
const resources = this.lineResources();
|
|
409
|
+
if (!resources)
|
|
768
410
|
return;
|
|
769
|
-
|
|
411
|
+
this.gridLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Upload the DXF reference-layer's line paths as a flat `[x,y,z, x,y,z, …]`
|
|
415
|
+
* line-list in world space (issue #2043). Mirrors `uploadGridLines3D` with
|
|
416
|
+
* a separate buffer so 3D DXF visibility is independent of the 2D
|
|
417
|
+
* underlay and the other overlays above. Pass an empty array (or omit)
|
|
418
|
+
* to clear.
|
|
419
|
+
*/
|
|
420
|
+
uploadDxfLines3D(vertices) {
|
|
421
|
+
this.init();
|
|
422
|
+
this.dxfLines.upload(this.device, vertices);
|
|
423
|
+
}
|
|
424
|
+
clearDxfLines3D() {
|
|
425
|
+
this.dxfLines.clear();
|
|
426
|
+
}
|
|
427
|
+
hasDxfLines3D() {
|
|
428
|
+
return this.dxfLines.has();
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Draw the 3D DXF reference-layer overlay. Identical pipeline/uniform
|
|
432
|
+
* setup as `drawGridLines3D`, reading from the separate DXF buffer.
|
|
433
|
+
*/
|
|
434
|
+
drawDxfLines3D(pass, viewProj) {
|
|
435
|
+
this.init();
|
|
436
|
+
const resources = this.lineResources();
|
|
437
|
+
if (!resources)
|
|
770
438
|
return;
|
|
771
|
-
|
|
772
|
-
uniforms.set(viewProj, 0);
|
|
773
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
774
|
-
uniforms.set(this.overlayLineColor, 36); // lineColor (byte offset 144)
|
|
775
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
776
|
-
pass.setPipeline(this.linePipeline);
|
|
777
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
778
|
-
pass.setVertexBuffer(0, this.gridLineVertexBuffer);
|
|
779
|
-
pass.draw(this.gridLineVertexCount);
|
|
439
|
+
this.dxfLines.draw(pass, resources, viewProj, this.overlayLineColor);
|
|
780
440
|
}
|
|
781
441
|
/** Colour for the clash-overlap box (its own, not the shared overlay colour). */
|
|
782
442
|
setClashBoxLineColor(color) {
|
|
@@ -789,46 +449,21 @@ export class Section2DOverlayRenderer {
|
|
|
789
449
|
*/
|
|
790
450
|
uploadClashBoxLines3D(vertices) {
|
|
791
451
|
this.init();
|
|
792
|
-
|
|
793
|
-
this.clashBoxLineVertexBuffer.destroy();
|
|
794
|
-
this.clashBoxLineVertexBuffer = null;
|
|
795
|
-
}
|
|
796
|
-
this.clashBoxLineVertexCount = 0;
|
|
797
|
-
if (vertices.length < 6)
|
|
798
|
-
return;
|
|
799
|
-
this.clashBoxLineVertexBuffer = this.device.createBuffer({
|
|
800
|
-
size: vertices.byteLength,
|
|
801
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
802
|
-
});
|
|
803
|
-
this.device.queue.writeBuffer(this.clashBoxLineVertexBuffer, 0, vertices);
|
|
804
|
-
this.clashBoxLineVertexCount = vertices.length / 3;
|
|
452
|
+
this.clashBoxLines.upload(this.device, vertices);
|
|
805
453
|
}
|
|
806
454
|
clearClashBoxLines3D() {
|
|
807
|
-
|
|
808
|
-
this.clashBoxLineVertexBuffer.destroy();
|
|
809
|
-
this.clashBoxLineVertexBuffer = null;
|
|
810
|
-
}
|
|
811
|
-
this.clashBoxLineVertexCount = 0;
|
|
455
|
+
this.clashBoxLines.clear();
|
|
812
456
|
}
|
|
813
457
|
hasClashBoxLines3D() {
|
|
814
|
-
return this.
|
|
458
|
+
return this.clashBoxLines.has();
|
|
815
459
|
}
|
|
816
460
|
/** Draw the clash-overlap box in its own colour. Same line pipeline. (#1277) */
|
|
817
461
|
drawClashBoxLines3D(pass, viewProj) {
|
|
818
462
|
this.init();
|
|
819
|
-
|
|
463
|
+
const resources = this.lineResources();
|
|
464
|
+
if (!resources)
|
|
820
465
|
return;
|
|
821
|
-
|
|
822
|
-
return;
|
|
823
|
-
const uniforms = new Float32Array(40);
|
|
824
|
-
uniforms.set(viewProj, 0);
|
|
825
|
-
// planeOffset = 0 — vertices are already in world space.
|
|
826
|
-
uniforms.set(this.clashBoxLineColor, 36); // lineColor (byte offset 144)
|
|
827
|
-
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
|
|
828
|
-
pass.setPipeline(this.linePipeline);
|
|
829
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
830
|
-
pass.setVertexBuffer(0, this.clashBoxLineVertexBuffer);
|
|
831
|
-
pass.draw(this.clashBoxLineVertexCount);
|
|
466
|
+
this.clashBoxLines.draw(pass, resources, viewProj, this.clashBoxLineColor);
|
|
832
467
|
}
|
|
833
468
|
/**
|
|
834
469
|
* Check if there is geometry to draw
|
|
@@ -847,7 +482,7 @@ export class Section2DOverlayRenderer {
|
|
|
847
482
|
if (!this.hasGeometry()) {
|
|
848
483
|
return;
|
|
849
484
|
}
|
|
850
|
-
const {
|
|
485
|
+
const { viewProj } = options;
|
|
851
486
|
// No offset — cap renders exactly on the section plane. The previous
|
|
852
487
|
// 0.3m bias was there to keep the outline lines clear of below-plane
|
|
853
488
|
// geometry, but it made the cap visually drift off the slider plane
|
|
@@ -856,55 +491,43 @@ export class Section2DOverlayRenderer {
|
|
|
856
491
|
// coincident below-plane top faces is not an issue; the stencil gate
|
|
857
492
|
// keeps the fill restricted to the actual cap polygons.
|
|
858
493
|
const offset = [0, 0, 0];
|
|
859
|
-
// Update uniforms.
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
uniforms.
|
|
869
|
-
uniforms.set(this.overlayLineColor, 36); // section-cut outline colour
|
|
870
|
-
uniforms[16] = offset[0];
|
|
871
|
-
uniforms[17] = offset[1];
|
|
872
|
-
uniforms[18] = offset[2];
|
|
873
|
-
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;
|
|
874
504
|
const cs = options.capStyle;
|
|
875
505
|
if (cs) {
|
|
876
|
-
uniforms
|
|
877
|
-
uniforms
|
|
878
|
-
uniforms[
|
|
879
|
-
uniforms[
|
|
880
|
-
uniforms[
|
|
881
|
-
uniforms[
|
|
882
|
-
uniforms[
|
|
883
|
-
uniforms[27] = cs.strokeColor[3];
|
|
884
|
-
uniforms[28] = cs.patternId;
|
|
885
|
-
uniforms[29] = cs.spacingPx;
|
|
886
|
-
uniforms[30] = cs.angleRad;
|
|
887
|
-
uniforms[31] = cs.widthPx;
|
|
888
|
-
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;
|
|
889
513
|
}
|
|
890
514
|
else {
|
|
891
515
|
// Sensible defaults when caller omits style (e.g. legacy lines-only
|
|
892
516
|
// use): solid fill using a warm-paper colour, no hatch.
|
|
893
|
-
uniforms[
|
|
894
|
-
uniforms[
|
|
895
|
-
uniforms[
|
|
896
|
-
uniforms[
|
|
897
|
-
uniforms[
|
|
898
|
-
uniforms[
|
|
899
|
-
uniforms[
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
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);
|
|
908
531
|
// Filled polygons = the 3D section cap. Render them ONLY when the
|
|
909
532
|
// caller opts in (`showFills: true` + a capStyle). This replaces the
|
|
910
533
|
// old stencil-parity cap, which leaked hatch into empty sky on non-
|
|
@@ -917,7 +540,7 @@ export class Section2DOverlayRenderer {
|
|
|
917
540
|
this.fillIndexBuffer &&
|
|
918
541
|
this.fillIndexCount > 0) {
|
|
919
542
|
pass.setPipeline(this.fillPipeline);
|
|
920
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
543
|
+
pass.setBindGroup(0, this.bindGroup, [capOffset]);
|
|
921
544
|
pass.setVertexBuffer(0, this.fillVertexBuffer);
|
|
922
545
|
pass.setIndexBuffer(this.fillIndexBuffer, 'uint32');
|
|
923
546
|
pass.drawIndexed(this.fillIndexCount);
|
|
@@ -929,19 +552,26 @@ export class Section2DOverlayRenderer {
|
|
|
929
552
|
this.lineVertexBuffer &&
|
|
930
553
|
this.lineVertexCount > 0) {
|
|
931
554
|
pass.setPipeline(this.linePipeline);
|
|
932
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
555
|
+
pass.setBindGroup(0, this.bindGroup, [capOffset]);
|
|
933
556
|
pass.setVertexBuffer(0, this.lineVertexBuffer);
|
|
934
557
|
pass.draw(this.lineVertexCount);
|
|
935
558
|
}
|
|
936
559
|
}
|
|
937
560
|
/**
|
|
938
|
-
* 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.
|
|
939
567
|
*/
|
|
940
568
|
dispose() {
|
|
941
569
|
this.clearGeometry();
|
|
942
570
|
this.clearAnnotationLines3D();
|
|
943
571
|
this.clearAlignmentLines3D();
|
|
944
572
|
this.clearGridLines3D();
|
|
573
|
+
this.clearDxfLines3D();
|
|
574
|
+
this.clearClashBoxLines3D();
|
|
945
575
|
if (this.uniformBuffer) {
|
|
946
576
|
this.uniformBuffer.destroy();
|
|
947
577
|
this.uniformBuffer = null;
|