@ifc-lite/viewer 1.29.0 → 1.30.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 +32 -31
- package/CHANGELOG.md +58 -0
- package/dist/assets/{basketViewActivator-BSRgF1Hw.js → basketViewActivator-BHNb23Vw.js} +6 -6
- package/dist/assets/{bcf-3uE1MvcT.js → bcf-C0XZ2DSl.js} +1 -1
- package/dist/assets/{deflate-BLlUfw9-.js → deflate-DvvFcUtV.js} +1 -1
- package/dist/assets/{exporters-BL6UmxRa.js → exporters-CvORJOLn.js} +1345 -1434
- package/dist/assets/geometry.worker-B7X9DQQY.js +1 -0
- package/dist/assets/{geotiff-BydcIud8.js → geotiff-fSD_sVw_.js} +10 -10
- package/dist/assets/{ids-DFl74rTt.js → ids-BUOe5QQl.js} +951 -713
- package/dist/assets/idsValidation.worker-DEodXb0f.js +190468 -0
- package/dist/assets/ifc-lite_bg-CmMuB1zf.wasm +0 -0
- package/dist/assets/{index-BNTlm2lP.js → index-B6T42T86.js} +35235 -32937
- package/dist/assets/index-D0tqJL0X.css +1 -0
- package/dist/assets/{index.es-Bk4nLsyS.js → index.es-YGMensDM.js} +7 -7
- package/dist/assets/{jpeg-BvMO8-Tc.js → jpeg-0Sla88_N.js} +1 -1
- package/dist/assets/{jspdf.es.min-BZ_ed66E.js → jspdf.es.min-mnbLNj-p.js} +4 -4
- package/dist/assets/{lerc-CNnDpLpV.js → lerc-C7xUDHpL.js} +1 -1
- package/dist/assets/{lzw-DBaPrGGZ.js → lzw-CK480t0_.js} +1 -1
- package/dist/assets/{native-bridge-DFOoBvTg.js → native-bridge-sLWRanza.js} +1 -1
- package/dist/assets/{packbits-C7uyD2Bi.js → packbits-DcL4imYS.js} +1 -1
- package/dist/assets/parser.worker-BsGV6ml7.js +182 -0
- package/dist/assets/{pdf-DlqdjX9e.js → pdf-BARGfLmx.js} +8 -8
- package/dist/assets/raw-BMWh6mDy.js +1 -0
- package/dist/assets/{sandbox-0Z2NzeOJ.js → sandbox-BSiO04m8.js} +2801 -2609
- package/dist/assets/server-client-AlpWMVq9.js +741 -0
- package/dist/assets/{webimage-zN-oCabb.js → webimage-uy5DjZLk.js} +1 -1
- package/dist/assets/{xlsx-N2LbIR1G.js → xlsx-D02ho69_.js} +6 -6
- package/dist/assets/{zstd-Jk3QKIeb.js → zstd-DcR1TBwT.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +27 -32
- package/src/components/viewer/CesiumOverlay.tsx +195 -8
- package/src/components/viewer/IDSPanel.tsx +23 -7
- package/src/components/viewer/MainToolbar.tsx +31 -0
- package/src/components/viewer/SunSkyPanel.tsx +363 -0
- package/src/components/viewer/Viewport.tsx +49 -1
- package/src/components/viewer/ViewportContainer.tsx +20 -1
- package/src/components/viewer/useAnimationLoop.ts +19 -1
- package/src/disable-react-dev-perf-track.ts +38 -0
- package/src/hooks/has-entity-type.ts +33 -0
- package/src/hooks/ids/idsWorkerClient.ts +136 -0
- package/src/hooks/ingest/federationAlign.ts +1 -1
- package/src/hooks/ingest/pointCloudIngest.ts +22 -98
- package/src/hooks/ingest/viewerModelIngest.ts +8 -13
- package/src/hooks/useAlignmentLines3D.ts +5 -0
- package/src/hooks/useGridLines3D.ts +4 -0
- package/src/hooks/useIDS.ts +77 -13
- package/src/hooks/useIfcCache.ts +1 -1
- package/src/hooks/useIfcFederation.ts +1 -1
- package/src/hooks/useIfcServer.ts +1 -1
- package/src/hooks/useModelSelection.ts +1 -1
- package/src/hooks/useSolarEnvironment.ts +114 -0
- package/src/hooks/useSolarSweep.ts +66 -0
- package/src/hooks/useSymbolicAnnotations.ts +10 -0
- package/src/hooks/useViewerSelectors.ts +1 -1
- package/src/lib/geo/cesium-sun.ts +277 -0
- package/src/lib/geo/solar-direction.test.ts +70 -0
- package/src/lib/geo/solar-direction.ts +94 -0
- package/src/lib/lighting-presets.ts +128 -0
- package/src/lib/recent-files.ts +4 -24
- package/src/lib/solar-time.ts +55 -0
- package/src/main.tsx +5 -0
- package/src/store/index.ts +8 -0
- package/src/store/slices/annotationsSlice.test.ts +0 -16
- package/src/store/slices/cesiumSlice.ts +3 -3
- package/src/store/slices/dataSlice.test.ts +0 -40
- package/src/store/slices/environmentSlice.ts +101 -0
- package/src/store/slices/idsSlice.ts +6 -1
- package/src/store/slices/selectionSlice.test.ts +0 -43
- package/src/store/slices/solarSlice.ts +121 -0
- package/src/store/slices/visibilitySlice.test.ts +15 -45
- package/src/utils/loadingUtils.ts +1 -1
- package/src/workers/idsValidation.worker.ts +98 -0
- package/dist/assets/geometry.worker-DVwFYHTq.js +0 -1
- package/dist/assets/ifc-lite_bg-FPffpFK_.wasm +0 -0
- package/dist/assets/index-DpoJvkdg.css +0 -1
- package/dist/assets/parser.worker-U_PVhLNi.js +0 -182
- package/dist/assets/raw-p_2cfl6T.js +0 -1
- package/dist/assets/server-client-DUMy2mXg.js +0 -719
- package/src/components/ui/context-menu.tsx +0 -174
- package/src/store.ts +0 -80
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
* Main-thread client for the IDS validation worker.
|
|
7
|
+
*
|
|
8
|
+
* Spawns the worker per run (validation is infrequent and the worker
|
|
9
|
+
* re-parses the model, so a long-lived instance would only pin memory),
|
|
10
|
+
* streams progress back to the caller, and resolves with the report.
|
|
11
|
+
* The caller falls back to in-process validation when `isSupported()`
|
|
12
|
+
* is false or the worker rejects.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
IDSDocument,
|
|
17
|
+
IDSValidationReport,
|
|
18
|
+
ValidationProgress,
|
|
19
|
+
} from '@ifc-lite/ids';
|
|
20
|
+
import type {
|
|
21
|
+
IdsWorkerRequest,
|
|
22
|
+
IdsWorkerResponse,
|
|
23
|
+
} from '@/workers/idsValidation.worker';
|
|
24
|
+
|
|
25
|
+
export function idsWorkerSupported(): boolean {
|
|
26
|
+
return typeof Worker !== 'undefined';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunInWorkerArgs {
|
|
30
|
+
/** Raw IFC/STEP bytes from the loaded model's data store. */
|
|
31
|
+
source: Uint8Array;
|
|
32
|
+
document: IDSDocument;
|
|
33
|
+
schemaVersion: string;
|
|
34
|
+
modelId: string;
|
|
35
|
+
locale: 'en' | 'de' | 'fr';
|
|
36
|
+
includePassingEntities: boolean;
|
|
37
|
+
onProgress?: (progress: ValidationProgress) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hand the model bytes + parsed IDS document to the worker and resolve
|
|
42
|
+
* with the validation report. A SharedArrayBuffer-backed source is
|
|
43
|
+
* shared zero-copy; a plain ArrayBuffer is copied and transferred so
|
|
44
|
+
* the main-thread store is never detached.
|
|
45
|
+
*/
|
|
46
|
+
export function runValidationInWorker(
|
|
47
|
+
args: RunInWorkerArgs
|
|
48
|
+
): Promise<IDSValidationReport> {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
let worker: Worker;
|
|
51
|
+
try {
|
|
52
|
+
worker = new Worker(
|
|
53
|
+
new URL('../../workers/idsValidation.worker.ts', import.meta.url),
|
|
54
|
+
{ type: 'module' }
|
|
55
|
+
);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
reject(
|
|
58
|
+
new Error(
|
|
59
|
+
`Failed to spawn IDS worker: ${err instanceof Error ? err.message : String(err)}`
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const id = Date.now();
|
|
66
|
+
const { buffer, transfer } = prepareSource(args.source);
|
|
67
|
+
|
|
68
|
+
const settle = (fn: () => void) => {
|
|
69
|
+
worker.onmessage = null;
|
|
70
|
+
worker.onerror = null;
|
|
71
|
+
worker.onmessageerror = null;
|
|
72
|
+
worker.terminate();
|
|
73
|
+
fn();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
worker.onmessage = (event: MessageEvent<IdsWorkerResponse>) => {
|
|
77
|
+
const msg = event.data;
|
|
78
|
+
if (!msg || msg.id !== id) return;
|
|
79
|
+
switch (msg.type) {
|
|
80
|
+
case 'progress':
|
|
81
|
+
args.onProgress?.(msg.progress);
|
|
82
|
+
return;
|
|
83
|
+
case 'complete':
|
|
84
|
+
settle(() => resolve(msg.report));
|
|
85
|
+
return;
|
|
86
|
+
case 'error':
|
|
87
|
+
settle(() => reject(new Error(msg.message)));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
worker.onerror = (event) => {
|
|
93
|
+
settle(() => reject(new Error(event.message || 'IDS worker crashed')));
|
|
94
|
+
};
|
|
95
|
+
worker.onmessageerror = () => {
|
|
96
|
+
settle(() => reject(new Error('IDS worker message deserialization failed')));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const request: IdsWorkerRequest = {
|
|
100
|
+
type: 'validate',
|
|
101
|
+
id,
|
|
102
|
+
source: buffer,
|
|
103
|
+
document: args.document,
|
|
104
|
+
schemaVersion: args.schemaVersion,
|
|
105
|
+
modelId: args.modelId,
|
|
106
|
+
locale: args.locale,
|
|
107
|
+
includePassingEntities: args.includePassingEntities,
|
|
108
|
+
};
|
|
109
|
+
worker.postMessage(request, transfer);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the exact source bytes into a worker-postable buffer.
|
|
115
|
+
* SharedArrayBuffer is shared by reference (zero copy); a plain
|
|
116
|
+
* ArrayBuffer (or a partial view) is copied into a fresh, transferable
|
|
117
|
+
* ArrayBuffer so the caller's store keeps its bytes.
|
|
118
|
+
*/
|
|
119
|
+
function prepareSource(source: Uint8Array): {
|
|
120
|
+
buffer: ArrayBuffer | SharedArrayBuffer;
|
|
121
|
+
transfer: Transferable[];
|
|
122
|
+
} {
|
|
123
|
+
const sharedAvailable = typeof SharedArrayBuffer !== 'undefined';
|
|
124
|
+
if (
|
|
125
|
+
sharedAvailable &&
|
|
126
|
+
source.buffer instanceof SharedArrayBuffer &&
|
|
127
|
+
source.byteOffset === 0 &&
|
|
128
|
+
source.byteLength === source.buffer.byteLength
|
|
129
|
+
) {
|
|
130
|
+
// Shared by reference — no copy, the worker only reads it.
|
|
131
|
+
return { buffer: source.buffer, transfer: [] };
|
|
132
|
+
}
|
|
133
|
+
// Copy the exact bytes; transfer the copy (never the original).
|
|
134
|
+
const copy = source.slice();
|
|
135
|
+
return { buffer: copy.buffer, transfer: [copy.buffer] };
|
|
136
|
+
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type ProjectedCRS,
|
|
18
18
|
} from '@ifc-lite/parser';
|
|
19
19
|
import type { CoordinateInfo } from '@ifc-lite/geometry';
|
|
20
|
-
import { useViewerStore, type FederatedModel } from '../../store.js';
|
|
20
|
+
import { useViewerStore, type FederatedModel } from '../../store/index.js';
|
|
21
21
|
import { getEffectiveGeoreference, getEffectiveHorizontalScale, hasStandardGeoreferencing, type GeorefMutationDataLike } from '../../lib/geo/effective-georef.js';
|
|
22
22
|
import { resolveMapUnitToMetreScale } from '../../lib/geo/geo-scale.js';
|
|
23
23
|
import { resolveProjection } from '../../lib/geo/reproject.js';
|
|
@@ -19,118 +19,42 @@ import {
|
|
|
19
19
|
type StreamHandle,
|
|
20
20
|
} from '@ifc-lite/pointcloud';
|
|
21
21
|
import type { CoordinateInfo, GeometryResult, PointCloudAsset } from '@ifc-lite/geometry';
|
|
22
|
-
import type
|
|
22
|
+
import { createSyntheticDataStore, type IfcDataStore } from '@ifc-lite/parser';
|
|
23
23
|
import type { SchemaVersion } from '../../store/types.js';
|
|
24
24
|
import { createCoordinateInfo } from '../../utils/localParsingUtils.js';
|
|
25
25
|
|
|
26
26
|
export type PointCloudFormat = 'las' | 'laz' | 'ply' | 'pcd' | 'e57' | 'pts' | 'xyz';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `properties.getForEntity` — without those methods, picking crashes
|
|
41
|
-
* with "getTypeName is not a function". We give it just enough shape
|
|
42
|
-
* to round-trip the single synthetic entity.
|
|
29
|
+
* Synthetic IfcDataStore for a point-cloud-only model. Picking a point sets
|
|
30
|
+
* the synthetic expressId as the selected entity, which then runs through the
|
|
31
|
+
* regular property/hover/properties-panel pipeline. That pipeline calls
|
|
32
|
+
* `entities.getTypeName / getName / getGlobalId` and `properties.getForEntity`
|
|
33
|
+
* — `createSyntheticDataStore` builds a real single-row entity table (plus the
|
|
34
|
+
* lazy `getEntity` / `getProperties` accessors) so every member of the
|
|
35
|
+
* `IfcDataStore` contract is present and compiler-enforced — no `as unknown as`
|
|
36
|
+
* shims that silently drop an accessor and crash picking at runtime (#1004).
|
|
37
|
+
*
|
|
38
|
+
* `IfcGeographicElement` is the closest IFC4 entity for a real-world scan; the
|
|
39
|
+
* entity table derives its enum (58) from the type name itself.
|
|
43
40
|
*/
|
|
44
41
|
function emptyDataStore(
|
|
45
42
|
buffer: ArrayBuffer,
|
|
46
43
|
expressId: number,
|
|
47
44
|
fileName: string,
|
|
48
45
|
): IfcDataStore {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const empty8 = new Uint8Array(0);
|
|
52
|
-
const emptyI32 = new Int32Array(0);
|
|
53
|
-
const indexOf = (id: number) => (id === expressId ? 0 : -1);
|
|
54
|
-
const entities = {
|
|
55
|
-
count: 1,
|
|
56
|
-
expressId: expressIds,
|
|
57
|
-
typeEnum: new Uint16Array([IFC_GEOGRAPHIC_ELEMENT_ENUM]),
|
|
58
|
-
globalId: empty32,
|
|
59
|
-
name: empty32,
|
|
60
|
-
description: empty32,
|
|
61
|
-
objectType: empty32,
|
|
62
|
-
flags: new Uint8Array([0]),
|
|
63
|
-
containedInStorey: new Int32Array([-1]),
|
|
64
|
-
definedByType: new Int32Array([-1]),
|
|
65
|
-
geometryIndex: new Int32Array([-1]),
|
|
66
|
-
typeRanges: new Map(),
|
|
67
|
-
getGlobalId: (id: number) => (indexOf(id) >= 0 ? `pointcloud-${expressId}` : ''),
|
|
68
|
-
getName: (id: number) => (indexOf(id) >= 0 ? fileName : ''),
|
|
69
|
-
getDescription: () => '',
|
|
70
|
-
getObjectType: () => '',
|
|
71
|
-
getTypeName: (id: number) => (indexOf(id) >= 0 ? 'IfcGeographicElement' : 'Unknown'),
|
|
72
|
-
hasGeometry: (id: number) => indexOf(id) >= 0,
|
|
73
|
-
getByType: () => [expressId],
|
|
74
|
-
getTypeEnum: (id: number) =>
|
|
75
|
-
indexOf(id) >= 0 ? IFC_GEOGRAPHIC_ELEMENT_ENUM : 9999, // 9999 = Unknown
|
|
76
|
-
getExpressIdByGlobalId: (gid: string) =>
|
|
77
|
-
gid === `pointcloud-${expressId}` ? expressId : -1,
|
|
78
|
-
getGlobalIdMap: () => new Map([[`pointcloud-${expressId}`, expressId]]),
|
|
79
|
-
};
|
|
80
|
-
const properties = {
|
|
81
|
-
count: 0,
|
|
82
|
-
entityId: empty32, psetName: empty32, psetGlobalId: empty32,
|
|
83
|
-
propName: empty32, propType: empty8,
|
|
84
|
-
valueString: empty32, valueReal: new Float64Array(0),
|
|
85
|
-
valueInt: emptyI32, valueBool: empty8, unitId: emptyI32,
|
|
86
|
-
entityIndex: new Map<number, number[]>(),
|
|
87
|
-
psetIndex: new Map<number, number[]>(),
|
|
88
|
-
propIndex: new Map<number, number[]>(),
|
|
89
|
-
getForEntity: () => [],
|
|
90
|
-
getPropertyValue: () => null,
|
|
91
|
-
findByProperty: () => [],
|
|
92
|
-
};
|
|
93
|
-
const quantities = {
|
|
94
|
-
count: 0,
|
|
95
|
-
entityId: empty32, qsetName: empty32, qsetGlobalId: empty32,
|
|
96
|
-
quantityName: empty32, quantityType: empty8,
|
|
97
|
-
valueReal: new Float64Array(0), unitId: emptyI32,
|
|
98
|
-
entityIndex: new Map<number, number[]>(),
|
|
99
|
-
qsetIndex: new Map<number, number[]>(),
|
|
100
|
-
getForEntity: () => [],
|
|
101
|
-
};
|
|
102
|
-
const relationships = {
|
|
103
|
-
count: 0,
|
|
104
|
-
relType: empty8, relatingId: empty32, relatedId: empty32,
|
|
105
|
-
byRelating: new Map<number, number[]>(),
|
|
106
|
-
byRelated: new Map<number, number[]>(),
|
|
107
|
-
getOutgoing: () => [],
|
|
108
|
-
getIncoming: () => [],
|
|
109
|
-
getRelated: () => [],
|
|
110
|
-
getRelating: () => [],
|
|
111
|
-
};
|
|
112
|
-
const byId = new Map<number, unknown>([[expressId, { expressId }]]);
|
|
113
|
-
return {
|
|
46
|
+
return createSyntheticDataStore({
|
|
47
|
+
schemaVersion: 'IFC4',
|
|
114
48
|
fileSize: buffer.byteLength,
|
|
115
|
-
schemaVersion: 'IFC4' as const,
|
|
116
49
|
entityCount: 1,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
getId: () => 0,
|
|
126
|
-
count: 0,
|
|
127
|
-
} as unknown as IfcDataStore['strings'],
|
|
128
|
-
entities: entities as unknown as IfcDataStore['entities'],
|
|
129
|
-
properties: properties as unknown as IfcDataStore['properties'],
|
|
130
|
-
quantities: quantities as unknown as IfcDataStore['quantities'],
|
|
131
|
-
relationships: relationships as unknown as IfcDataStore['relationships'],
|
|
132
|
-
spatialHierarchy: undefined,
|
|
133
|
-
} as unknown as IfcDataStore;
|
|
50
|
+
entities: [{
|
|
51
|
+
expressId,
|
|
52
|
+
type: 'IfcGeographicElement',
|
|
53
|
+
globalId: `pointcloud-${expressId}`,
|
|
54
|
+
name: fileName,
|
|
55
|
+
hasGeometry: true,
|
|
56
|
+
}],
|
|
57
|
+
});
|
|
134
58
|
}
|
|
135
59
|
|
|
136
60
|
export interface PointCloudIngestResult {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
import { parseIfcx, type IfcDataStore, type PointCloudExtraction } from '@ifc-lite/parser';
|
|
5
|
+
import { parseIfcx, createSyntheticDataStore, type IfcDataStore, type PointCloudExtraction } from '@ifc-lite/parser';
|
|
6
6
|
import { type GeometryResult, type MeshData, type PointCloudAsset } from '@ifc-lite/geometry';
|
|
7
7
|
import { loadGLBToMeshData } from '@ifc-lite/cache';
|
|
8
8
|
import type { SchemaVersion } from '../../store/types.js';
|
|
@@ -44,20 +44,15 @@ export function convertIfcxMeshes(rawMeshes: RawIfcxMesh[]): MeshData[] {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function createMinimalGlbDataStore(buffer: ArrayBuffer, meshCount: number): IfcDataStore {
|
|
47
|
-
|
|
47
|
+
// A GLB carries renderable meshes but no IFC entities. Build a typed,
|
|
48
|
+
// entity-less store via the shared factory so the full `IfcDataStore`
|
|
49
|
+
// contract (including the lazy `getEntity` / `getProperties` accessors the
|
|
50
|
+
// query path calls) is compiler-enforced instead of cast away (#1004).
|
|
51
|
+
return createSyntheticDataStore({
|
|
52
|
+
schemaVersion: 'IFC4',
|
|
48
53
|
fileSize: buffer.byteLength,
|
|
49
|
-
schemaVersion: 'IFC4' as const,
|
|
50
54
|
entityCount: meshCount,
|
|
51
|
-
|
|
52
|
-
source: new Uint8Array(0),
|
|
53
|
-
entityIndex: { byId: new Map(), byType: new Map() },
|
|
54
|
-
strings: { getString: () => undefined, getStringId: () => undefined, count: 0 } as unknown as IfcDataStore['strings'],
|
|
55
|
-
entities: { count: 0, getId: () => 0, getType: () => 0, getName: () => undefined, getGlobalId: () => undefined } as unknown as IfcDataStore['entities'],
|
|
56
|
-
properties: { count: 0, getPropertiesForEntity: () => [], getPropertySetForEntity: () => [] } as unknown as IfcDataStore['properties'],
|
|
57
|
-
quantities: { count: 0, getQuantitiesForEntity: () => [] } as unknown as IfcDataStore['quantities'],
|
|
58
|
-
relationships: { count: 0, getRelationships: () => [], getRelated: () => [] } as unknown as IfcDataStore['relationships'],
|
|
59
|
-
spatialHierarchy: null as unknown as IfcDataStore['spatialHierarchy'],
|
|
60
|
-
} as unknown as IfcDataStore;
|
|
55
|
+
});
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
export function getMaxExpressId(dataStore: IfcDataStore, meshes: MeshData[]): number {
|
|
@@ -24,6 +24,7 @@ import { useViewerStore } from '@/store';
|
|
|
24
24
|
import { useShallow } from 'zustand/react/shallow';
|
|
25
25
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
26
26
|
import { sourceKey } from './source-key.js';
|
|
27
|
+
import { hasEntityType } from './has-entity-type.js';
|
|
27
28
|
|
|
28
29
|
const EMPTY_F32 = new Float32Array(0);
|
|
29
30
|
|
|
@@ -42,6 +43,10 @@ function notifyCacheChange(): void {
|
|
|
42
43
|
async function parseAlignmentLinesFor(store: IfcDataStore): Promise<Float32Array> {
|
|
43
44
|
const source = store.source;
|
|
44
45
|
if (!source || source.byteLength === 0) return EMPTY_F32;
|
|
46
|
+
// Most models (all buildings) have no alignments. Skip the full-source WASM
|
|
47
|
+
// scan — it copies the entire IFC source into the WASM heap on the main thread
|
|
48
|
+
// just to find none (~0.5s on a 170MB file).
|
|
49
|
+
if (!hasEntityType(store, 'IfcAlignment', 'IfcAlignmentCurve')) return EMPTY_F32;
|
|
45
50
|
const processor = new GeometryProcessor();
|
|
46
51
|
try {
|
|
47
52
|
await processor.init();
|
|
@@ -24,6 +24,7 @@ import { useViewerStore } from '@/store';
|
|
|
24
24
|
import { useShallow } from 'zustand/react/shallow';
|
|
25
25
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
26
26
|
import { sourceKey } from './source-key.js';
|
|
27
|
+
import { hasEntityType } from './has-entity-type.js';
|
|
27
28
|
|
|
28
29
|
const EMPTY_F32 = new Float32Array(0);
|
|
29
30
|
|
|
@@ -42,6 +43,9 @@ function notifyCacheChange(): void {
|
|
|
42
43
|
async function parseGridLinesFor(store: IfcDataStore): Promise<Float32Array> {
|
|
43
44
|
const source = store.source;
|
|
44
45
|
if (!source || source.byteLength === 0) return EMPTY_F32;
|
|
46
|
+
// Skip the full-source WASM scan when the model has no grid — it copies the
|
|
47
|
+
// entire IFC source into the WASM heap on the main thread just to find none.
|
|
48
|
+
if (!hasEntityType(store, 'IfcGridAxis', 'IfcGrid')) return EMPTY_F32;
|
|
45
49
|
const processor = new GeometryProcessor();
|
|
46
50
|
try {
|
|
47
51
|
await processor.init();
|
package/src/hooks/useIDS.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { getEntityBounds } from '@/utils/viewportUtils';
|
|
|
38
38
|
import { getGlobalRenderer } from '@/hooks/useBCF';
|
|
39
39
|
|
|
40
40
|
import { createDataAccessor } from './ids/idsDataAccessor';
|
|
41
|
+
import { runValidationInWorker, idsWorkerSupported } from './ids/idsWorkerClient';
|
|
41
42
|
import {
|
|
42
43
|
DEFAULT_FAILED_COLOR,
|
|
43
44
|
DEFAULT_PASSED_COLOR,
|
|
@@ -386,23 +387,85 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
386
387
|
try {
|
|
387
388
|
setIdsLoading(true);
|
|
388
389
|
setIdsError(null);
|
|
390
|
+
// Paint a "starting" state immediately so the button shows work is
|
|
391
|
+
// underway before the first real progress event arrives.
|
|
392
|
+
setIdsProgress({
|
|
393
|
+
phase: 'filtering',
|
|
394
|
+
specificationIndex: 0,
|
|
395
|
+
totalSpecifications: document.specifications.length,
|
|
396
|
+
entitiesProcessed: 0,
|
|
397
|
+
totalEntities: 0,
|
|
398
|
+
percentage: 0,
|
|
399
|
+
});
|
|
389
400
|
|
|
390
|
-
//
|
|
391
|
-
|
|
401
|
+
// Force the loading state to actually paint before spawning the
|
|
402
|
+
// worker and doing any heavy synchronous work, so the spinner +
|
|
403
|
+
// initial progress bar are guaranteed on screen immediately. Race
|
|
404
|
+
// the frame wait against a timer so a backgrounded tab (where
|
|
405
|
+
// requestAnimationFrame is paused) can't stall the run.
|
|
406
|
+
await new Promise<void>((resolve) => {
|
|
407
|
+
let settled = false;
|
|
408
|
+
const done = () => {
|
|
409
|
+
if (settled) return;
|
|
410
|
+
settled = true;
|
|
411
|
+
resolve();
|
|
412
|
+
};
|
|
413
|
+
requestAnimationFrame(() => requestAnimationFrame(done));
|
|
414
|
+
setTimeout(done, 200);
|
|
415
|
+
});
|
|
392
416
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
417
|
+
const schemaVersion = dataStore.schemaVersion || 'IFC4';
|
|
418
|
+
|
|
419
|
+
// Progress events arrive far faster than React should re-render
|
|
420
|
+
// (per 100 entities / per spec); throttle store updates to ~8/s
|
|
421
|
+
// and always pass the terminal event.
|
|
422
|
+
let lastProgressUpdate = 0;
|
|
423
|
+
const onProgress = (p: ValidationProgress) => {
|
|
424
|
+
const now = performance.now();
|
|
425
|
+
if (p.phase === 'complete' || now - lastProgressUpdate >= 120) {
|
|
426
|
+
lastProgressUpdate = now;
|
|
427
|
+
setIdsProgress(p);
|
|
428
|
+
}
|
|
398
429
|
};
|
|
399
430
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
431
|
+
let validationReport: IDSValidationReport | null = null;
|
|
432
|
+
|
|
433
|
+
// Preferred path: validate in a Web Worker so the whole run is off
|
|
434
|
+
// the main thread — the UI stays at full frame rate and progress
|
|
435
|
+
// actually paints. Every other heavy stage (parse, geometry)
|
|
436
|
+
// already runs in a worker; this brings validation in line. Falls
|
|
437
|
+
// back to in-process validation if the worker is unavailable or
|
|
438
|
+
// fails (e.g. no source bytes for non-STEP models).
|
|
439
|
+
const canUseWorker = idsWorkerSupported() && !!dataStore.source && dataStore.source.byteLength > 0;
|
|
440
|
+
if (canUseWorker) {
|
|
441
|
+
try {
|
|
442
|
+
validationReport = await runValidationInWorker({
|
|
443
|
+
source: dataStore.source!,
|
|
444
|
+
document,
|
|
445
|
+
schemaVersion,
|
|
446
|
+
modelId,
|
|
447
|
+
locale,
|
|
448
|
+
includePassingEntities: true,
|
|
449
|
+
onProgress,
|
|
450
|
+
});
|
|
451
|
+
} catch (workerErr) {
|
|
452
|
+
console.warn('[IDS] Worker validation failed; falling back to main thread.', workerErr);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!validationReport) {
|
|
457
|
+
const accessor = createDataAccessor(dataStore, modelId);
|
|
458
|
+
const modelInfo: IDSModelInfo = {
|
|
459
|
+
modelId,
|
|
460
|
+
schemaVersion,
|
|
461
|
+
entityCount: dataStore.entityCount || accessor.getAllEntityIds().length,
|
|
462
|
+
};
|
|
463
|
+
validationReport = await validateIDS(document, accessor, modelInfo, {
|
|
464
|
+
translator,
|
|
465
|
+
onProgress,
|
|
466
|
+
includePassingEntities: true,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
406
469
|
|
|
407
470
|
setIdsValidationReport(validationReport);
|
|
408
471
|
|
|
@@ -426,6 +489,7 @@ export function useIDS(options: UseIDSOptions = {}): UseIDSResult {
|
|
|
426
489
|
models,
|
|
427
490
|
activeModelId,
|
|
428
491
|
translator,
|
|
492
|
+
locale,
|
|
429
493
|
setIdsLoading,
|
|
430
494
|
setIdsError,
|
|
431
495
|
setIdsProgress,
|
package/src/hooks/useIfcCache.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
|
23
23
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
24
24
|
|
|
25
25
|
import { useShallow } from 'zustand/react/shallow';
|
|
26
|
-
import { useViewerStore } from '../store.js';
|
|
26
|
+
import { useViewerStore } from '../store/index.js';
|
|
27
27
|
import { getCached, setCached, deleteCached, type CacheResult } from '../services/cacheService.js';
|
|
28
28
|
import { rebuildSpatialHierarchy, rebuildOnDemandMaps } from '../utils/spatialHierarchy.js';
|
|
29
29
|
import { calculateStoreyHeights } from '../utils/localParsingUtils.js';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { useCallback, useRef } from 'react';
|
|
14
14
|
import { useShallow } from 'zustand/react/shallow';
|
|
15
|
-
import { useViewerStore, type FederatedModel, type SchemaVersion } from '../store.js';
|
|
15
|
+
import { useViewerStore, type FederatedModel, type SchemaVersion } from '../store/index.js';
|
|
16
16
|
import {
|
|
17
17
|
detectFormat,
|
|
18
18
|
parseFederatedIfcx,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { useEffect } from 'react';
|
|
22
|
-
import { useViewerStore } from '../store.js';
|
|
22
|
+
import { useViewerStore } from '../store/index.js';
|
|
23
23
|
import { resolveEntityRef } from '../store/resolveEntityRef.js';
|
|
24
24
|
|
|
25
25
|
export function useModelSelection() {
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
* Bridge the solar study to the WebGPU renderer's lighting environment.
|
|
7
|
+
*
|
|
8
|
+
* While the study is enabled and the model is georeferenced, this hook
|
|
9
|
+
* computes the sun's true direction for the studied instant at the site and
|
|
10
|
+
* publishes it (viewer/world space, Y-up) to `solarSunDirection` — Viewport
|
|
11
|
+
* folds it into `RenderOptions.environment`, so the WebGPU sun and sky track
|
|
12
|
+
* the same instant Cesium's clock is pinned to.
|
|
13
|
+
*
|
|
14
|
+
* It also publishes the panel readout (`solarSunInfo`) when the Cesium
|
|
15
|
+
* overlay is OFF — with Cesium on, CesiumOverlay's solar effect owns that
|
|
16
|
+
* write (it uses the terrain-clamped bridge origin).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useState } from 'react';
|
|
20
|
+
import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser';
|
|
21
|
+
import type { CoordinateInfo } from '@ifc-lite/geometry';
|
|
22
|
+
import { sunPosition, sunTimes, azimuthAltitudeToEnu } from '@ifc-lite/solar';
|
|
23
|
+
import { useViewerStore } from '@/store';
|
|
24
|
+
import { computeCesiumModelOrigin } from '@/lib/geo/cesium-bridge';
|
|
25
|
+
import { enuToViewerDirection } from '@/lib/geo/solar-direction';
|
|
26
|
+
|
|
27
|
+
export interface SolarEnvironmentGeoref {
|
|
28
|
+
mapConversion?: MapConversion;
|
|
29
|
+
projectedCRS?: ProjectedCRS;
|
|
30
|
+
coordinateInfo?: CoordinateInfo;
|
|
31
|
+
lengthUnitScale?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SiteOrigin {
|
|
35
|
+
latitude: number;
|
|
36
|
+
longitude: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useSolarEnvironment(georef: SolarEnvironmentGeoref | null): void {
|
|
40
|
+
const solarEnabled = useViewerStore((s) => s.solarEnabled);
|
|
41
|
+
const solarDateMs = useViewerStore((s) => s.solarDateMs);
|
|
42
|
+
const cesiumEnabled = useViewerStore((s) => s.cesiumEnabled);
|
|
43
|
+
const setSolarSunDirection = useViewerStore((s) => s.setSolarSunDirection);
|
|
44
|
+
const setSolarSunInfo = useViewerStore((s) => s.setSolarSunInfo);
|
|
45
|
+
|
|
46
|
+
const [origin, setOrigin] = useState<SiteOrigin | null>(null);
|
|
47
|
+
|
|
48
|
+
const mapConversion = georef?.mapConversion;
|
|
49
|
+
const projectedCRS = georef?.projectedCRS;
|
|
50
|
+
|
|
51
|
+
// Resolve the site's lat/lon once per georeference (proj4 lookup is async).
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!solarEnabled || !mapConversion || !projectedCRS) {
|
|
54
|
+
setOrigin(null);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
(async () => {
|
|
59
|
+
try {
|
|
60
|
+
const resolved = await computeCesiumModelOrigin(
|
|
61
|
+
mapConversion,
|
|
62
|
+
projectedCRS,
|
|
63
|
+
georef?.coordinateInfo,
|
|
64
|
+
georef?.lengthUnitScale ?? 1,
|
|
65
|
+
);
|
|
66
|
+
if (!cancelled) {
|
|
67
|
+
setOrigin(resolved ? { latitude: resolved.latitude, longitude: resolved.longitude } : null);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
if (!cancelled) setOrigin(null);
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
return () => { cancelled = true; };
|
|
74
|
+
}, [solarEnabled, mapConversion, projectedCRS, georef?.coordinateInfo, georef?.lengthUnitScale]);
|
|
75
|
+
|
|
76
|
+
// Publish the viewer-space sun direction for the studied instant.
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!solarEnabled || !origin) {
|
|
79
|
+
setSolarSunDirection(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const date = new Date(solarDateMs);
|
|
83
|
+
const sp = sunPosition(date, origin.latitude, origin.longitude);
|
|
84
|
+
const enu = azimuthAltitudeToEnu(sp.azimuth, sp.altitude);
|
|
85
|
+
setSolarSunDirection(enuToViewerDirection(
|
|
86
|
+
enu,
|
|
87
|
+
mapConversion?.xAxisAbscissa ?? 1,
|
|
88
|
+
mapConversion?.xAxisOrdinate ?? 0,
|
|
89
|
+
));
|
|
90
|
+
|
|
91
|
+
// Panel readout — only when CesiumOverlay isn't publishing it.
|
|
92
|
+
if (!cesiumEnabled) {
|
|
93
|
+
const times = sunTimes(date, origin.latitude, origin.longitude);
|
|
94
|
+
setSolarSunInfo({
|
|
95
|
+
latitude: origin.latitude,
|
|
96
|
+
longitude: origin.longitude,
|
|
97
|
+
azimuth: sp.azimuth,
|
|
98
|
+
altitude: sp.altitude,
|
|
99
|
+
sunriseMs: times.sunrise ? times.sunrise.getTime() : null,
|
|
100
|
+
sunsetMs: times.sunset ? times.sunset.getTime() : null,
|
|
101
|
+
solarNoonMs: times.solarNoon.getTime(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}, [
|
|
105
|
+
solarEnabled,
|
|
106
|
+
solarDateMs,
|
|
107
|
+
origin,
|
|
108
|
+
cesiumEnabled,
|
|
109
|
+
mapConversion?.xAxisAbscissa,
|
|
110
|
+
mapConversion?.xAxisOrdinate,
|
|
111
|
+
setSolarSunDirection,
|
|
112
|
+
setSolarSunInfo,
|
|
113
|
+
]);
|
|
114
|
+
}
|