@ifc-lite/viewer 1.25.2 → 1.27.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/.turbo/turbo-build.log +40 -30
- package/CHANGELOG.md +110 -0
- package/dist/assets/{basketViewActivator-CTgyKI3U.js → basketViewActivator-B3CdrLsb.js} +7 -7
- package/dist/assets/{bcf-7jQby1qi.js → bcf-QeHK_Aud.js} +5 -5
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{deflate-Cfp9t1Df.js → deflate-B-d0SYQM.js} +1 -1
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DfSvJPi4.js → exporters-B4LbZFeT.js} +1434 -1179
- package/dist/assets/geometry.worker-BdH-E6NB.js +1 -0
- package/dist/assets/{geotiff-xZoE8BkO.js → geotiff-CrVtDRFq.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-Cu73hD0Y.js → ids-DjsGFN10.js} +21 -21
- package/dist/assets/ifc-lite_bg-DsYUIHm3.wasm +0 -0
- package/dist/assets/{index-WSbA5iy6.js → index-COYokSKc.js} +44122 -38782
- package/dist/assets/index-ajK6D32J.css +1 -0
- package/dist/assets/index.es-CY202jA3.js +6866 -0
- package/dist/assets/{jpeg-DhwFEbqb.js → jpeg-D4wOkf5h.js} +1 -1
- package/dist/assets/jspdf.es.min-DIGb9BHN.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/{lerc-Dz6BXOVb.js → lerc-DmW0_tgf.js} +1 -1
- package/dist/assets/{lzw-C9z0fG2o.js → lzw-oWetY-d6.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-RvDmzO-2.js → native-bridge-BX8_tHXE.js} +1 -1
- package/dist/assets/{packbits-jfwifz7C.js → packbits-F8Nkp4NY.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/{parser.worker-C594dWxH.js → parser.worker-D591Zu_-.js} +3 -3
- package/dist/assets/pdf-Dsh3HPZB.js +135 -0
- package/dist/assets/raw-D9iw0tmc.js +1 -0
- package/dist/assets/{sandbox-DDSZ7rek.js → sandbox-BAC3a-eN.js} +4235 -2716
- package/dist/assets/server-client-Cjwnm7il.js +706 -0
- package/dist/assets/{webimage-XFHVyVtC.js → webimage-BLV1dgmd.js} +1 -1
- package/dist/assets/xlsx-Bc2HTrjC.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-3q5qcl5V.js → zstd-C_1HxVrA.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +13 -9
- package/src/components/extensions/FlavorDialog.tsx +18 -2
- package/src/components/extensions/FlavorListView.tsx +12 -3
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +277 -0
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/ClashBcfExportDialog.tsx +271 -0
- package/src/components/viewer/ClashPanel.tsx +370 -0
- package/src/components/viewer/ClashSettingsDialog.tsx +407 -0
- package/src/components/viewer/CommandPalette.tsx +14 -15
- package/src/components/viewer/MainToolbar.tsx +155 -175
- package/src/components/viewer/PropertiesPanel.tsx +13 -6
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +64 -9
- package/src/components/viewer/ViewportContainer.tsx +45 -3
- package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/useGeometryStreaming.ts +21 -1
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/ingest/streamCleanup.test.ts +41 -0
- package/src/hooks/ingest/streamCleanup.ts +45 -0
- package/src/hooks/ingest/viewerModelIngest.ts +64 -42
- package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
- package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +139 -0
- package/src/hooks/useClash.ts +420 -0
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfcFederation.ts +16 -2
- package/src/hooks/useIfcLoader.ts +5 -7
- package/src/lib/clash/persistence.ts +308 -0
- package/src/lib/geo/effective-georef.test.ts +66 -0
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +3 -3
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/services/extensions/host.ts +13 -0
- package/src/store/constants.ts +33 -25
- package/src/store/index.ts +29 -8
- package/src/store/slices/clashSlice.ts +251 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/store/slices/visibilitySlice.test.ts +23 -5
- package/src/store/slices/visibilitySlice.ts +18 -8
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/dist/assets/geometry.worker-Cyn5BybV.js +0 -1
- package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
- package/dist/assets/index-Bws3UAkj.css +0 -1
- package/dist/assets/raw-R2QfzPAR.js +0 -1
- package/dist/assets/server-client-Ctk8_Bof.js +0 -626
package/src/lib/slab-edit.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { MutablePropertyView, StoreEditor } from '@ifc-lite/mutations';
|
|
|
35
35
|
import {
|
|
36
36
|
asExpressIdRef,
|
|
37
37
|
asCoordinateTriple,
|
|
38
|
+
asDirectionRatios,
|
|
38
39
|
readAttributes,
|
|
39
40
|
resolvePlacementChain,
|
|
40
41
|
} from './placement-core.js';
|
|
@@ -61,6 +62,93 @@ function stepTypeToSlabLike(stepType: string): SlabLikeType | null {
|
|
|
61
62
|
return SLAB_LIKE_STEP_TYPES[stepType.toUpperCase()] ?? null;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A 2D rigid transform mapping a profile-coordinate point into the
|
|
67
|
+
* solid's local plan (XY). Built from the `IfcExtrudedAreaSolid`'s
|
|
68
|
+
* `Position` (an `IfcAxis2Placement3D`), it folds in the in-place
|
|
69
|
+
* translation + rotation that real-world authoring tools bake there.
|
|
70
|
+
* In-store-built slabs carry an identity Position, so the resolver
|
|
71
|
+
* defaults to the identity transform for them.
|
|
72
|
+
*/
|
|
73
|
+
type Xform2D = (p: [number, number]) => [number, number];
|
|
74
|
+
|
|
75
|
+
const IDENTITY_XFORM2D: Xform2D = (p) => [p[0], p[1]];
|
|
76
|
+
|
|
77
|
+
function readDirection(
|
|
78
|
+
dataStore: IfcDataStore,
|
|
79
|
+
view: MutablePropertyView,
|
|
80
|
+
editor: StoreEditor,
|
|
81
|
+
id: number | null,
|
|
82
|
+
): [number, number, number] | null {
|
|
83
|
+
if (id === null) return null;
|
|
84
|
+
const attrs = readAttributes(dataStore, view, editor, id);
|
|
85
|
+
return attrs ? asDirectionRatios(attrs[0]) : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the plan-space transform for an `IfcExtrudedAreaSolid.Position`.
|
|
90
|
+
* The profile lives in the placement's local XY plane; we map a profile
|
|
91
|
+
* point `(px, py)` to `origin + px·X + py·Y` and keep the XY components
|
|
92
|
+
* (the footprint is the plan). X comes from RefDirection (orthonormalised
|
|
93
|
+
* against the Axis/Z), Y = Z × X — matching the IFC placement convention,
|
|
94
|
+
* including axis flips (e.g. Axis `(0,0,-1)`, RefDirection `(-1,0,0)`).
|
|
95
|
+
* Returns identity when the placement is absent or degenerate.
|
|
96
|
+
*/
|
|
97
|
+
function resolveSolidPositionXform(
|
|
98
|
+
dataStore: IfcDataStore,
|
|
99
|
+
view: MutablePropertyView,
|
|
100
|
+
editor: StoreEditor,
|
|
101
|
+
placementId: number | null,
|
|
102
|
+
): Xform2D {
|
|
103
|
+
if (placementId === null) return IDENTITY_XFORM2D;
|
|
104
|
+
const attrs = readAttributes(dataStore, view, editor, placementId);
|
|
105
|
+
if (!attrs) return IDENTITY_XFORM2D;
|
|
106
|
+
|
|
107
|
+
// IfcAxis2Placement3D: [0] Location · [1] Axis (Z) · [2] RefDirection (X).
|
|
108
|
+
const locId = asExpressIdRef(attrs[0]);
|
|
109
|
+
let ox = 0;
|
|
110
|
+
let oy = 0;
|
|
111
|
+
if (locId !== null) {
|
|
112
|
+
const locAttrs = readAttributes(dataStore, view, editor, locId);
|
|
113
|
+
const c = locAttrs ? asCoordinateTriple(locAttrs[0]) : null;
|
|
114
|
+
if (c) {
|
|
115
|
+
ox = c[0];
|
|
116
|
+
oy = c[1];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// IfcDirection ratios are NOT guaranteed unit length, so normalise Z
|
|
121
|
+
// before using it as a basis vector — otherwise the Gram-Schmidt
|
|
122
|
+
// projection (which assumes |Z|=1) and Y = Z × X both pick up |Z| as a
|
|
123
|
+
// stray scale factor, skewing the footprint away from the rendered mesh
|
|
124
|
+
// for files with e.g. Axis=(0,0,2). The Rust profile extractor
|
|
125
|
+
// normalises the same placement.
|
|
126
|
+
const rawZ = readDirection(dataStore, view, editor, asExpressIdRef(attrs[1])) ?? [0, 0, 1];
|
|
127
|
+
const zlen = Math.hypot(rawZ[0], rawZ[1], rawZ[2]);
|
|
128
|
+
if (zlen < 1e-9) return IDENTITY_XFORM2D;
|
|
129
|
+
const z: [number, number, number] = [rawZ[0] / zlen, rawZ[1] / zlen, rawZ[2] / zlen];
|
|
130
|
+
const refX = readDirection(dataStore, view, editor, asExpressIdRef(attrs[2])) ?? [1, 0, 0];
|
|
131
|
+
|
|
132
|
+
// Orthonormalise X against the unit Z (Gram-Schmidt), then Y = Z × X.
|
|
133
|
+
const dot = refX[0] * z[0] + refX[1] * z[1] + refX[2] * z[2];
|
|
134
|
+
let xv: [number, number, number] = [
|
|
135
|
+
refX[0] - dot * z[0],
|
|
136
|
+
refX[1] - dot * z[1],
|
|
137
|
+
refX[2] - dot * z[2],
|
|
138
|
+
];
|
|
139
|
+
const xlen = Math.hypot(xv[0], xv[1], xv[2]);
|
|
140
|
+
if (xlen < 1e-9) return IDENTITY_XFORM2D;
|
|
141
|
+
xv = [xv[0] / xlen, xv[1] / xlen, xv[2] / xlen];
|
|
142
|
+
// Z and X are now orthonormal, so Y = Z × X is already unit length.
|
|
143
|
+
const yv: [number, number, number] = [
|
|
144
|
+
z[1] * xv[2] - z[2] * xv[1],
|
|
145
|
+
z[2] * xv[0] - z[0] * xv[2],
|
|
146
|
+
z[0] * xv[1] - z[1] * xv[0],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
return (p) => [ox + p[0] * xv[0] + p[1] * yv[0], oy + p[0] * xv[1] + p[1] * yv[1]];
|
|
150
|
+
}
|
|
151
|
+
|
|
64
152
|
export interface SlabEditChain {
|
|
65
153
|
/** STEP type name, for the slice's dispatch. */
|
|
66
154
|
elementType: SlabLikeType;
|
|
@@ -106,22 +194,23 @@ function rectangleFootprint(
|
|
|
106
194
|
profileOrigin2D: [number, number],
|
|
107
195
|
xdim: number,
|
|
108
196
|
ydim: number,
|
|
197
|
+
solidXform: Xform2D,
|
|
109
198
|
): Point2D[] {
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
199
|
+
// Rectangle corners in the profile coordinate system (centred on the
|
|
200
|
+
// profile origin), mapped through the solid Position into plan space,
|
|
201
|
+
// then offset by the slab's placement origin.
|
|
113
202
|
const [px, py] = placementOrigin;
|
|
114
203
|
const [cx, cy] = profileOrigin2D;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
[xMin, yMin],
|
|
121
|
-
[xMax, yMin],
|
|
122
|
-
[xMax, yMax],
|
|
123
|
-
[xMin, yMax],
|
|
204
|
+
const corners: Point2D[] = [
|
|
205
|
+
[cx - xdim / 2, cy - ydim / 2],
|
|
206
|
+
[cx + xdim / 2, cy - ydim / 2],
|
|
207
|
+
[cx + xdim / 2, cy + ydim / 2],
|
|
208
|
+
[cx - xdim / 2, cy + ydim / 2],
|
|
124
209
|
];
|
|
210
|
+
return corners.map((c) => {
|
|
211
|
+
const [wx, wy] = solidXform(c);
|
|
212
|
+
return [px + wx, py + wy] as Point2D;
|
|
213
|
+
});
|
|
125
214
|
}
|
|
126
215
|
|
|
127
216
|
/**
|
|
@@ -136,6 +225,7 @@ function polylineFootprint(
|
|
|
136
225
|
polylineId: number,
|
|
137
226
|
placementOrigin: [number, number, number],
|
|
138
227
|
profileOrigin2D: [number, number],
|
|
228
|
+
solidXform: Xform2D,
|
|
139
229
|
): Point2D[] | null {
|
|
140
230
|
const attrs = readAttributes(dataStore, view, editor, polylineId);
|
|
141
231
|
if (!attrs) return null;
|
|
@@ -155,7 +245,10 @@ function polylineFootprint(
|
|
|
155
245
|
// tolerantly — IFC files in the wild sometimes pad with Z=0.
|
|
156
246
|
const coords = asCoordinateTriple(ptAttrs[0]);
|
|
157
247
|
if (!coords) return null;
|
|
158
|
-
|
|
248
|
+
// Point in profile CS → solid plan (Position translation + rotation)
|
|
249
|
+
// → slab placement origin.
|
|
250
|
+
const [wx, wy] = solidXform([cx + coords[0], cy + coords[1]]);
|
|
251
|
+
out.push([px + wx, py + wy]);
|
|
159
252
|
}
|
|
160
253
|
// IfcPolyline for a closed profile may or may not repeat the
|
|
161
254
|
// first vertex at the end — strip if present, our clip API
|
|
@@ -170,22 +263,57 @@ function polylineFootprint(
|
|
|
170
263
|
return out.length >= 3 ? out : null;
|
|
171
264
|
}
|
|
172
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Scale a chain's coordinate-bearing fields (footprint, placement
|
|
268
|
+
* origin, thickness) by `scale`. Identity when `scale === 1`. Used to
|
|
269
|
+
* lift a native-unit (e.g. millimetre) STEP read into the viewer's
|
|
270
|
+
* metre working space — see `resolveSlabEditChain`'s `lengthUnitScale`.
|
|
271
|
+
*/
|
|
272
|
+
function scaleSlabChain(chain: SlabEditChain, scale: number): SlabEditChain {
|
|
273
|
+
if (scale === 1) return chain;
|
|
274
|
+
return {
|
|
275
|
+
...chain,
|
|
276
|
+
placementOrigin: [
|
|
277
|
+
chain.placementOrigin[0] * scale,
|
|
278
|
+
chain.placementOrigin[1] * scale,
|
|
279
|
+
chain.placementOrigin[2] * scale,
|
|
280
|
+
],
|
|
281
|
+
footprint: chain.footprint.map(([x, y]) => [x * scale, y * scale] as Point2D),
|
|
282
|
+
thickness: chain.thickness * scale,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
173
286
|
/**
|
|
174
287
|
* Resolve the slab chain (placement + footprint + extrusion). Works
|
|
175
288
|
* for IfcSlab / IfcRoof / IfcPlate / IfcSpace whose representation
|
|
176
289
|
* matches the in-store builder shape; null otherwise.
|
|
290
|
+
*
|
|
291
|
+
* `lengthUnitScale` is the model's native-unit → metre factor (e.g.
|
|
292
|
+
* `0.001` for a millimetre file). Raw STEP coordinate reads are in
|
|
293
|
+
* native units, but the rest of the split flow — raycast cut points,
|
|
294
|
+
* preview meshes, selection hit-tests — lives in metres, so the
|
|
295
|
+
* resolved footprint/thickness are scaled to match. Authored overlay
|
|
296
|
+
* entities are skipped: the in-store builders already emit metres, so
|
|
297
|
+
* scaling them would double-apply (re-splitting a freshly-cut half).
|
|
177
298
|
*/
|
|
178
299
|
export function resolveSlabEditChain(
|
|
179
300
|
dataStore: IfcDataStore,
|
|
180
301
|
view: MutablePropertyView,
|
|
181
302
|
editor: StoreEditor,
|
|
182
303
|
expressId: number,
|
|
304
|
+
lengthUnitScale = 1,
|
|
183
305
|
): SlabEditChain | null {
|
|
184
306
|
const rawType = readEntityType(dataStore, view, editor, expressId);
|
|
185
307
|
if (!rawType) return null;
|
|
186
308
|
const elementType = stepTypeToSlabLike(rawType);
|
|
187
309
|
if (!elementType) return null;
|
|
188
310
|
|
|
311
|
+
// Overlay (authored) entities are stored in metres by the in-store
|
|
312
|
+
// builders; only native STEP reads need the unit scale applied.
|
|
313
|
+
// `getNewEntity` returns null (not undefined) for source entities.
|
|
314
|
+
const isAuthored = editor.getNewEntity(expressId) != null;
|
|
315
|
+
const scale = isAuthored ? 1 : lengthUnitScale;
|
|
316
|
+
|
|
189
317
|
const chain = resolvePlacementChain(dataStore, view, editor, expressId);
|
|
190
318
|
if (!chain) return null;
|
|
191
319
|
const placementOrigin = chain.coordinates;
|
|
@@ -212,6 +340,18 @@ export function resolveSlabEditChain(
|
|
|
212
340
|
const thicknessRaw = solidAttrs[3];
|
|
213
341
|
if (profileId === null || typeof thicknessRaw !== 'number') return null;
|
|
214
342
|
|
|
343
|
+
// IfcExtrudedAreaSolid.Position (attr 1) is an IfcAxis2Placement3D that
|
|
344
|
+
// places the profile in the solid's frame — real authoring tools bake
|
|
345
|
+
// the slab's plan offset + rotation here (in-store-built slabs leave it
|
|
346
|
+
// identity). Fold it into the footprint so the preview, cut line, and
|
|
347
|
+
// resulting halves land where the rendered mesh actually is.
|
|
348
|
+
const solidXform = resolveSolidPositionXform(
|
|
349
|
+
dataStore,
|
|
350
|
+
view,
|
|
351
|
+
editor,
|
|
352
|
+
asExpressIdRef(solidAttrs[1]),
|
|
353
|
+
);
|
|
354
|
+
|
|
215
355
|
// Profile dispatch — rectangle vs polygon, both produced by
|
|
216
356
|
// addSlabToStore. Source-buffer slabs with mapped representations,
|
|
217
357
|
// I-shape profiles, etc. land in `null` here and the slice
|
|
@@ -244,29 +384,29 @@ export function resolveSlabEditChain(
|
|
|
244
384
|
const xdim = profileAttrs[3];
|
|
245
385
|
const ydim = profileAttrs[4];
|
|
246
386
|
if (typeof xdim !== 'number' || typeof ydim !== 'number') return null;
|
|
247
|
-
return {
|
|
387
|
+
return scaleSlabChain({
|
|
248
388
|
elementType,
|
|
249
389
|
placementOrigin,
|
|
250
|
-
footprint: rectangleFootprint(placementOrigin, profileOrigin2D, xdim, ydim),
|
|
390
|
+
footprint: rectangleFootprint(placementOrigin, profileOrigin2D, xdim, ydim, solidXform),
|
|
251
391
|
extrudedSolidId: solidId,
|
|
252
392
|
thickness: thicknessRaw,
|
|
253
393
|
profileKind: 'rectangle',
|
|
254
|
-
};
|
|
394
|
+
}, scale);
|
|
255
395
|
}
|
|
256
396
|
if (profileType && profileType.toUpperCase() === 'IFCARBITRARYCLOSEDPROFILEDEF') {
|
|
257
397
|
// OuterCurve at attr 2.
|
|
258
398
|
const polylineId = asExpressIdRef(profileAttrs[2]);
|
|
259
399
|
if (polylineId === null) return null;
|
|
260
|
-
const fp = polylineFootprint(dataStore, view, editor, polylineId, placementOrigin, profileOrigin2D);
|
|
400
|
+
const fp = polylineFootprint(dataStore, view, editor, polylineId, placementOrigin, profileOrigin2D, solidXform);
|
|
261
401
|
if (!fp) return null;
|
|
262
|
-
return {
|
|
402
|
+
return scaleSlabChain({
|
|
263
403
|
elementType,
|
|
264
404
|
placementOrigin,
|
|
265
405
|
footprint: fp,
|
|
266
406
|
extrudedSolidId: solidId,
|
|
267
407
|
thickness: thicknessRaw,
|
|
268
408
|
profileKind: 'polygon',
|
|
269
|
-
};
|
|
409
|
+
}, scale);
|
|
270
410
|
}
|
|
271
411
|
return null;
|
|
272
412
|
}
|
|
@@ -153,13 +153,13 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
|
153
153
|
if (!model?.ifcDataStore) return [];
|
|
154
154
|
|
|
155
155
|
const node = new EntityNode(model.ifcDataStore, ref.expressId);
|
|
156
|
-
return node.properties().map((pset
|
|
156
|
+
return node.properties().map((pset) => ({
|
|
157
157
|
name: pset.name,
|
|
158
158
|
globalId: pset.globalId,
|
|
159
|
-
properties: pset.properties.map((p
|
|
159
|
+
properties: pset.properties.map((p) => ({
|
|
160
160
|
name: p.name,
|
|
161
161
|
type: p.type,
|
|
162
|
-
value: p.value,
|
|
162
|
+
value: p.value as string | number | boolean | null,
|
|
163
163
|
})),
|
|
164
164
|
}));
|
|
165
165
|
}
|
|
@@ -136,13 +136,13 @@ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
|
|
|
136
136
|
if (!model?.ifcDataStore) return [];
|
|
137
137
|
|
|
138
138
|
const node = new EntityNode(model.ifcDataStore, ref.expressId);
|
|
139
|
-
return node.properties().map((pset
|
|
139
|
+
return node.properties().map((pset) => ({
|
|
140
140
|
name: pset.name,
|
|
141
141
|
globalId: pset.globalId,
|
|
142
|
-
properties: pset.properties.map((p
|
|
142
|
+
properties: pset.properties.map((p) => ({
|
|
143
143
|
name: p.name,
|
|
144
144
|
type: p.type,
|
|
145
|
-
value: p.value,
|
|
145
|
+
value: p.value as string | number | boolean | null,
|
|
146
146
|
})),
|
|
147
147
|
}));
|
|
148
148
|
}
|
|
@@ -436,6 +436,19 @@ export class ExtensionHostService {
|
|
|
436
436
|
} catch (err) {
|
|
437
437
|
console.warn('[ext-host] lens restore on switch failed:', err);
|
|
438
438
|
}
|
|
439
|
+
// Restore the flavor's clash config (rule-set + detection settings) from the
|
|
440
|
+
// opaque settings.clash blob, mirroring the lens roundtrip above. Missing /
|
|
441
|
+
// malformed blobs deserialize to null and are skipped (no-op).
|
|
442
|
+
try {
|
|
443
|
+
const { deserializeClashConfig } = await import('@/lib/clash/persistence');
|
|
444
|
+
const config = deserializeClashConfig((target.settings as Record<string, unknown> | undefined)?.clash);
|
|
445
|
+
if (config) {
|
|
446
|
+
const { useViewerStore } = await import('@/store');
|
|
447
|
+
useViewerStore.getState().applyClashFlavorConfig(config);
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.warn('[ext-host] clash restore on switch failed:', err);
|
|
451
|
+
}
|
|
439
452
|
this.emit();
|
|
440
453
|
}
|
|
441
454
|
|
package/src/store/constants.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Store constants - extracted magic numbers for maintainability
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { TypeVisibility } from './types.js';
|
|
10
|
+
|
|
9
11
|
// ============================================================================
|
|
10
12
|
// Camera Defaults
|
|
11
13
|
// ============================================================================
|
|
@@ -182,37 +184,43 @@ function readPersistedBool(key: string, fallback: boolean): boolean {
|
|
|
182
184
|
// on first load. IfcSite + IfcAnnotation + IfcGrid on — all three convey
|
|
183
185
|
// design intent users expect to see by default. (Issue #862 split grid
|
|
184
186
|
// into its own toggle so dense-grid models can hide grids without losing
|
|
185
|
-
// dimensions/labels.)
|
|
186
|
-
|
|
187
|
+
// dimensions/labels.) Exported so the "Reset" action in the visibility
|
|
188
|
+
// menu can restore these without re-deriving them.
|
|
189
|
+
export const TYPE_VISIBILITY_SEMANTIC_DEFAULTS: TypeVisibility = {
|
|
187
190
|
spaces: false,
|
|
188
191
|
openings: false,
|
|
189
192
|
site: true,
|
|
190
193
|
ifcAnnotations: true,
|
|
191
194
|
ifcGrid: true,
|
|
192
|
-
}
|
|
195
|
+
};
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the full type-visibility preference set from localStorage.
|
|
199
|
+
*
|
|
200
|
+
* Read fresh on EVERY call — not captured once at module load. The store
|
|
201
|
+
* applies this both at boot (slice init) and on every new-file load
|
|
202
|
+
* (`resetViewerState`). A module-level constant would snapshot localStorage
|
|
203
|
+
* at first import and then go stale after the first in-session toggle, so
|
|
204
|
+
* loading a second model would silently revert the user's choices (e.g.
|
|
205
|
+
* "Show Annotations" flipping back on). Reading live keeps every toggle
|
|
206
|
+
* sticky across reloads AND across model swaps within a session.
|
|
207
|
+
*/
|
|
208
|
+
export function getPersistedTypeVisibility(): TypeVisibility {
|
|
209
|
+
return {
|
|
210
|
+
spaces: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.spaces, TYPE_VISIBILITY_SEMANTIC_DEFAULTS.spaces),
|
|
211
|
+
openings: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.openings, TYPE_VISIBILITY_SEMANTIC_DEFAULTS.openings),
|
|
212
|
+
site: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.site, TYPE_VISIBILITY_SEMANTIC_DEFAULTS.site),
|
|
213
|
+
ifcAnnotations: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.ifcAnnotations, TYPE_VISIBILITY_SEMANTIC_DEFAULTS.ifcAnnotations),
|
|
214
|
+
// Issue #862. Migration: if the new grid key isn't set yet, fall back to
|
|
215
|
+
// the legacy combined `ifcAnnotations` preference so a user who turned
|
|
216
|
+
// the old "Annotations & Grids" toggle off keeps grids hidden after
|
|
217
|
+
// upgrade instead of grids silently reappearing (PR #868 review).
|
|
218
|
+
ifcGrid: readPersistedBool(
|
|
219
|
+
TYPE_VISIBILITY_STORAGE_KEYS.ifcGrid,
|
|
220
|
+
readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.ifcAnnotations, TYPE_VISIBILITY_SEMANTIC_DEFAULTS.ifcGrid),
|
|
221
|
+
),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
216
224
|
|
|
217
225
|
// ============================================================================
|
|
218
226
|
// Data Defaults
|
package/src/store/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { createExtensionsSlice, type ExtensionsSlice } from './slices/extensions
|
|
|
33
33
|
import { createListSlice, type ListSlice } from './slices/listSlice.js';
|
|
34
34
|
import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
|
|
35
35
|
import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
|
|
36
|
+
import { createClashSlice, type ClashSlice } from './slices/clashSlice.js';
|
|
36
37
|
import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
|
|
37
38
|
import { createChatSlice, type ChatSlice } from './slices/chatSlice.js';
|
|
38
39
|
import { createCesiumSlice, type CesiumSlice } from './slices/cesiumSlice.js';
|
|
@@ -49,7 +50,7 @@ import { createPointCloudSlice, type PointCloudSlice, POINT_CLOUD_DEFAULTS } fro
|
|
|
49
50
|
import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
|
|
50
51
|
|
|
51
52
|
// Import constants for reset function
|
|
52
|
-
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS,
|
|
53
|
+
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, getPersistedTypeVisibility } from './constants.js';
|
|
53
54
|
|
|
54
55
|
// Re-export types for consumers
|
|
55
56
|
export type * from './types.js';
|
|
@@ -129,6 +130,7 @@ export type ViewerState = LoadingSlice &
|
|
|
129
130
|
ListSlice &
|
|
130
131
|
PinboardSlice &
|
|
131
132
|
LensSlice &
|
|
133
|
+
ClashSlice &
|
|
132
134
|
ScriptSlice &
|
|
133
135
|
ChatSlice &
|
|
134
136
|
CesiumSlice &
|
|
@@ -144,6 +146,16 @@ export type ViewerState = LoadingSlice &
|
|
|
144
146
|
PointCloudSlice &
|
|
145
147
|
ExtensionsSlice & {
|
|
146
148
|
resetViewerState: () => void;
|
|
149
|
+
/**
|
|
150
|
+
* Open one right-side analysis panel and close the others, so the chosen
|
|
151
|
+
* panel is always the topmost/active one. The right panel renders a single
|
|
152
|
+
* mutually-exclusive chain (lens → clash → ids → bcf → extensions), so
|
|
153
|
+
* leaving a sibling flag set would keep the higher-precedence panel on top
|
|
154
|
+
* (the cause of "I have to close clash before I see BCF"). Also un-collapses
|
|
155
|
+
* the right panel. Routed through by the toolbar, command palette, and the
|
|
156
|
+
* BCF overlay so every entry point behaves identically.
|
|
157
|
+
*/
|
|
158
|
+
openWorkspacePanel: (panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'extensions') => void;
|
|
147
159
|
};
|
|
148
160
|
|
|
149
161
|
/**
|
|
@@ -169,6 +181,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
169
181
|
...createListSlice(...args),
|
|
170
182
|
...createPinboardSlice(...args),
|
|
171
183
|
...createLensSlice(...args),
|
|
184
|
+
...createClashSlice(...args),
|
|
172
185
|
...createScriptSlice(...args),
|
|
173
186
|
...createChatSlice(...args),
|
|
174
187
|
...createCesiumSlice(...args),
|
|
@@ -203,13 +216,9 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
203
216
|
hiddenEntities: new Set(),
|
|
204
217
|
isolatedEntities: null,
|
|
205
218
|
classFilter: null,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
site: TYPE_VISIBILITY_DEFAULTS.SITE,
|
|
210
|
-
ifcAnnotations: TYPE_VISIBILITY_DEFAULTS.IFC_ANNOTATIONS,
|
|
211
|
-
ifcGrid: TYPE_VISIBILITY_DEFAULTS.IFC_GRID,
|
|
212
|
-
},
|
|
219
|
+
// Re-read persisted toggles on every file load so a new model never
|
|
220
|
+
// reverts the user's visibility choices (e.g. "Show Annotations").
|
|
221
|
+
typeVisibility: getPersistedTypeVisibility(),
|
|
213
222
|
|
|
214
223
|
// Visibility (multi-model)
|
|
215
224
|
hiddenEntitiesByModel: new Map(),
|
|
@@ -443,6 +452,18 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
443
452
|
pointCloudFixedColor: [...POINT_CLOUD_DEFAULTS.pointCloudFixedColor] as [number, number, number, number],
|
|
444
453
|
});
|
|
445
454
|
},
|
|
455
|
+
|
|
456
|
+
openWorkspacePanel: (panel) => {
|
|
457
|
+
const [set] = args;
|
|
458
|
+
set({
|
|
459
|
+
bcfPanelVisible: panel === 'bcf',
|
|
460
|
+
idsPanelVisible: panel === 'ids',
|
|
461
|
+
lensPanelVisible: panel === 'lens',
|
|
462
|
+
clashPanelVisible: panel === 'clash',
|
|
463
|
+
extensionsPanelVisible: panel === 'extensions',
|
|
464
|
+
rightPanelCollapsed: false,
|
|
465
|
+
});
|
|
466
|
+
},
|
|
446
467
|
}));
|
|
447
468
|
|
|
448
469
|
const STORE_SINGLETON_KEY = '__ifc_lite_viewer_store__';
|