@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
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from './ingest/pointCloudIngest.js';
|
|
41
41
|
import { getGlobalRenderer } from './useBCF.js';
|
|
42
42
|
import { readNativeFile, type NativeFileHandle } from '../services/file-dialog.js';
|
|
43
|
-
import { getEffectiveGeoreference, getEffectiveHorizontalScale, type GeorefMutationDataLike } from '../lib/geo/effective-georef.js';
|
|
43
|
+
import { getEffectiveGeoreference, getEffectiveHorizontalScale, hasStandardGeoreferencing, type GeorefMutationDataLike } from '../lib/geo/effective-georef.js';
|
|
44
44
|
import { resolveMapUnitToMetreScale } from '../lib/geo/geo-scale.js';
|
|
45
45
|
import { resolveProjection } from '../lib/geo/reproject.js';
|
|
46
46
|
import { toast } from '../components/ui/toast.js';
|
|
@@ -105,7 +105,21 @@ function extractModelGeoref(
|
|
|
105
105
|
mutations?: GeorefMutationDataLike,
|
|
106
106
|
): ModelGeoref | null {
|
|
107
107
|
const georef = getEffectiveGeoreference(dataStore, coordinateInfo, mutations);
|
|
108
|
-
|
|
108
|
+
// Only TRUE georeferencing (real IfcMapConversion + IfcProjectedCRS) may drive
|
|
109
|
+
// federation alignment. A file with no IfcMapConversion gets a synthesised
|
|
110
|
+
// `source: 'siteLocation'` georef (EPSG:4326 from IfcSite RefLatitude/Longitude/
|
|
111
|
+
// Elevation) so it can still be pinned on the location map — but those are
|
|
112
|
+
// geographic degrees plus a raw, un-unit-scaled site elevation, not a projected
|
|
113
|
+
// metric frame. buildGeorefAlignmentTransform assumes projected eastings/
|
|
114
|
+
// northings/height in metres, so feeding it site data places the second model
|
|
115
|
+
// kilometres away: the BIMcollab ARC/STR pair share a site GUID but carry
|
|
116
|
+
// RefElevation 0 vs 20000 mm, and the height term lands ARC ~20 km below STR.
|
|
117
|
+
// Such models have no real georef relationship, so leave them in their own local
|
|
118
|
+
// frames where they overlay correctly. hasStandardGeoreferencing() excludes
|
|
119
|
+
// 'siteLocation' (see effective-georef.test.ts). (Regression from #658.)
|
|
120
|
+
if (!hasStandardGeoreferencing(georef) || !georef?.mapConversion || !georef.projectedCRS?.name) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
109
123
|
return {
|
|
110
124
|
mapConversion: georef.mapConversion,
|
|
111
125
|
projectedCRS: georef.projectedCRS,
|
|
@@ -55,6 +55,7 @@ import { useIfcCache, getCached } from './useIfcCache.js';
|
|
|
55
55
|
import { useIfcServer } from './useIfcServer.js';
|
|
56
56
|
|
|
57
57
|
import { getMaxExpressId, parseGlbViewerModel, parseIfcxViewerModel } from './ingest/viewerModelIngest.js';
|
|
58
|
+
import { boundedIteratorReturn } from './ingest/streamCleanup.js';
|
|
58
59
|
import { detectPointCloudFormat, ingestPointCloud } from './ingest/pointCloudIngest.js';
|
|
59
60
|
import { getGlobalRenderer } from './useBCF.js';
|
|
60
61
|
|
|
@@ -2053,13 +2054,10 @@ export function useIfcLoader() {
|
|
|
2053
2054
|
closeGeometryIterator = async () => {
|
|
2054
2055
|
if (geometryIteratorClosed || typeof geometryIterator.return !== 'function') return;
|
|
2055
2056
|
geometryIteratorClosed = true;
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
} catch {
|
|
2061
|
-
// Ignore iterator shutdown failures during recovery.
|
|
2062
|
-
}
|
|
2057
|
+
// Bound the shutdown: `return()` cannot interrupt a generator parked
|
|
2058
|
+
// on a stalled worker await, so an unbounded await would re-wedge on
|
|
2059
|
+
// the very stall the watchdog escaped. See boundedIteratorReturn.
|
|
2060
|
+
await boundedIteratorReturn(geometryIterator);
|
|
2063
2061
|
};
|
|
2064
2062
|
|
|
2065
2063
|
while (true) {
|
|
@@ -0,0 +1,308 @@
|
|
|
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
|
+
* localStorage persistence for clash detection settings + the user's rule preset
|
|
7
|
+
* set. Mirrors the lens slice's "built-ins + overrides + custom" model and the
|
|
8
|
+
* scripts module's quota-safe `SaveResult`:
|
|
9
|
+
*
|
|
10
|
+
* - Presets: the built-in `CLASH_RULE_PRESETS` are always present (projected to
|
|
11
|
+
* editable items with `enabled`/`builtin`); the user may toggle/edit them
|
|
12
|
+
* (stored as overrides) and add custom presets. Only customs + modified
|
|
13
|
+
* built-ins are persisted, so shipping a new built-in just works.
|
|
14
|
+
* - Settings: one flat JSON blob (mode/tolerance/clearance/clusterEpsilon/
|
|
15
|
+
* reportTouch/groupBy), every numeric clamped to a sane range on load.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
CLASH_RULE_PRESETS,
|
|
20
|
+
type ClashRulePreset,
|
|
21
|
+
type ClashMode,
|
|
22
|
+
type ClashSeverity,
|
|
23
|
+
} from '@ifc-lite/clash';
|
|
24
|
+
|
|
25
|
+
/** A built-in or user-defined clash rule preset, with editor/runtime flags. */
|
|
26
|
+
export type ClashPreset = ClashRulePreset & { enabled: boolean; builtin: boolean };
|
|
27
|
+
|
|
28
|
+
/** How the panel groups the flat clash list (display only). */
|
|
29
|
+
export type ClashSettingsGroupBy = 'severity' | 'rule' | 'typePair';
|
|
30
|
+
|
|
31
|
+
/** Global detection settings, persisted as one blob. */
|
|
32
|
+
export interface ClashGlobalSettings {
|
|
33
|
+
mode: ClashMode;
|
|
34
|
+
tolerance: number;
|
|
35
|
+
clearance: number;
|
|
36
|
+
clusterEpsilon: number;
|
|
37
|
+
reportTouch: boolean;
|
|
38
|
+
groupBy: ClashSettingsGroupBy;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SaveResult =
|
|
42
|
+
| { ok: true }
|
|
43
|
+
| { ok: false; reason: 'quota' | 'serialize' | 'too_many'; message: string };
|
|
44
|
+
|
|
45
|
+
const PRESETS_KEY = 'ifc-lite-clash-presets';
|
|
46
|
+
const SETTINGS_KEY = 'ifc-lite-clash-settings';
|
|
47
|
+
const SCHEMA_VERSION = 1;
|
|
48
|
+
|
|
49
|
+
const MAX_PRESETS = 200;
|
|
50
|
+
const MAX_NAME = 100;
|
|
51
|
+
|
|
52
|
+
/** [min, max] clamps applied to settings numerics on load and on commit. */
|
|
53
|
+
export const CLASH_BOUNDS = {
|
|
54
|
+
tolerance: [0, 1] as const,
|
|
55
|
+
clearance: [0, 5] as const,
|
|
56
|
+
clusterEpsilon: [0.01, 50] as const,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_CLASH_SETTINGS: ClashGlobalSettings = {
|
|
60
|
+
mode: 'hard',
|
|
61
|
+
tolerance: 0.002,
|
|
62
|
+
clearance: 0.05,
|
|
63
|
+
clusterEpsilon: 1.5,
|
|
64
|
+
reportTouch: false,
|
|
65
|
+
groupBy: 'severity',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const BUILTIN_PRESET_IDS = new Set(CLASH_RULE_PRESETS.map((p) => p.id));
|
|
69
|
+
const SEVERITIES: ClashSeverity[] = ['critical', 'major', 'minor', 'info'];
|
|
70
|
+
const GROUP_BYS: ClashSettingsGroupBy[] = ['severity', 'rule', 'typePair'];
|
|
71
|
+
|
|
72
|
+
export function clampToBounds(value: unknown, [min, max]: readonly [number, number], fallback: number): number {
|
|
73
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
74
|
+
if (!Number.isFinite(n)) return fallback;
|
|
75
|
+
return Math.min(max, Math.max(min, n));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Trim + length-cap a preset name; null if empty (invalid). */
|
|
79
|
+
export function validatePresetName(name: string): string | null {
|
|
80
|
+
const t = name.trim();
|
|
81
|
+
return t ? t.slice(0, MAX_NAME) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Trim a selector; null if empty (invalid). An empty selector matches everything. */
|
|
85
|
+
export function validateSelector(selector: string): string | null {
|
|
86
|
+
const t = selector.trim();
|
|
87
|
+
return t ? t : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isValidStoredPreset(p: unknown): p is ClashPreset {
|
|
91
|
+
if (!p || typeof p !== 'object') return false;
|
|
92
|
+
const r = p as Record<string, unknown>;
|
|
93
|
+
return (
|
|
94
|
+
typeof r.id === 'string' && r.id.length > 0 &&
|
|
95
|
+
typeof r.name === 'string' && r.name.trim().length > 0 &&
|
|
96
|
+
typeof r.selectorA === 'string' && r.selectorA.trim().length > 0 &&
|
|
97
|
+
typeof r.selectorB === 'string' && r.selectorB.trim().length > 0 &&
|
|
98
|
+
typeof r.severity === 'string' && SEVERITIES.includes(r.severity as ClashSeverity)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read stored presets, accepting the versioned wrapper or a legacy bare array. */
|
|
103
|
+
function readStoredPresets(): ClashPreset[] {
|
|
104
|
+
try {
|
|
105
|
+
const raw = localStorage.getItem(PRESETS_KEY);
|
|
106
|
+
if (!raw) return [];
|
|
107
|
+
const parsed: unknown = JSON.parse(raw);
|
|
108
|
+
const list = Array.isArray(parsed)
|
|
109
|
+
? parsed
|
|
110
|
+
: parsed && typeof parsed === 'object' && Array.isArray((parsed as { presets?: unknown }).presets)
|
|
111
|
+
? (parsed as { presets: unknown[] }).presets
|
|
112
|
+
: [];
|
|
113
|
+
return list
|
|
114
|
+
.filter(isValidStoredPreset)
|
|
115
|
+
.map((p) => ({
|
|
116
|
+
id: p.id,
|
|
117
|
+
name: p.name,
|
|
118
|
+
description: typeof p.description === 'string' ? p.description : '',
|
|
119
|
+
severity: p.severity,
|
|
120
|
+
selectorA: p.selectorA,
|
|
121
|
+
selectorB: p.selectorB,
|
|
122
|
+
enabled: p.enabled !== false,
|
|
123
|
+
builtin: BUILTIN_PRESET_IDS.has(p.id),
|
|
124
|
+
}));
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The pristine built-in preset set, no overrides/customs — the "reset" target. */
|
|
131
|
+
export function defaultPresets(): ClashPreset[] {
|
|
132
|
+
return CLASH_RULE_PRESETS.map((p) => ({ ...p, enabled: true, builtin: true }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The full preset list shown to the user: every built-in (with any saved
|
|
137
|
+
* override applied) followed by custom presets. Built-ins are always present
|
|
138
|
+
* even if storage is empty or dropped them.
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a stored (customs + modified-built-ins) list into the full preset
|
|
142
|
+
* list: every built-in (with any override applied), then customs. Built-ins are
|
|
143
|
+
* always present, so a list from an older app version still picks up new ones.
|
|
144
|
+
*/
|
|
145
|
+
export function mergeStoredPresets(stored: ClashPreset[]): ClashPreset[] {
|
|
146
|
+
const overrides = new Map(stored.filter((p) => p.builtin).map((p) => [p.id, p]));
|
|
147
|
+
const builtins: ClashPreset[] = CLASH_RULE_PRESETS.map(
|
|
148
|
+
(p) => overrides.get(p.id) ?? { ...p, enabled: true, builtin: true },
|
|
149
|
+
);
|
|
150
|
+
const custom = stored.filter((p) => !p.builtin);
|
|
151
|
+
return [...builtins, ...custom];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildInitialPresets(): ClashPreset[] {
|
|
155
|
+
return mergeStoredPresets(readStoredPresets());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function builtinDiffersFromDefault(p: ClashPreset): boolean {
|
|
159
|
+
const orig = CLASH_RULE_PRESETS.find((b) => b.id === p.id);
|
|
160
|
+
if (!orig) return true;
|
|
161
|
+
return (
|
|
162
|
+
!p.enabled ||
|
|
163
|
+
p.name !== orig.name ||
|
|
164
|
+
p.severity !== orig.severity ||
|
|
165
|
+
p.selectorA !== orig.selectorA ||
|
|
166
|
+
p.selectorB !== orig.selectorB ||
|
|
167
|
+
p.description !== orig.description
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The minimal stored shape: customs + only the built-ins that differ from default. */
|
|
172
|
+
export function presetsToStore(presets: ClashPreset[]): ClashPreset[] {
|
|
173
|
+
return [
|
|
174
|
+
...presets.filter((p) => !p.builtin),
|
|
175
|
+
...presets.filter((p) => p.builtin && builtinDiffersFromDefault(p)),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Persist only custom presets + modified built-ins (quota-safe). */
|
|
180
|
+
export function savePresets(presets: ClashPreset[]): SaveResult {
|
|
181
|
+
const custom = presets.filter((p) => !p.builtin);
|
|
182
|
+
if (custom.length > MAX_PRESETS) {
|
|
183
|
+
return { ok: false, reason: 'too_many', message: `Too many custom rules (max ${MAX_PRESETS}).` };
|
|
184
|
+
}
|
|
185
|
+
const toStore = presetsToStore(presets);
|
|
186
|
+
let payload: string;
|
|
187
|
+
try {
|
|
188
|
+
payload = JSON.stringify({ schemaVersion: SCHEMA_VERSION, presets: toStore });
|
|
189
|
+
} catch {
|
|
190
|
+
return { ok: false, reason: 'serialize', message: 'Could not serialize clash rules.' };
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
localStorage.setItem(PRESETS_KEY, payload);
|
|
194
|
+
return { ok: true };
|
|
195
|
+
} catch {
|
|
196
|
+
return { ok: false, reason: 'quota', message: 'Browser storage is full — clash rules were not saved.' };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Coerce arbitrary input into valid, bounds-clamped settings (defaults on junk). */
|
|
201
|
+
export function normalizeSettings(raw: unknown): ClashGlobalSettings {
|
|
202
|
+
const s = (raw && typeof raw === 'object' && 'settings' in raw
|
|
203
|
+
? (raw as { settings: unknown }).settings
|
|
204
|
+
: raw) as Partial<ClashGlobalSettings> | null;
|
|
205
|
+
if (!s || typeof s !== 'object') return { ...DEFAULT_CLASH_SETTINGS };
|
|
206
|
+
return {
|
|
207
|
+
mode: s.mode === 'clearance' ? 'clearance' : 'hard',
|
|
208
|
+
tolerance: clampToBounds(s.tolerance, CLASH_BOUNDS.tolerance, DEFAULT_CLASH_SETTINGS.tolerance),
|
|
209
|
+
clearance: clampToBounds(s.clearance, CLASH_BOUNDS.clearance, DEFAULT_CLASH_SETTINGS.clearance),
|
|
210
|
+
clusterEpsilon: clampToBounds(s.clusterEpsilon, CLASH_BOUNDS.clusterEpsilon, DEFAULT_CLASH_SETTINGS.clusterEpsilon),
|
|
211
|
+
reportTouch: s.reportTouch === true,
|
|
212
|
+
groupBy: GROUP_BYS.includes(s.groupBy as ClashSettingsGroupBy) ? (s.groupBy as ClashSettingsGroupBy) : 'severity',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function loadSettings(): ClashGlobalSettings {
|
|
217
|
+
try {
|
|
218
|
+
const raw = localStorage.getItem(SETTINGS_KEY);
|
|
219
|
+
if (!raw) return { ...DEFAULT_CLASH_SETTINGS };
|
|
220
|
+
return normalizeSettings(JSON.parse(raw));
|
|
221
|
+
} catch {
|
|
222
|
+
return { ...DEFAULT_CLASH_SETTINGS };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function saveSettings(settings: ClashGlobalSettings): SaveResult {
|
|
227
|
+
try {
|
|
228
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ schemaVersion: SCHEMA_VERSION, settings }));
|
|
229
|
+
return { ok: true };
|
|
230
|
+
} catch {
|
|
231
|
+
return { ok: false, reason: 'quota', message: 'Browser storage is full — clash settings were not saved.' };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Download the user's presets (customs + modified built-ins) as a JSON file. */
|
|
236
|
+
export function exportPresets(presets: ClashPreset[]): void {
|
|
237
|
+
const custom = presets.filter((p) => !p.builtin || builtinDiffersFromDefault(p));
|
|
238
|
+
const blob = new Blob([JSON.stringify({ schemaVersion: SCHEMA_VERSION, presets: custom }, null, 2)], {
|
|
239
|
+
type: 'application/json',
|
|
240
|
+
});
|
|
241
|
+
const url = URL.createObjectURL(blob);
|
|
242
|
+
const a = document.createElement('a');
|
|
243
|
+
a.href = url;
|
|
244
|
+
a.download = 'clash-rules.clash-presets.json';
|
|
245
|
+
a.click();
|
|
246
|
+
URL.revokeObjectURL(url);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Parse an exported file into custom presets (ids regenerated, `builtin` stripped). */
|
|
250
|
+
export async function importPresets(file: File): Promise<ClashPreset[]> {
|
|
251
|
+
const text = await file.text();
|
|
252
|
+
const parsed: unknown = JSON.parse(text);
|
|
253
|
+
const list = Array.isArray(parsed)
|
|
254
|
+
? parsed
|
|
255
|
+
: parsed && typeof parsed === 'object' && Array.isArray((parsed as { presets?: unknown }).presets)
|
|
256
|
+
? (parsed as { presets: unknown[] }).presets
|
|
257
|
+
: [];
|
|
258
|
+
return list.filter(isValidStoredPreset).map((p) => ({
|
|
259
|
+
id: `custom-${crypto.randomUUID()}`,
|
|
260
|
+
name: p.name.slice(0, MAX_NAME),
|
|
261
|
+
description: typeof p.description === 'string' ? p.description : '',
|
|
262
|
+
severity: p.severity,
|
|
263
|
+
selectorA: p.selectorA,
|
|
264
|
+
selectorB: p.selectorB,
|
|
265
|
+
enabled: p.enabled !== false,
|
|
266
|
+
builtin: false,
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Flavor integration ───────────────────────────────────────────────────────
|
|
271
|
+
// Clash config rides inside a flavor's generic `settings.clash` blob, so each
|
|
272
|
+
// flavor/profile carries its own rule-set + detection settings (and they travel
|
|
273
|
+
// with flavor export/import). Serialize stores the minimal shape (customs +
|
|
274
|
+
// modified built-ins + settings); deserialize rebuilds the full, validated state.
|
|
275
|
+
|
|
276
|
+
/** Plain-JSON snapshot of clash config stored in a flavor. */
|
|
277
|
+
export interface ClashFlavorConfig {
|
|
278
|
+
schemaVersion: number;
|
|
279
|
+
settings: ClashGlobalSettings;
|
|
280
|
+
/** Customs + modified built-ins only (built-ins are re-merged on restore). */
|
|
281
|
+
presets: ClashPreset[];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function serializeClashConfig(presets: ClashPreset[], settings: ClashGlobalSettings): ClashFlavorConfig {
|
|
285
|
+
return { schemaVersion: SCHEMA_VERSION, settings: { ...settings }, presets: presetsToStore(presets) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Rebuild clash state from a flavor blob: the full resolved preset list (defaults
|
|
290
|
+
* + the blob's overrides/customs) and bounds-clamped settings. Returns null when
|
|
291
|
+
* the blob is missing/garbage so the caller can skip the restore.
|
|
292
|
+
*/
|
|
293
|
+
export function deserializeClashConfig(blob: unknown): { presets: ClashPreset[]; settings: ClashGlobalSettings } | null {
|
|
294
|
+
if (!blob || typeof blob !== 'object') return null;
|
|
295
|
+
const b = blob as Partial<ClashFlavorConfig>;
|
|
296
|
+
const storedRaw = Array.isArray(b.presets) ? b.presets : [];
|
|
297
|
+
const stored = storedRaw.filter(isValidStoredPreset).map((p) => ({
|
|
298
|
+
id: p.id,
|
|
299
|
+
name: p.name,
|
|
300
|
+
description: typeof p.description === 'string' ? p.description : '',
|
|
301
|
+
severity: p.severity,
|
|
302
|
+
selectorA: p.selectorA,
|
|
303
|
+
selectorB: p.selectorB,
|
|
304
|
+
enabled: p.enabled !== false,
|
|
305
|
+
builtin: BUILTIN_PRESET_IDS.has(p.id),
|
|
306
|
+
}));
|
|
307
|
+
return { presets: mergeStoredPresets(stored), settings: normalizeSettings(b.settings) };
|
|
308
|
+
}
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
* Model length-unit → metres scale, memoised per `IfcDataStore`.
|
|
7
|
+
*
|
|
8
|
+
* The viewer's render + authoring space is **metres**: the geometry
|
|
9
|
+
* pipeline bakes the file's length-unit scale into tessellated vertices,
|
|
10
|
+
* raycast hit-points come back in metres, and `spatialHierarchy.
|
|
11
|
+
* storeyElevations` are pre-scaled. But raw coordinate reads straight off
|
|
12
|
+
* the STEP model — split footprints, placement chains — arrive in the
|
|
13
|
+
* file's **native** units (e.g. millimetres). Multiply those by this
|
|
14
|
+
* factor to bring them into the same metre space as everything else.
|
|
15
|
+
*
|
|
16
|
+
* Returns `1` when the scale can't be determined (already-metres models,
|
|
17
|
+
* or bounded-geometry mode having released the source buffer) — the
|
|
18
|
+
* safe identity that leaves native-unit reads untouched.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
22
|
+
|
|
23
|
+
const scaleCache = new WeakMap<IfcDataStore, number>();
|
|
24
|
+
|
|
25
|
+
export function getModelLengthUnitScale(dataStore: IfcDataStore | null | undefined): number {
|
|
26
|
+
if (!dataStore) return 1;
|
|
27
|
+
const cached = scaleCache.get(dataStore);
|
|
28
|
+
if (cached !== undefined) return cached;
|
|
29
|
+
|
|
30
|
+
// The columnar parser stashes the scale on the store; the wasm fast
|
|
31
|
+
// path does not, so fall back to extracting it from the source bytes.
|
|
32
|
+
let scale = typeof dataStore.lengthUnitScale === 'number' ? dataStore.lengthUnitScale : undefined;
|
|
33
|
+
if (scale === undefined || !Number.isFinite(scale) || scale <= 0) {
|
|
34
|
+
if (!dataStore.source?.length || !dataStore.entityIndex) return 1;
|
|
35
|
+
scale = extractLengthUnitScale(dataStore.source, dataStore.entityIndex);
|
|
36
|
+
}
|
|
37
|
+
if (!Number.isFinite(scale) || scale <= 0) scale = 1;
|
|
38
|
+
|
|
39
|
+
scaleCache.set(dataStore, scale);
|
|
40
|
+
return scale;
|
|
41
|
+
}
|
package/src/lib/lists/adapter.ts
CHANGED
|
@@ -11,10 +11,31 @@
|
|
|
11
11
|
* and Tag which are not stored during the fast initial parse.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
15
|
-
import {
|
|
14
|
+
import type { IfcDataStore, MaterialInfo } from '@ifc-lite/parser';
|
|
15
|
+
import {
|
|
16
|
+
extractPropertiesOnDemand,
|
|
17
|
+
extractQuantitiesOnDemand,
|
|
18
|
+
extractEntityAttributesOnDemand,
|
|
19
|
+
extractMaterialsOnDemand,
|
|
20
|
+
extractClassificationsOnDemand,
|
|
21
|
+
} from '@ifc-lite/parser';
|
|
16
22
|
import type { PropertySet, QuantitySet } from '@ifc-lite/data';
|
|
17
|
-
import
|
|
23
|
+
import { ENTITY_ATTRIBUTES } from '@ifc-lite/lists';
|
|
24
|
+
import type { ListDataProvider, ListClassificationRef, DiscoveredColumns } from '@ifc-lite/lists';
|
|
25
|
+
|
|
26
|
+
/** Collect every material-name string an element exposes — top-level
|
|
27
|
+
* material plus layer / constituent / profile names and list members. */
|
|
28
|
+
function materialNamesOf(info: MaterialInfo | null): string[] {
|
|
29
|
+
if (!info) return [];
|
|
30
|
+
const names: string[] = [];
|
|
31
|
+
const push = (s: string | undefined) => { if (s) names.push(s); };
|
|
32
|
+
push(info.name);
|
|
33
|
+
for (const l of info.layers ?? []) { push(l.materialName); push(l.name); }
|
|
34
|
+
for (const c of info.constituents ?? []) { push(c.materialName); push(c.name); }
|
|
35
|
+
for (const p of info.profiles ?? []) { push(p.materialName); push(p.name); }
|
|
36
|
+
for (const m of info.materials ?? []) push(m.name);
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
18
39
|
|
|
19
40
|
/**
|
|
20
41
|
* Create a ListDataProvider backed by an IfcDataStore.
|
|
@@ -26,6 +47,10 @@ export function createListDataProvider(store: IfcDataStore): ListDataProvider {
|
|
|
26
47
|
// but are needed for list display. Cache avoids re-parsing per column.
|
|
27
48
|
const attrCache = new Map<number, { description: string; objectType: string; tag: string }>();
|
|
28
49
|
|
|
50
|
+
// Lazily materialised list of every non-empty express id — used for
|
|
51
|
+
// class-less list targeting. Cached because the provider outlives a run.
|
|
52
|
+
let allIdsCache: number[] | null = null;
|
|
53
|
+
|
|
29
54
|
function getOnDemandAttrs(id: number): { description: string; objectType: string; tag: string } {
|
|
30
55
|
const cached = attrCache.get(id);
|
|
31
56
|
if (cached) return cached;
|
|
@@ -42,6 +67,23 @@ export function createListDataProvider(store: IfcDataStore): ListDataProvider {
|
|
|
42
67
|
return empty;
|
|
43
68
|
}
|
|
44
69
|
|
|
70
|
+
// Complete column discovery is cached — the provider outlives a builder
|
|
71
|
+
// open, and the scan touches every entity that declares a pset/qto.
|
|
72
|
+
let columnsCache: DiscoveredColumns | null = null;
|
|
73
|
+
|
|
74
|
+
const usesOnDemandProps = !!store.onDemandPropertyMap && store.source?.length > 0;
|
|
75
|
+
const usesOnDemandQtos = !!store.onDemandQuantityMap && store.source?.length > 0;
|
|
76
|
+
|
|
77
|
+
function getPropertySetsFor(entityId: number): PropertySet[] {
|
|
78
|
+
if (usesOnDemandProps) return extractPropertiesOnDemand(store, entityId) as PropertySet[];
|
|
79
|
+
return store.properties?.getForEntity(entityId) ?? [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getQuantitySetsFor(entityId: number): QuantitySet[] {
|
|
83
|
+
if (usesOnDemandQtos) return extractQuantitiesOnDemand(store, entityId) as QuantitySet[];
|
|
84
|
+
return store.quantities?.getForEntity(entityId) ?? [];
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
return {
|
|
46
88
|
getEntitiesByType: (type) => store.entities.getByType(type),
|
|
47
89
|
|
|
@@ -52,18 +94,101 @@ export function createListDataProvider(store: IfcDataStore): ListDataProvider {
|
|
|
52
94
|
getEntityTag: (id) => getOnDemandAttrs(id).tag,
|
|
53
95
|
getEntityTypeName: (id) => store.entities.getTypeName(id),
|
|
54
96
|
|
|
55
|
-
getPropertySets
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
getPropertySets: getPropertySetsFor,
|
|
98
|
+
getQuantitySets: getQuantitySetsFor,
|
|
99
|
+
|
|
100
|
+
getAllEntityIds(): number[] {
|
|
101
|
+
if (allIdsCache) return allIdsCache;
|
|
102
|
+
// Restrict "all elements" to geometry-bearing (selectable) products.
|
|
103
|
+
// The raw expressId column also holds relationships, property sets,
|
|
104
|
+
// materials, classifications and other non-element records — a
|
|
105
|
+
// class-less list should not surface those as rows.
|
|
106
|
+
const ids: number[] = [];
|
|
107
|
+
const col = store.entities.expressId;
|
|
108
|
+
for (let i = 0; i < col.length; i++) {
|
|
109
|
+
const id = col[i];
|
|
110
|
+
if (id && store.entities.hasGeometry(id)) ids.push(id);
|
|
58
111
|
}
|
|
59
|
-
|
|
112
|
+
allIdsCache = ids;
|
|
113
|
+
return ids;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
getMaterialNames(entityId: number): string[] {
|
|
117
|
+
return materialNamesOf(extractMaterialsOnDemand(store, entityId));
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
getClassifications(entityId: number): ListClassificationRef[] {
|
|
121
|
+
return extractClassificationsOnDemand(store, entityId).map((c) => ({
|
|
122
|
+
system: c.system,
|
|
123
|
+
code: c.identification,
|
|
124
|
+
name: c.name,
|
|
125
|
+
}));
|
|
60
126
|
},
|
|
61
127
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
128
|
+
getStoreyName(entityId: number): string {
|
|
129
|
+
const hierarchy = store.spatialHierarchy;
|
|
130
|
+
if (!hierarchy) return '';
|
|
131
|
+
const storeyId = hierarchy.elementToStorey.get(entityId);
|
|
132
|
+
if (!storeyId) return '';
|
|
133
|
+
return store.entities.getName(storeyId) || '';
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
discoverAllColumns(): DiscoveredColumns {
|
|
137
|
+
if (columnsCache) return columnsCache;
|
|
138
|
+
|
|
139
|
+
const properties = new Map<string, Set<string>>();
|
|
140
|
+
const quantities = new Map<string, Set<string>>();
|
|
141
|
+
|
|
142
|
+
const ingestProps = (id: number) => {
|
|
143
|
+
for (const set of getPropertySetsFor(id)) {
|
|
144
|
+
if (!set.name) continue;
|
|
145
|
+
let bucket = properties.get(set.name);
|
|
146
|
+
if (!bucket) { bucket = new Set(); properties.set(set.name, bucket); }
|
|
147
|
+
for (const p of set.properties) if (p.name) bucket.add(p.name);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const ingestQtos = (id: number) => {
|
|
151
|
+
for (const set of getQuantitySetsFor(id)) {
|
|
152
|
+
if (!set.name) continue;
|
|
153
|
+
let bucket = quantities.get(set.name);
|
|
154
|
+
if (!bucket) { bucket = new Set(); quantities.set(set.name, bucket); }
|
|
155
|
+
for (const q of set.quantities) if (q.name) bucket.add(q.name);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// On-demand path: scan exactly the entities that declare a pset/qto —
|
|
160
|
+
// the minimal complete set (every distinct set/property in the model).
|
|
161
|
+
if (usesOnDemandProps && store.onDemandPropertyMap) {
|
|
162
|
+
for (const id of store.onDemandPropertyMap.keys()) ingestProps(id);
|
|
163
|
+
}
|
|
164
|
+
if (usesOnDemandQtos && store.onDemandQuantityMap) {
|
|
165
|
+
for (const id of store.onDemandQuantityMap.keys()) ingestQtos(id);
|
|
65
166
|
}
|
|
66
|
-
|
|
167
|
+
// Table path (e.g. server-loaded models): scan the entity column using
|
|
168
|
+
// the pre-built tables. Capped so it can't run away on huge models.
|
|
169
|
+
if (!usesOnDemandProps || !usesOnDemandQtos) {
|
|
170
|
+
const col = store.entities.expressId;
|
|
171
|
+
const CAP = 100_000;
|
|
172
|
+
for (let i = 0, seen = 0; i < col.length && seen < CAP; i++) {
|
|
173
|
+
const id = col[i];
|
|
174
|
+
if (!id) continue;
|
|
175
|
+
seen++;
|
|
176
|
+
if (!usesOnDemandProps) ingestProps(id);
|
|
177
|
+
if (!usesOnDemandQtos) ingestQtos(id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const toSorted = (m: Map<string, Set<string>>) => {
|
|
182
|
+
const out = new Map<string, string[]>();
|
|
183
|
+
for (const [k, s] of m) out.set(k, Array.from(s).sort());
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
columnsCache = {
|
|
187
|
+
attributes: [...ENTITY_ATTRIBUTES],
|
|
188
|
+
properties: toSorted(properties),
|
|
189
|
+
quantities: toSorted(quantities),
|
|
190
|
+
};
|
|
191
|
+
return columnsCache;
|
|
67
192
|
},
|
|
68
193
|
};
|
|
69
194
|
}
|