@ifc-lite/viewer 1.25.1 → 1.26.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 +83 -85
- package/CHANGELOG.md +104 -0
- package/dist/assets/{basketViewActivator-Dkn92C04.js → basketViewActivator-ZpTYWE3K.js} +6 -6
- package/dist/assets/{bcf-DP2AK1-_.js → bcf-Ctcu_Sc2.js} +5 -5
- package/dist/assets/{deflate-BYqYwhkl.js → deflate-Cnx0il6E.js} +1 -1
- package/dist/assets/exporters-DSq76AVM.js +4687 -0
- package/dist/assets/geometry.worker-0Q9qEa6p.js +1 -0
- package/dist/assets/{geotiff-By06vdeL.js → geotiff-A5UjhI6L.js} +10 -10
- package/dist/assets/{ids-DDkkb4mo.js → ids-DiLcGTer.js} +4 -4
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +1 -0
- package/dist/assets/{index-CqBdDOAZ.js → index-BAH8IJVR.js} +39550 -36936
- package/dist/assets/{jpeg-B4IBTphL.js → jpeg-BzSkwo5D.js} +1 -1
- package/dist/assets/{lerc-DQ3jI0Ke.js → lerc-Cg2Rz-D5.js} +1 -1
- package/dist/assets/{lzw-CtdH775t.js → lzw-BBPPLW-0.js} +1 -1
- package/dist/assets/{native-bridge-DA8wxaN_.js → native-bridge-CPojOeGE.js} +1 -1
- package/dist/assets/{packbits-DG3zn49C.js → packbits-yLSpjW-V.js} +1 -1
- package/dist/assets/parser.worker-8md211IW.js +182 -0
- package/dist/assets/raw-BQrAgxwT.js +1 -0
- package/dist/assets/{sandbox-D1pQT-5R.js → sandbox-CsRXlgCO.js} +4715 -3081
- package/dist/assets/{server-client-D9xO_8yX.js → server-client-Bk4c1CPO.js} +1 -1
- package/dist/assets/{webimage-_-qCDjkn.js → webimage-YafxjjGr.js} +1 -1
- package/dist/assets/{zstd-DlfgC8gA.js → zstd-CkSLOiuu.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +23 -21
- package/src/App.tsx +4 -0
- package/src/components/extensions/FlavorDialog.tsx +18 -2
- package/src/components/extensions/FlavorListView.tsx +12 -3
- 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 +19 -16
- package/src/components/viewer/MainToolbar.tsx +155 -153
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +97 -12
- package/src/components/viewer/ViewportContainer.tsx +45 -3
- package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
- package/src/components/viewer/hierarchy/ifc-icons.ts +60 -0
- package/src/components/viewer/useGeometryStreaming.ts +134 -19
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +61 -0
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +28 -0
- package/src/hooks/ingest/streamCleanup.test.ts +41 -0
- package/src/hooks/ingest/streamCleanup.ts +45 -0
- package/src/hooks/ingest/viewerModelIngest.ts +118 -52
- package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
- package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
- package/src/hooks/useAlignmentLines3D.ts +164 -0
- package/src/hooks/useClash.ts +420 -0
- package/src/hooks/useIfcCache.ts +44 -18
- package/src/hooks/useIfcFederation.ts +16 -2
- package/src/hooks/useIfcLoader.ts +6 -30
- package/src/hooks/useSymbolicAnnotations.ts +170 -35
- package/src/lib/clash/persistence.ts +308 -0
- package/src/lib/geo/effective-georef.test.ts +66 -0
- package/src/services/extensions/host.ts +13 -0
- package/src/store/constants.ts +38 -14
- package/src/store/index.ts +29 -7
- package/src/store/slices/clashSlice.ts +251 -0
- package/src/store/slices/visibilitySlice.test.ts +23 -5
- package/src/store/slices/visibilitySlice.ts +19 -8
- package/src/store/types.ts +9 -0
- package/src/utils/serverDataModel.test.ts +51 -1
- package/src/utils/serverDataModel.ts +2 -26
- package/vite.config.ts +0 -5
- package/dist/assets/exporters-CZe0D8N-.js +0 -5957
- package/dist/assets/geometry-controller.worker-pD49_fH6.js +0 -7
- package/dist/assets/geometry.worker-D4c-06r5.js +0 -1
- package/dist/assets/ifc-lite-DxGqDbjO.js +0 -7
- package/dist/assets/ifc-lite_bg-BNeu7R_V.wasm +0 -0
- package/dist/assets/ifc-lite_bg-DuxUZomW.wasm +0 -0
- package/dist/assets/index-Bws3UAkj.css +0 -1
- package/dist/assets/parser.worker-BZZcO7DB.js +0 -182
- package/dist/assets/raw-DY7Y_acr.js +0 -1
- package/dist/assets/wasm-bridge-DMX8Acuf.js +0 -1
- package/dist/assets/workerHelpers-Crstj4Oa.js +0 -36
|
@@ -8,6 +8,7 @@ import assert from 'node:assert';
|
|
|
8
8
|
import {
|
|
9
9
|
detectScaleUnitMismatch,
|
|
10
10
|
getEffectiveHorizontalScale,
|
|
11
|
+
hasStandardGeoreferencing,
|
|
11
12
|
inferMapUnitScale,
|
|
12
13
|
mergeMapConversion,
|
|
13
14
|
mergeProjectedCRS,
|
|
@@ -237,4 +238,69 @@ describe('effective georeferencing', () => {
|
|
|
237
238
|
assert.strictEqual(m!.effectiveScale, 2);
|
|
238
239
|
});
|
|
239
240
|
});
|
|
241
|
+
|
|
242
|
+
describe('hasStandardGeoreferencing (federation alignment gate)', () => {
|
|
243
|
+
// Federation affine alignment (extractModelGeoref → buildGeorefAlignmentTransform)
|
|
244
|
+
// gates on this predicate. A site-location-only georef must NOT qualify: it is
|
|
245
|
+
// EPSG:4326 lat/long degrees + a raw, un-unit-scaled IfcSite RefElevation, which
|
|
246
|
+
// the projected-CRS transform misreads as metres and flings the second federated
|
|
247
|
+
// model kilometres away. These tests lock that invariant.
|
|
248
|
+
const mapConversion: MapConversion = {
|
|
249
|
+
id: 1,
|
|
250
|
+
sourceCRS: 0,
|
|
251
|
+
targetCRS: 0,
|
|
252
|
+
eastings: 100,
|
|
253
|
+
northings: 200,
|
|
254
|
+
orthogonalHeight: 5,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
it('rejects synthesised site-location georef even with a CRS name + map conversion', () => {
|
|
258
|
+
assert.strictEqual(
|
|
259
|
+
hasStandardGeoreferencing({
|
|
260
|
+
source: 'siteLocation',
|
|
261
|
+
projectedCRS: { id: 1, name: 'EPSG:4326' },
|
|
262
|
+
mapConversion,
|
|
263
|
+
}),
|
|
264
|
+
false,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('accepts true IfcMapConversion + IfcProjectedCRS georef', () => {
|
|
269
|
+
assert.strictEqual(
|
|
270
|
+
hasStandardGeoreferencing({
|
|
271
|
+
source: 'mapConversion',
|
|
272
|
+
projectedCRS: { id: 1, name: 'EPSG:28992' },
|
|
273
|
+
mapConversion,
|
|
274
|
+
}),
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('rejects georef missing a map conversion', () => {
|
|
280
|
+
assert.strictEqual(
|
|
281
|
+
hasStandardGeoreferencing({
|
|
282
|
+
source: 'mapConversion',
|
|
283
|
+
projectedCRS: { id: 1, name: 'EPSG:28992' },
|
|
284
|
+
mapConversion: undefined,
|
|
285
|
+
}),
|
|
286
|
+
false,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('rejects georef missing a projected CRS name', () => {
|
|
291
|
+
assert.strictEqual(
|
|
292
|
+
hasStandardGeoreferencing({
|
|
293
|
+
source: 'mapConversion',
|
|
294
|
+
projectedCRS: { id: 1, name: '' },
|
|
295
|
+
mapConversion,
|
|
296
|
+
}),
|
|
297
|
+
false,
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('rejects null / undefined', () => {
|
|
302
|
+
assert.strictEqual(hasStandardGeoreferencing(null), false);
|
|
303
|
+
assert.strictEqual(hasStandardGeoreferencing(undefined), false);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
240
306
|
});
|
|
@@ -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
|
// ============================================================================
|
|
@@ -159,6 +161,7 @@ export const TYPE_VISIBILITY_STORAGE_KEYS = {
|
|
|
159
161
|
openings: 'ifc-lite-ifc-openings-visible',
|
|
160
162
|
site: 'ifc-lite-ifc-site-visible',
|
|
161
163
|
ifcAnnotations: 'ifc-lite-ifc-annotations-visible',
|
|
164
|
+
ifcGrid: 'ifc-lite-ifc-grid-visible',
|
|
162
165
|
} as const;
|
|
163
166
|
|
|
164
167
|
/** Legacy alias — kept until external callers migrate. */
|
|
@@ -178,25 +181,46 @@ function readPersistedBool(key: string, fallback: boolean): boolean {
|
|
|
178
181
|
|
|
179
182
|
// Semantic defaults applied when no localStorage preference is set.
|
|
180
183
|
// IfcSpace / IfcOpeningElement off — they cover walls and confuse novices
|
|
181
|
-
// on first load. IfcSite + IfcAnnotation
|
|
182
|
-
// design intent users expect to see by default.
|
|
183
|
-
|
|
184
|
+
// on first load. IfcSite + IfcAnnotation + IfcGrid on — all three convey
|
|
185
|
+
// design intent users expect to see by default. (Issue #862 split grid
|
|
186
|
+
// into its own toggle so dense-grid models can hide grids without losing
|
|
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 = {
|
|
184
190
|
spaces: false,
|
|
185
191
|
openings: false,
|
|
186
192
|
site: true,
|
|
187
193
|
ifcAnnotations: true,
|
|
188
|
-
|
|
194
|
+
ifcGrid: true,
|
|
195
|
+
};
|
|
189
196
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
224
|
|
|
201
225
|
// ============================================================================
|
|
202
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,12 +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
|
-
},
|
|
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(),
|
|
212
222
|
|
|
213
223
|
// Visibility (multi-model)
|
|
214
224
|
hiddenEntitiesByModel: new Map(),
|
|
@@ -442,6 +452,18 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
442
452
|
pointCloudFixedColor: [...POINT_CLOUD_DEFAULTS.pointCloudFixedColor] as [number, number, number, number],
|
|
443
453
|
});
|
|
444
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
|
+
},
|
|
445
467
|
}));
|
|
446
468
|
|
|
447
469
|
const STORE_SINGLETON_KEY = '__ifc_lite_viewer_store__';
|
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
* Clash detection panel state (Phase 1). Detection itself lives in
|
|
7
|
+
* `@ifc-lite/clash`; this slice holds the panel's UI state, the last result,
|
|
8
|
+
* and the user's persisted detection settings + rule presets (see
|
|
9
|
+
* `lib/clash/persistence.ts`, modeled on the lens slice). Orchestration
|
|
10
|
+
* (gathering elements, running the engine, applying colors / selection /
|
|
11
|
+
* camera, BCF export) lives in the `useClash` hook.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StateCreator } from 'zustand';
|
|
15
|
+
import type { ClashResult, ClashGroup, ClashMode, ClashProgress } from '@ifc-lite/clash';
|
|
16
|
+
import {
|
|
17
|
+
buildInitialPresets,
|
|
18
|
+
defaultPresets,
|
|
19
|
+
loadSettings,
|
|
20
|
+
savePresets,
|
|
21
|
+
saveSettings,
|
|
22
|
+
validatePresetName,
|
|
23
|
+
validateSelector,
|
|
24
|
+
CLASH_BOUNDS,
|
|
25
|
+
clampToBounds,
|
|
26
|
+
DEFAULT_CLASH_SETTINGS,
|
|
27
|
+
type ClashPreset,
|
|
28
|
+
type ClashGlobalSettings,
|
|
29
|
+
type ClashSettingsGroupBy,
|
|
30
|
+
type SaveResult,
|
|
31
|
+
} from '@/lib/clash/persistence';
|
|
32
|
+
|
|
33
|
+
export type ClashGroupBy = ClashSettingsGroupBy;
|
|
34
|
+
export type { ClashPreset, ClashGlobalSettings, SaveResult };
|
|
35
|
+
|
|
36
|
+
/** Fields a user supplies when adding a custom rule (id/flags filled in here). */
|
|
37
|
+
export type NewClashPreset = {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
severity: ClashPreset['severity'];
|
|
41
|
+
selectorA: string;
|
|
42
|
+
selectorB: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface ClashSlice {
|
|
46
|
+
clashPanelVisible: boolean;
|
|
47
|
+
clashResult: ClashResult | null;
|
|
48
|
+
clashGroups: ClashGroup[] | null;
|
|
49
|
+
clashRunning: boolean;
|
|
50
|
+
clashError: string | null;
|
|
51
|
+
/** Live detection progress for the running rule (null when idle). */
|
|
52
|
+
clashProgress: ClashProgress | null;
|
|
53
|
+
/** Detection settings (persisted). */
|
|
54
|
+
clashMode: ClashMode;
|
|
55
|
+
clashTolerance: number;
|
|
56
|
+
clashClearance: number;
|
|
57
|
+
clashClusterEpsilon: number;
|
|
58
|
+
clashReportTouch: boolean;
|
|
59
|
+
/** How the result list is organized (persisted). */
|
|
60
|
+
clashGroupBy: ClashGroupBy;
|
|
61
|
+
/** Built-in + custom rule presets (persisted). */
|
|
62
|
+
clashPresets: ClashPreset[];
|
|
63
|
+
/** Currently focused clash id (for highlight in the list). */
|
|
64
|
+
clashSelectedId: string | null;
|
|
65
|
+
|
|
66
|
+
setClashPanelVisible: (visible: boolean) => void;
|
|
67
|
+
toggleClashPanel: () => void;
|
|
68
|
+
setClashResult: (result: ClashResult | null) => void;
|
|
69
|
+
setClashGroups: (groups: ClashGroup[] | null) => void;
|
|
70
|
+
setClashRunning: (running: boolean) => void;
|
|
71
|
+
setClashError: (error: string | null) => void;
|
|
72
|
+
setClashProgress: (progress: ClashProgress | null) => void;
|
|
73
|
+
setClashMode: (mode: ClashMode) => void;
|
|
74
|
+
setClashTolerance: (tolerance: number) => void;
|
|
75
|
+
setClashClearance: (clearance: number) => void;
|
|
76
|
+
setClashClusterEpsilon: (epsilon: number) => void;
|
|
77
|
+
setClashReportTouch: (reportTouch: boolean) => void;
|
|
78
|
+
setClashGroupBy: (groupBy: ClashGroupBy) => void;
|
|
79
|
+
resetClashSettings: () => void;
|
|
80
|
+
setClashSelectedId: (id: string | null) => void;
|
|
81
|
+
// Preset CRUD (persisted). create/update/import return a SaveResult so the UI
|
|
82
|
+
// can surface quota / cap failures; the rest are best-effort.
|
|
83
|
+
createClashPreset: (input: NewClashPreset) => SaveResult;
|
|
84
|
+
updateClashPreset: (id: string, patch: Partial<Omit<ClashPreset, 'id' | 'builtin'>>) => SaveResult;
|
|
85
|
+
deleteClashPreset: (id: string) => void;
|
|
86
|
+
setClashPresetEnabled: (id: string, enabled: boolean) => void;
|
|
87
|
+
resetClashPresets: () => void;
|
|
88
|
+
importClashPresets: (presets: ClashPreset[]) => SaveResult;
|
|
89
|
+
/**
|
|
90
|
+
* Replace the entire clash config (presets + detection settings) and persist.
|
|
91
|
+
* Used when activating a flavor/profile so each one carries its own rule-set.
|
|
92
|
+
*/
|
|
93
|
+
applyClashFlavorConfig: (config: { presets: ClashPreset[]; settings: ClashGlobalSettings }) => void;
|
|
94
|
+
clearClash: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build the persisted settings blob from current slice state. */
|
|
98
|
+
function snapshotSettings(s: ClashSlice): ClashGlobalSettings {
|
|
99
|
+
return {
|
|
100
|
+
mode: s.clashMode,
|
|
101
|
+
tolerance: s.clashTolerance,
|
|
102
|
+
clearance: s.clashClearance,
|
|
103
|
+
clusterEpsilon: s.clashClusterEpsilon,
|
|
104
|
+
reportTouch: s.clashReportTouch,
|
|
105
|
+
groupBy: s.clashGroupBy,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const createClashSlice: StateCreator<ClashSlice, [], [], ClashSlice> = (set, get) => {
|
|
110
|
+
const initial = loadSettings();
|
|
111
|
+
// Persist the current settings snapshot after a state change.
|
|
112
|
+
const persistSettings = () => saveSettings(snapshotSettings(get()));
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
clashPanelVisible: false,
|
|
116
|
+
clashResult: null,
|
|
117
|
+
clashGroups: null,
|
|
118
|
+
clashRunning: false,
|
|
119
|
+
clashError: null,
|
|
120
|
+
clashProgress: null,
|
|
121
|
+
clashMode: initial.mode,
|
|
122
|
+
clashTolerance: initial.tolerance,
|
|
123
|
+
clashClearance: initial.clearance,
|
|
124
|
+
clashClusterEpsilon: initial.clusterEpsilon,
|
|
125
|
+
clashReportTouch: initial.reportTouch,
|
|
126
|
+
clashGroupBy: initial.groupBy,
|
|
127
|
+
clashPresets: buildInitialPresets(),
|
|
128
|
+
clashSelectedId: null,
|
|
129
|
+
|
|
130
|
+
setClashPanelVisible: (clashPanelVisible) => set({ clashPanelVisible }),
|
|
131
|
+
toggleClashPanel: () => set((s) => ({ clashPanelVisible: !s.clashPanelVisible })),
|
|
132
|
+
setClashResult: (clashResult) => set({ clashResult }),
|
|
133
|
+
setClashGroups: (clashGroups) => set({ clashGroups }),
|
|
134
|
+
setClashRunning: (clashRunning) => set({ clashRunning }),
|
|
135
|
+
setClashError: (clashError) => set({ clashError }),
|
|
136
|
+
setClashProgress: (clashProgress) => set({ clashProgress }),
|
|
137
|
+
|
|
138
|
+
setClashMode: (clashMode) => { set({ clashMode }); persistSettings(); },
|
|
139
|
+
setClashTolerance: (clashTolerance) => {
|
|
140
|
+
set({ clashTolerance: clampToBounds(clashTolerance, CLASH_BOUNDS.tolerance, DEFAULT_CLASH_SETTINGS.tolerance) });
|
|
141
|
+
persistSettings();
|
|
142
|
+
},
|
|
143
|
+
setClashClearance: (clashClearance) => {
|
|
144
|
+
set({ clashClearance: clampToBounds(clashClearance, CLASH_BOUNDS.clearance, DEFAULT_CLASH_SETTINGS.clearance) });
|
|
145
|
+
persistSettings();
|
|
146
|
+
},
|
|
147
|
+
setClashClusterEpsilon: (clashClusterEpsilon) => {
|
|
148
|
+
set({ clashClusterEpsilon: clampToBounds(clashClusterEpsilon, CLASH_BOUNDS.clusterEpsilon, DEFAULT_CLASH_SETTINGS.clusterEpsilon) });
|
|
149
|
+
persistSettings();
|
|
150
|
+
},
|
|
151
|
+
setClashReportTouch: (clashReportTouch) => { set({ clashReportTouch }); persistSettings(); },
|
|
152
|
+
setClashGroupBy: (clashGroupBy) => { set({ clashGroupBy }); persistSettings(); },
|
|
153
|
+
resetClashSettings: () => {
|
|
154
|
+
set({
|
|
155
|
+
clashMode: DEFAULT_CLASH_SETTINGS.mode,
|
|
156
|
+
clashTolerance: DEFAULT_CLASH_SETTINGS.tolerance,
|
|
157
|
+
clashClearance: DEFAULT_CLASH_SETTINGS.clearance,
|
|
158
|
+
clashClusterEpsilon: DEFAULT_CLASH_SETTINGS.clusterEpsilon,
|
|
159
|
+
clashReportTouch: DEFAULT_CLASH_SETTINGS.reportTouch,
|
|
160
|
+
clashGroupBy: DEFAULT_CLASH_SETTINGS.groupBy,
|
|
161
|
+
});
|
|
162
|
+
persistSettings();
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
setClashSelectedId: (clashSelectedId) => set({ clashSelectedId }),
|
|
166
|
+
|
|
167
|
+
createClashPreset: (input) => {
|
|
168
|
+
const name = validatePresetName(input.name);
|
|
169
|
+
const selectorA = validateSelector(input.selectorA);
|
|
170
|
+
const selectorB = validateSelector(input.selectorB);
|
|
171
|
+
if (!name || !selectorA || !selectorB) {
|
|
172
|
+
return { ok: false, reason: 'serialize', message: 'Name and both selectors are required.' };
|
|
173
|
+
}
|
|
174
|
+
const preset: ClashPreset = {
|
|
175
|
+
id: `custom-${crypto.randomUUID()}`,
|
|
176
|
+
name,
|
|
177
|
+
description: input.description?.trim() ?? '',
|
|
178
|
+
severity: input.severity,
|
|
179
|
+
selectorA,
|
|
180
|
+
selectorB,
|
|
181
|
+
enabled: true,
|
|
182
|
+
builtin: false,
|
|
183
|
+
};
|
|
184
|
+
const next = [...get().clashPresets, preset];
|
|
185
|
+
const result = savePresets(next);
|
|
186
|
+
if (result.ok) set({ clashPresets: next });
|
|
187
|
+
return result;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
updateClashPreset: (id, patch) => {
|
|
191
|
+
const next = get().clashPresets.map((p) => (p.id === id ? { ...p, ...patch } : p));
|
|
192
|
+
const result = savePresets(next);
|
|
193
|
+
if (result.ok) set({ clashPresets: next });
|
|
194
|
+
return result;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
deleteClashPreset: (id) => {
|
|
198
|
+
const target = get().clashPresets.find((p) => p.id === id);
|
|
199
|
+
if (!target || target.builtin) return; // built-ins are reset, never deleted
|
|
200
|
+
const next = get().clashPresets.filter((p) => p.id !== id);
|
|
201
|
+
savePresets(next);
|
|
202
|
+
set({ clashPresets: next });
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
setClashPresetEnabled: (id, enabled) => {
|
|
206
|
+
const next = get().clashPresets.map((p) => (p.id === id ? { ...p, enabled } : p));
|
|
207
|
+
savePresets(next);
|
|
208
|
+
set({ clashPresets: next });
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
resetClashPresets: () => {
|
|
212
|
+
const next = defaultPresets(); // drops all overrides + customs
|
|
213
|
+
savePresets(next);
|
|
214
|
+
set({ clashPresets: next });
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
importClashPresets: (presets) => {
|
|
218
|
+
const next = [...get().clashPresets, ...presets.filter((p) => !p.builtin)];
|
|
219
|
+
const result = savePresets(next);
|
|
220
|
+
if (result.ok) set({ clashPresets: next });
|
|
221
|
+
return result;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
applyClashFlavorConfig: ({ presets, settings }) => {
|
|
225
|
+
set({
|
|
226
|
+
clashPresets: presets,
|
|
227
|
+
clashMode: settings.mode,
|
|
228
|
+
clashTolerance: settings.tolerance,
|
|
229
|
+
clashClearance: settings.clearance,
|
|
230
|
+
clashClusterEpsilon: settings.clusterEpsilon,
|
|
231
|
+
clashReportTouch: settings.reportTouch,
|
|
232
|
+
clashGroupBy: settings.groupBy,
|
|
233
|
+
});
|
|
234
|
+
// Persist so the activated flavor's config becomes the working set on reload.
|
|
235
|
+
savePresets(presets);
|
|
236
|
+
saveSettings(settings);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
clearClash: () =>
|
|
240
|
+
// Keep presets + settings (workspace prefs, like saved lenses): only the
|
|
241
|
+
// run result/panel state is cleared.
|
|
242
|
+
set({
|
|
243
|
+
clashResult: null,
|
|
244
|
+
clashGroups: null,
|
|
245
|
+
clashRunning: false,
|
|
246
|
+
clashError: null,
|
|
247
|
+
clashProgress: null,
|
|
248
|
+
clashSelectedId: null,
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describe, it, beforeEach } from 'node:test';
|
|
6
6
|
import assert from 'node:assert';
|
|
7
7
|
import { createVisibilitySlice, type VisibilitySlice } from './visibilitySlice.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getPersistedTypeVisibility } from '../constants.js';
|
|
9
9
|
|
|
10
10
|
describe('VisibilitySlice', () => {
|
|
11
11
|
let state: VisibilitySlice;
|
|
@@ -33,10 +33,13 @@ describe('VisibilitySlice', () => {
|
|
|
33
33
|
assert.strictEqual(state.isolatedEntitiesByModel.size, 0);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('should
|
|
37
|
-
|
|
38
|
-
assert.strictEqual(state.typeVisibility.
|
|
39
|
-
assert.strictEqual(state.typeVisibility.
|
|
36
|
+
it('should initialise type visibility from persisted preferences', () => {
|
|
37
|
+
const persisted = getPersistedTypeVisibility();
|
|
38
|
+
assert.strictEqual(state.typeVisibility.spaces, persisted.spaces);
|
|
39
|
+
assert.strictEqual(state.typeVisibility.openings, persisted.openings);
|
|
40
|
+
assert.strictEqual(state.typeVisibility.site, persisted.site);
|
|
41
|
+
assert.strictEqual(state.typeVisibility.ifcAnnotations, persisted.ifcAnnotations);
|
|
42
|
+
assert.strictEqual(state.typeVisibility.ifcGrid, persisted.ifcGrid);
|
|
40
43
|
});
|
|
41
44
|
});
|
|
42
45
|
|
|
@@ -306,5 +309,20 @@ describe('VisibilitySlice', () => {
|
|
|
306
309
|
state.toggleTypeVisibility('ifcAnnotations');
|
|
307
310
|
assert.strictEqual(state.typeVisibility.ifcAnnotations, !initial);
|
|
308
311
|
});
|
|
312
|
+
|
|
313
|
+
it('resetTypeVisibility restores semantic defaults', () => {
|
|
314
|
+
// Flip everything away from defaults first.
|
|
315
|
+
state.toggleTypeVisibility('spaces'); // false -> true
|
|
316
|
+
state.toggleTypeVisibility('site'); // true -> false
|
|
317
|
+
state.toggleTypeVisibility('ifcGrid'); // true -> false
|
|
318
|
+
state.resetTypeVisibility();
|
|
319
|
+
assert.deepStrictEqual(state.typeVisibility, {
|
|
320
|
+
spaces: false,
|
|
321
|
+
openings: false,
|
|
322
|
+
site: true,
|
|
323
|
+
ifcAnnotations: true,
|
|
324
|
+
ifcGrid: true,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
309
327
|
});
|
|
310
328
|
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { StateCreator } from 'zustand';
|
|
13
13
|
import type { TypeVisibility, EntityRef } from '../types.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getPersistedTypeVisibility, TYPE_VISIBILITY_STORAGE_KEYS, TYPE_VISIBILITY_SEMANTIC_DEFAULTS } from '../constants.js';
|
|
15
15
|
|
|
16
16
|
export interface VisibilitySlice {
|
|
17
17
|
// State (legacy - single model)
|
|
@@ -43,7 +43,9 @@ export interface VisibilitySlice {
|
|
|
43
43
|
clearAllFilters: () => void;
|
|
44
44
|
showAll: () => void;
|
|
45
45
|
isEntityVisible: (id: number) => boolean;
|
|
46
|
-
toggleTypeVisibility: (type: 'spaces' | 'openings' | 'site' | 'ifcAnnotations') => void;
|
|
46
|
+
toggleTypeVisibility: (type: 'spaces' | 'openings' | 'site' | 'ifcAnnotations' | 'ifcGrid') => void;
|
|
47
|
+
/** Restore every type-visibility toggle to its semantic default (and persist). */
|
|
48
|
+
resetTypeVisibility: () => void;
|
|
47
49
|
/** Set all hidden entities at once (for BCF viewpoint application) */
|
|
48
50
|
setHiddenEntities: (ids: Set<number>) => void;
|
|
49
51
|
/** Set all isolated entities at once (for BCF viewpoint with defaultVisibility=false) */
|
|
@@ -75,12 +77,8 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
|
|
|
75
77
|
hiddenEntities: new Set(),
|
|
76
78
|
isolatedEntities: null,
|
|
77
79
|
classFilter: null,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
openings: TYPE_VISIBILITY_DEFAULTS.OPENINGS,
|
|
81
|
-
site: TYPE_VISIBILITY_DEFAULTS.SITE,
|
|
82
|
-
ifcAnnotations: TYPE_VISIBILITY_DEFAULTS.IFC_ANNOTATIONS,
|
|
83
|
-
},
|
|
80
|
+
// Read persisted toggles fresh so the user's choices survive reloads.
|
|
81
|
+
typeVisibility: getPersistedTypeVisibility(),
|
|
84
82
|
|
|
85
83
|
// Initial state (multi-model)
|
|
86
84
|
hiddenEntitiesByModel: new Map(),
|
|
@@ -210,6 +208,19 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
|
|
|
210
208
|
};
|
|
211
209
|
}),
|
|
212
210
|
|
|
211
|
+
resetTypeVisibility: () => set(() => {
|
|
212
|
+
// Restore semantic defaults and persist them per-key (same storage
|
|
213
|
+
// pattern as toggleTypeVisibility) so the reset survives reloads.
|
|
214
|
+
if (typeof window !== 'undefined') {
|
|
215
|
+
(Object.keys(TYPE_VISIBILITY_STORAGE_KEYS) as (keyof typeof TYPE_VISIBILITY_STORAGE_KEYS)[])
|
|
216
|
+
.forEach((key) => {
|
|
217
|
+
try { localStorage.setItem(TYPE_VISIBILITY_STORAGE_KEYS[key], String(TYPE_VISIBILITY_SEMANTIC_DEFAULTS[key])); }
|
|
218
|
+
catch { /* private-mode storage rejection — non-fatal */ }
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { typeVisibility: { ...TYPE_VISIBILITY_SEMANTIC_DEFAULTS } };
|
|
222
|
+
}),
|
|
223
|
+
|
|
213
224
|
// Actions (multi-model)
|
|
214
225
|
hideEntityInModel: (modelId, expressId) => set((state) => {
|
|
215
226
|
const newMap = new Map(state.hiddenEntitiesByModel);
|
package/src/store/types.ts
CHANGED
|
@@ -199,6 +199,15 @@ export interface TypeVisibility {
|
|
|
199
199
|
site: boolean;
|
|
200
200
|
/** IfcAnnotation (2D symbolic curves) - on by default when present */
|
|
201
201
|
ifcAnnotations: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* IfcGrid axis lines + bubble tags — split from `ifcAnnotations`
|
|
204
|
+
* (issue #862). Default true to match the legacy combined behaviour;
|
|
205
|
+
* users with dense grids that obscure components can hide grids while
|
|
206
|
+
* keeping annotations on. Unlike `ifcAnnotations`, grids are also
|
|
207
|
+
* section-clipped when a 3D section plane is active so each storey's
|
|
208
|
+
* grid lines only show for storeys near the cut.
|
|
209
|
+
*/
|
|
210
|
+
ifcGrid: boolean;
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
// ============================================================================
|