@ifc-lite/viewer 1.7.0 → 1.8.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/CHANGELOG.md +35 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CwcRxist.js} +1 -1
- package/dist/assets/index-7WoQ-qVC.css +1 -0
- package/dist/assets/{index-dgdgiQ9p.js → index-BSANf7-H.js} +20926 -17587
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-5LbrYh3R.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-CgpLtj1h.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -18
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +114 -81
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/Viewport.tsx +57 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +12 -4
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +113 -14
- package/src/hooks/useLens.ts +39 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/store/index.ts +14 -1
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -38
- package/src/store.ts +3 -0
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Revision cloud (scalloped border) path generation.
|
|
7
|
+
* Generates arc data for drawing cloud annotations on a canvas or SVG.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Point2D {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A single arc segment in the cloud border */
|
|
16
|
+
export interface CloudArc {
|
|
17
|
+
/** Start point of the arc */
|
|
18
|
+
start: Point2D;
|
|
19
|
+
/** End point of the arc */
|
|
20
|
+
end: Point2D;
|
|
21
|
+
/** Center of the arc circle */
|
|
22
|
+
center: Point2D;
|
|
23
|
+
/** Radius of the arc */
|
|
24
|
+
radius: number;
|
|
25
|
+
/** Start angle in radians */
|
|
26
|
+
startAngle: number;
|
|
27
|
+
/** End angle in radians */
|
|
28
|
+
endAngle: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate cloud arc data from two rectangle corner points.
|
|
33
|
+
* The two points define opposite corners of the rectangle.
|
|
34
|
+
* Arcs bulge outward from the rectangle edges.
|
|
35
|
+
*
|
|
36
|
+
* @param p1 First corner (e.g. top-left)
|
|
37
|
+
* @param p2 Second corner (e.g. bottom-right)
|
|
38
|
+
* @param arcRadius Radius of each scallop arc in drawing coords
|
|
39
|
+
* @returns Array of arc segments forming the cloud border
|
|
40
|
+
*/
|
|
41
|
+
export function generateCloudArcs(
|
|
42
|
+
p1: Point2D,
|
|
43
|
+
p2: Point2D,
|
|
44
|
+
arcRadius: number
|
|
45
|
+
): CloudArc[] {
|
|
46
|
+
// Build rectangle corners in clockwise order
|
|
47
|
+
const minX = Math.min(p1.x, p2.x);
|
|
48
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
49
|
+
const minY = Math.min(p1.y, p2.y);
|
|
50
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
51
|
+
|
|
52
|
+
const corners: Point2D[] = [
|
|
53
|
+
{ x: minX, y: minY }, // top-left (in drawing coords, Y increases downward on screen)
|
|
54
|
+
{ x: maxX, y: minY }, // top-right
|
|
55
|
+
{ x: maxX, y: maxY }, // bottom-right
|
|
56
|
+
{ x: minX, y: maxY }, // bottom-left
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const arcs: CloudArc[] = [];
|
|
60
|
+
|
|
61
|
+
// For each edge, generate scallop arcs
|
|
62
|
+
for (let i = 0; i < corners.length; i++) {
|
|
63
|
+
const edgeStart = corners[i];
|
|
64
|
+
const edgeEnd = corners[(i + 1) % corners.length];
|
|
65
|
+
|
|
66
|
+
const dx = edgeEnd.x - edgeStart.x;
|
|
67
|
+
const dy = edgeEnd.y - edgeStart.y;
|
|
68
|
+
const edgeLength = Math.sqrt(dx * dx + dy * dy);
|
|
69
|
+
|
|
70
|
+
if (edgeLength < 0.001) continue;
|
|
71
|
+
|
|
72
|
+
// Number of arcs along this edge
|
|
73
|
+
const arcDiameter = arcRadius * 2;
|
|
74
|
+
const arcCount = Math.max(1, Math.round(edgeLength / arcDiameter));
|
|
75
|
+
const segmentLength = edgeLength / arcCount;
|
|
76
|
+
const actualRadius = segmentLength / 2;
|
|
77
|
+
|
|
78
|
+
// Unit direction along edge
|
|
79
|
+
const ux = dx / edgeLength;
|
|
80
|
+
const uy = dy / edgeLength;
|
|
81
|
+
|
|
82
|
+
// Outward normal (perpendicular, pointing outward from rectangle)
|
|
83
|
+
// For clockwise winding, outward normal is to the right of the edge direction
|
|
84
|
+
const nx = uy;
|
|
85
|
+
const ny = -ux;
|
|
86
|
+
|
|
87
|
+
for (let j = 0; j < arcCount; j++) {
|
|
88
|
+
const t0 = j / arcCount;
|
|
89
|
+
const t1 = (j + 1) / arcCount;
|
|
90
|
+
|
|
91
|
+
const arcStart: Point2D = {
|
|
92
|
+
x: edgeStart.x + dx * t0,
|
|
93
|
+
y: edgeStart.y + dy * t0,
|
|
94
|
+
};
|
|
95
|
+
const arcEnd: Point2D = {
|
|
96
|
+
x: edgeStart.x + dx * t1,
|
|
97
|
+
y: edgeStart.y + dy * t1,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Center of the arc is offset outward from the midpoint
|
|
101
|
+
const midX = (arcStart.x + arcEnd.x) / 2;
|
|
102
|
+
const midY = (arcStart.y + arcEnd.y) / 2;
|
|
103
|
+
|
|
104
|
+
// The arc center is at the midpoint of the segment (on the edge)
|
|
105
|
+
// The arc bulges outward by the radius amount
|
|
106
|
+
const center: Point2D = {
|
|
107
|
+
x: midX,
|
|
108
|
+
y: midY,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Compute angles from center to start and end
|
|
112
|
+
const startAngle = Math.atan2(arcStart.y - center.y, arcStart.x - center.x);
|
|
113
|
+
const endAngle = Math.atan2(arcEnd.y - center.y, arcEnd.x - center.x);
|
|
114
|
+
|
|
115
|
+
arcs.push({
|
|
116
|
+
start: arcStart,
|
|
117
|
+
end: arcEnd,
|
|
118
|
+
center,
|
|
119
|
+
radius: actualRadius,
|
|
120
|
+
startAngle,
|
|
121
|
+
endAngle,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return arcs;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Draw cloud arcs on a Canvas 2D context.
|
|
131
|
+
* Arcs are drawn as semicircular bumps bulging outward.
|
|
132
|
+
*/
|
|
133
|
+
export function drawCloudOnCanvas(
|
|
134
|
+
ctx: CanvasRenderingContext2D,
|
|
135
|
+
p1: Point2D,
|
|
136
|
+
p2: Point2D,
|
|
137
|
+
arcRadius: number,
|
|
138
|
+
toScreenX: (x: number) => number,
|
|
139
|
+
toScreenY: (y: number) => number,
|
|
140
|
+
screenScale: number
|
|
141
|
+
): void {
|
|
142
|
+
const minX = Math.min(p1.x, p2.x);
|
|
143
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
144
|
+
const minY = Math.min(p1.y, p2.y);
|
|
145
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
146
|
+
|
|
147
|
+
const corners: Point2D[] = [
|
|
148
|
+
{ x: minX, y: minY },
|
|
149
|
+
{ x: maxX, y: minY },
|
|
150
|
+
{ x: maxX, y: maxY },
|
|
151
|
+
{ x: minX, y: maxY },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
ctx.beginPath();
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < corners.length; i++) {
|
|
157
|
+
const edgeStart = corners[i];
|
|
158
|
+
const edgeEnd = corners[(i + 1) % corners.length];
|
|
159
|
+
|
|
160
|
+
const dx = edgeEnd.x - edgeStart.x;
|
|
161
|
+
const dy = edgeEnd.y - edgeStart.y;
|
|
162
|
+
const edgeLength = Math.sqrt(dx * dx + dy * dy);
|
|
163
|
+
|
|
164
|
+
if (edgeLength < 0.001) continue;
|
|
165
|
+
|
|
166
|
+
const arcDiameter = arcRadius * 2;
|
|
167
|
+
const arcCount = Math.max(1, Math.round(edgeLength / arcDiameter));
|
|
168
|
+
const actualRadius = (edgeLength / arcCount) / 2;
|
|
169
|
+
|
|
170
|
+
// Unit direction along edge
|
|
171
|
+
const ux = dx / edgeLength;
|
|
172
|
+
const uy = dy / edgeLength;
|
|
173
|
+
|
|
174
|
+
// Outward normal for clockwise winding
|
|
175
|
+
const nx = uy;
|
|
176
|
+
const ny = -ux;
|
|
177
|
+
|
|
178
|
+
for (let j = 0; j < arcCount; j++) {
|
|
179
|
+
const t0 = j / arcCount;
|
|
180
|
+
const t1 = (j + 1) / arcCount;
|
|
181
|
+
|
|
182
|
+
const sx = edgeStart.x + dx * t0;
|
|
183
|
+
const sy = edgeStart.y + dy * t0;
|
|
184
|
+
const ex = edgeStart.x + dx * t1;
|
|
185
|
+
const ey = edgeStart.y + dy * t1;
|
|
186
|
+
|
|
187
|
+
// Arc center is on the edge at midpoint
|
|
188
|
+
const cx = (sx + ex) / 2;
|
|
189
|
+
const cy = (sy + ey) / 2;
|
|
190
|
+
|
|
191
|
+
// Convert to screen coords
|
|
192
|
+
const scx = toScreenX(cx);
|
|
193
|
+
const scy = toScreenY(cy);
|
|
194
|
+
const screenRadius = actualRadius * screenScale;
|
|
195
|
+
|
|
196
|
+
// Angles in screen space (Y may be flipped)
|
|
197
|
+
const ssx = toScreenX(sx);
|
|
198
|
+
const ssy = toScreenY(sy);
|
|
199
|
+
const sex = toScreenX(ex);
|
|
200
|
+
const sey = toScreenY(ey);
|
|
201
|
+
|
|
202
|
+
const startAngle = Math.atan2(ssy - scy, ssx - scx);
|
|
203
|
+
const endAngle = Math.atan2(sey - scy, sex - scx);
|
|
204
|
+
|
|
205
|
+
// Draw arc clockwise (false) so the semicircle bulges outward from the rectangle
|
|
206
|
+
ctx.arc(scx, scy, screenRadius, startAngle, endAngle, false);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ctx.closePath();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Generate SVG path data for a cloud annotation.
|
|
215
|
+
*/
|
|
216
|
+
export function generateCloudSVGPath(
|
|
217
|
+
p1: Point2D,
|
|
218
|
+
p2: Point2D,
|
|
219
|
+
arcRadius: number,
|
|
220
|
+
transformX: (x: number) => number,
|
|
221
|
+
transformY: (y: number) => number,
|
|
222
|
+
): string {
|
|
223
|
+
const minX = Math.min(p1.x, p2.x);
|
|
224
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
225
|
+
const minY = Math.min(p1.y, p2.y);
|
|
226
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
227
|
+
|
|
228
|
+
const corners: Point2D[] = [
|
|
229
|
+
{ x: minX, y: minY },
|
|
230
|
+
{ x: maxX, y: minY },
|
|
231
|
+
{ x: maxX, y: maxY },
|
|
232
|
+
{ x: minX, y: maxY },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
let path = '';
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < corners.length; i++) {
|
|
238
|
+
const edgeStart = corners[i];
|
|
239
|
+
const edgeEnd = corners[(i + 1) % corners.length];
|
|
240
|
+
|
|
241
|
+
const dx = edgeEnd.x - edgeStart.x;
|
|
242
|
+
const dy = edgeEnd.y - edgeStart.y;
|
|
243
|
+
const edgeLength = Math.sqrt(dx * dx + dy * dy);
|
|
244
|
+
|
|
245
|
+
if (edgeLength < 0.001) continue;
|
|
246
|
+
|
|
247
|
+
const arcDiameter = arcRadius * 2;
|
|
248
|
+
const arcCount = Math.max(1, Math.round(edgeLength / arcDiameter));
|
|
249
|
+
const segmentLength = edgeLength / arcCount;
|
|
250
|
+
const r = segmentLength / 2;
|
|
251
|
+
|
|
252
|
+
for (let j = 0; j < arcCount; j++) {
|
|
253
|
+
const t0 = j / arcCount;
|
|
254
|
+
const t1 = (j + 1) / arcCount;
|
|
255
|
+
|
|
256
|
+
const sx = transformX(edgeStart.x + (edgeEnd.x - edgeStart.x) * t0);
|
|
257
|
+
const sy = transformY(edgeStart.y + (edgeEnd.y - edgeStart.y) * t0);
|
|
258
|
+
const ex = transformX(edgeStart.x + (edgeEnd.x - edgeStart.x) * t1);
|
|
259
|
+
const ey = transformY(edgeStart.y + (edgeEnd.y - edgeStart.y) * t1);
|
|
260
|
+
|
|
261
|
+
// Move to start of first arc
|
|
262
|
+
if (i === 0 && j === 0) {
|
|
263
|
+
path += `M ${sx.toFixed(4)} ${sy.toFixed(4)}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// SVG arc: A rx ry x-rotation large-arc-flag sweep-flag x y
|
|
267
|
+
// sweep-flag=1 for clockwise (outward bulge from rectangle)
|
|
268
|
+
const trR = Math.sqrt((ex - sx) ** 2 + (ey - sy) ** 2) / 2;
|
|
269
|
+
path += ` A ${trR.toFixed(4)} ${trR.toFixed(4)} 0 0 1 ${ex.toFixed(4)} ${ey.toFixed(4)}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
path += ' Z';
|
|
274
|
+
return path;
|
|
275
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import {
|
|
8
|
+
computePolygonArea,
|
|
9
|
+
computePolygonPerimeter,
|
|
10
|
+
computePolygonCentroid,
|
|
11
|
+
formatArea,
|
|
12
|
+
} from './computePolygonArea.js';
|
|
13
|
+
|
|
14
|
+
describe('computePolygonArea', () => {
|
|
15
|
+
it('returns 0 for fewer than 3 points', () => {
|
|
16
|
+
assert.strictEqual(computePolygonArea([]), 0);
|
|
17
|
+
assert.strictEqual(computePolygonArea([{ x: 0, y: 0 }]), 0);
|
|
18
|
+
assert.strictEqual(computePolygonArea([{ x: 0, y: 0 }, { x: 1, y: 0 }]), 0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('computes area of a unit square', () => {
|
|
22
|
+
const square = [
|
|
23
|
+
{ x: 0, y: 0 },
|
|
24
|
+
{ x: 1, y: 0 },
|
|
25
|
+
{ x: 1, y: 1 },
|
|
26
|
+
{ x: 0, y: 1 },
|
|
27
|
+
];
|
|
28
|
+
assert.strictEqual(computePolygonArea(square), 1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('computes area of a right triangle', () => {
|
|
32
|
+
const triangle = [
|
|
33
|
+
{ x: 0, y: 0 },
|
|
34
|
+
{ x: 4, y: 0 },
|
|
35
|
+
{ x: 0, y: 3 },
|
|
36
|
+
];
|
|
37
|
+
assert.strictEqual(computePolygonArea(triangle), 6);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('computes area of a rectangle', () => {
|
|
41
|
+
const rect = [
|
|
42
|
+
{ x: 0, y: 0 },
|
|
43
|
+
{ x: 5, y: 0 },
|
|
44
|
+
{ x: 5, y: 3 },
|
|
45
|
+
{ x: 0, y: 3 },
|
|
46
|
+
];
|
|
47
|
+
assert.strictEqual(computePolygonArea(rect), 15);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns positive area regardless of winding direction', () => {
|
|
51
|
+
// Counter-clockwise
|
|
52
|
+
const ccw = [
|
|
53
|
+
{ x: 0, y: 0 },
|
|
54
|
+
{ x: 0, y: 2 },
|
|
55
|
+
{ x: 2, y: 2 },
|
|
56
|
+
{ x: 2, y: 0 },
|
|
57
|
+
];
|
|
58
|
+
assert.strictEqual(computePolygonArea(ccw), 4);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('computes area of an irregular polygon', () => {
|
|
62
|
+
// L-shaped polygon (6 vertices)
|
|
63
|
+
const lShape = [
|
|
64
|
+
{ x: 0, y: 0 },
|
|
65
|
+
{ x: 3, y: 0 },
|
|
66
|
+
{ x: 3, y: 1 },
|
|
67
|
+
{ x: 1, y: 1 },
|
|
68
|
+
{ x: 1, y: 2 },
|
|
69
|
+
{ x: 0, y: 2 },
|
|
70
|
+
];
|
|
71
|
+
// Area: 3*1 + 1*1 = 4
|
|
72
|
+
assert.strictEqual(computePolygonArea(lShape), 4);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('computePolygonPerimeter', () => {
|
|
77
|
+
it('returns 0 for fewer than 2 points', () => {
|
|
78
|
+
assert.strictEqual(computePolygonPerimeter([]), 0);
|
|
79
|
+
assert.strictEqual(computePolygonPerimeter([{ x: 0, y: 0 }]), 0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('computes perimeter of a unit square', () => {
|
|
83
|
+
const square = [
|
|
84
|
+
{ x: 0, y: 0 },
|
|
85
|
+
{ x: 1, y: 0 },
|
|
86
|
+
{ x: 1, y: 1 },
|
|
87
|
+
{ x: 0, y: 1 },
|
|
88
|
+
];
|
|
89
|
+
assert.strictEqual(computePolygonPerimeter(square), 4);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('computes perimeter of a 3-4-5 right triangle', () => {
|
|
93
|
+
const triangle = [
|
|
94
|
+
{ x: 0, y: 0 },
|
|
95
|
+
{ x: 4, y: 0 },
|
|
96
|
+
{ x: 0, y: 3 },
|
|
97
|
+
];
|
|
98
|
+
assert.strictEqual(computePolygonPerimeter(triangle), 12);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('computes perimeter of a rectangle', () => {
|
|
102
|
+
const rect = [
|
|
103
|
+
{ x: 0, y: 0 },
|
|
104
|
+
{ x: 5, y: 0 },
|
|
105
|
+
{ x: 5, y: 3 },
|
|
106
|
+
{ x: 0, y: 3 },
|
|
107
|
+
];
|
|
108
|
+
assert.strictEqual(computePolygonPerimeter(rect), 16);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('computePolygonCentroid', () => {
|
|
113
|
+
it('returns origin for empty polygon', () => {
|
|
114
|
+
const c = computePolygonCentroid([]);
|
|
115
|
+
assert.strictEqual(c.x, 0);
|
|
116
|
+
assert.strictEqual(c.y, 0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('returns the point for a single point', () => {
|
|
120
|
+
const c = computePolygonCentroid([{ x: 3, y: 7 }]);
|
|
121
|
+
assert.strictEqual(c.x, 3);
|
|
122
|
+
assert.strictEqual(c.y, 7);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('computes centroid of a unit square at origin', () => {
|
|
126
|
+
const square = [
|
|
127
|
+
{ x: 0, y: 0 },
|
|
128
|
+
{ x: 2, y: 0 },
|
|
129
|
+
{ x: 2, y: 2 },
|
|
130
|
+
{ x: 0, y: 2 },
|
|
131
|
+
];
|
|
132
|
+
const c = computePolygonCentroid(square);
|
|
133
|
+
assert.strictEqual(c.x, 1);
|
|
134
|
+
assert.strictEqual(c.y, 1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('computes centroid of a triangle', () => {
|
|
138
|
+
const triangle = [
|
|
139
|
+
{ x: 0, y: 0 },
|
|
140
|
+
{ x: 6, y: 0 },
|
|
141
|
+
{ x: 0, y: 6 },
|
|
142
|
+
];
|
|
143
|
+
const c = computePolygonCentroid(triangle);
|
|
144
|
+
assert.strictEqual(c.x, 2);
|
|
145
|
+
assert.strictEqual(c.y, 2);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('formatArea', () => {
|
|
150
|
+
it('formats small areas in cm²', () => {
|
|
151
|
+
assert.strictEqual(formatArea(0.005), '50.0 cm²');
|
|
152
|
+
assert.strictEqual(formatArea(0.001), '10.0 cm²');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('formats medium areas in m²', () => {
|
|
156
|
+
assert.strictEqual(formatArea(1), '1.00 m²');
|
|
157
|
+
assert.strictEqual(formatArea(25.5), '25.50 m²');
|
|
158
|
+
assert.strictEqual(formatArea(100), '100.00 m²');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('formats large areas in hectares', () => {
|
|
162
|
+
assert.strictEqual(formatArea(10000), '1.00 ha');
|
|
163
|
+
assert.strictEqual(formatArea(50000), '5.00 ha');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Polygon area and perimeter computation utilities for 2D annotations.
|
|
7
|
+
* Uses the shoelace formula for area calculation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Point2D {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute the signed area of a simple polygon using the shoelace formula.
|
|
17
|
+
* Returns absolute value (always positive).
|
|
18
|
+
*/
|
|
19
|
+
export function computePolygonArea(points: Point2D[]): number {
|
|
20
|
+
if (points.length < 3) return 0;
|
|
21
|
+
let area = 0;
|
|
22
|
+
const n = points.length;
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const j = (i + 1) % n;
|
|
25
|
+
area += points[i].x * points[j].y;
|
|
26
|
+
area -= points[j].x * points[i].y;
|
|
27
|
+
}
|
|
28
|
+
return Math.abs(area) / 2;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compute the perimeter of a closed polygon.
|
|
33
|
+
*/
|
|
34
|
+
export function computePolygonPerimeter(points: Point2D[]): number {
|
|
35
|
+
if (points.length < 2) return 0;
|
|
36
|
+
let perimeter = 0;
|
|
37
|
+
const n = points.length;
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const j = (i + 1) % n;
|
|
40
|
+
const dx = points[j].x - points[i].x;
|
|
41
|
+
const dy = points[j].y - points[i].y;
|
|
42
|
+
perimeter += Math.sqrt(dx * dx + dy * dy);
|
|
43
|
+
}
|
|
44
|
+
return perimeter;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compute the centroid (geometric center) of a polygon.
|
|
49
|
+
*/
|
|
50
|
+
export function computePolygonCentroid(points: Point2D[]): Point2D {
|
|
51
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
52
|
+
let cx = 0;
|
|
53
|
+
let cy = 0;
|
|
54
|
+
for (const p of points) {
|
|
55
|
+
cx += p.x;
|
|
56
|
+
cy += p.y;
|
|
57
|
+
}
|
|
58
|
+
return { x: cx / points.length, y: cy / points.length };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format an area value for display with appropriate units.
|
|
63
|
+
*/
|
|
64
|
+
export function formatArea(squareMeters: number): string {
|
|
65
|
+
if (squareMeters < 0.01) {
|
|
66
|
+
return `${(squareMeters * 10000).toFixed(1)} cm²`;
|
|
67
|
+
} else if (squareMeters < 10000) {
|
|
68
|
+
return `${squareMeters.toFixed(2)} m²`;
|
|
69
|
+
} else {
|
|
70
|
+
return `${(squareMeters / 10000).toFixed(2)} ha`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -372,10 +372,12 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
372
372
|
prevIsStreamingRef.current = isStreaming;
|
|
373
373
|
}, [isStreaming, isInitialized]);
|
|
374
374
|
|
|
375
|
-
// Apply pending color updates
|
|
376
|
-
//
|
|
375
|
+
// Apply pending color updates as overlay batches (lens coloring).
|
|
376
|
+
// Uses scene.setColorOverrides() which builds overlay batches rendered on top
|
|
377
|
+
// of original geometry via depthCompare 'equal'. Original batches are NEVER
|
|
378
|
+
// modified, so clearing lens is instant (no batch rebuild).
|
|
377
379
|
useEffect(() => {
|
|
378
|
-
if (
|
|
380
|
+
if (pendingColorUpdates === null) return;
|
|
379
381
|
|
|
380
382
|
// Wait until viewport is initialized before applying color updates
|
|
381
383
|
if (!isInitialized) return;
|
|
@@ -388,7 +390,13 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
388
390
|
const scene = renderer.getScene();
|
|
389
391
|
|
|
390
392
|
if (device && pipeline) {
|
|
391
|
-
|
|
393
|
+
if (pendingColorUpdates.size === 0) {
|
|
394
|
+
// Empty map = clear overrides (lens deactivated)
|
|
395
|
+
scene.clearColorOverrides();
|
|
396
|
+
} else {
|
|
397
|
+
// Non-empty map = set color overrides
|
|
398
|
+
scene.setColorOverrides(pendingColorUpdates, device, pipeline);
|
|
399
|
+
}
|
|
392
400
|
renderer.render();
|
|
393
401
|
clearPendingColorUpdates();
|
|
394
402
|
}
|
|
@@ -331,7 +331,7 @@ export function buildReportHTML(report: IDSValidationReport, locale: SupportedLo
|
|
|
331
331
|
<thead>
|
|
332
332
|
<tr>
|
|
333
333
|
<th class="col-status" onclick="sortTable(${i}, 0)">Status <span class="sort-icon">▴▾</span></th>
|
|
334
|
-
<th class="col-type" onclick="sortTable(${i}, 1)">IFC
|
|
334
|
+
<th class="col-type" onclick="sortTable(${i}, 1)">IFC Class <span class="sort-icon">▴▾</span></th>
|
|
335
335
|
<th class="col-name" onclick="sortTable(${i}, 2)">Name <span class="sort-icon">▴▾</span></th>
|
|
336
336
|
<th class="col-globalid" onclick="sortTable(${i}, 3)">GlobalId <span class="sort-icon">▴▾</span></th>
|
|
337
337
|
<th class="col-expressid" onclick="sortTable(${i}, 4)">ID <span class="sort-icon">▴▾</span></th>
|