@ifc-lite/viewer 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
- package/dist/assets/browser-DdRf3aWl.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/ifc-lite_bg-C1-gLAHo.wasm +0 -0
- package/dist/assets/index-1ff6P0kc.js +100011 -0
- package/dist/assets/index-Bz7vHRxl.js +216 -0
- package/dist/assets/index-mvbV6NHd.css +1 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
- package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/MainToolbar.tsx +31 -3
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -2
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
- package/src/components/viewer/useAnimationLoop.ts +4 -1
- package/src/components/viewer/useGeometryStreaming.ts +13 -1
- package/src/components/viewer/useRenderUpdates.ts +6 -1
- package/src/hooks/useKeyboardShortcuts.ts +1 -0
- package/src/hooks/useLens.ts +2 -1
- package/src/hooks/useSandbox.ts +113 -0
- package/src/hooks/useViewerSelectors.ts +22 -0
- package/src/index.css +6 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +30 -2
- package/src/store/index.ts +24 -1
- package/src/store/slices/pinboardSlice.ts +37 -41
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +43 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/index-7WoQ-qVC.css +0 -1
- package/dist/assets/index-BSANf7-H.js +0 -78795
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
10
|
-
import { Renderer } from '@ifc-lite/renderer';
|
|
10
|
+
import { Renderer, type VisualEnhancementOptions } from '@ifc-lite/renderer';
|
|
11
11
|
import type { MeshData, CoordinateInfo } from '@ifc-lite/geometry';
|
|
12
12
|
import { useViewerStore, resolveEntityRef, type MeasurePoint, type SnapVisualization } from '@/store';
|
|
13
13
|
import {
|
|
@@ -158,7 +158,20 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
158
158
|
const { updateCameraRotationRealtime, updateScaleRealtime, setCameraCallbacks } = useCameraState();
|
|
159
159
|
|
|
160
160
|
// Theme state
|
|
161
|
-
const {
|
|
161
|
+
const {
|
|
162
|
+
theme,
|
|
163
|
+
isMobile,
|
|
164
|
+
visualEnhancementsEnabled,
|
|
165
|
+
edgeContrastEnabled,
|
|
166
|
+
edgeContrastIntensity,
|
|
167
|
+
contactShadingQuality,
|
|
168
|
+
contactShadingIntensity,
|
|
169
|
+
contactShadingRadius,
|
|
170
|
+
separationLinesEnabled,
|
|
171
|
+
separationLinesQuality,
|
|
172
|
+
separationLinesIntensity,
|
|
173
|
+
separationLinesRadius,
|
|
174
|
+
} = useThemeState();
|
|
162
175
|
|
|
163
176
|
// Hover state
|
|
164
177
|
const { hoverTooltipsEnabled, setHoverState, clearHover } = useHoverState();
|
|
@@ -215,6 +228,37 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
215
228
|
// Theme-aware clear color ref (updated when theme changes)
|
|
216
229
|
// Tokyo Night storm: #1a1b26 = rgb(26, 27, 38)
|
|
217
230
|
const clearColorRef = useRef<[number, number, number, number]>([0.102, 0.106, 0.149, 1]);
|
|
231
|
+
const visualEnhancement = useMemo<VisualEnhancementOptions>(() => ({
|
|
232
|
+
enabled: visualEnhancementsEnabled,
|
|
233
|
+
edgeContrast: {
|
|
234
|
+
enabled: edgeContrastEnabled,
|
|
235
|
+
intensity: edgeContrastIntensity,
|
|
236
|
+
},
|
|
237
|
+
contactShading: {
|
|
238
|
+
quality: isMobile ? 'off' : contactShadingQuality,
|
|
239
|
+
intensity: contactShadingIntensity,
|
|
240
|
+
radius: contactShadingRadius,
|
|
241
|
+
},
|
|
242
|
+
separationLines: {
|
|
243
|
+
enabled: separationLinesEnabled,
|
|
244
|
+
quality: isMobile ? 'low' : separationLinesQuality,
|
|
245
|
+
intensity: isMobile ? Math.min(0.4, separationLinesIntensity) : separationLinesIntensity,
|
|
246
|
+
radius: isMobile ? 1.0 : separationLinesRadius,
|
|
247
|
+
},
|
|
248
|
+
}), [
|
|
249
|
+
visualEnhancementsEnabled,
|
|
250
|
+
edgeContrastEnabled,
|
|
251
|
+
edgeContrastIntensity,
|
|
252
|
+
isMobile,
|
|
253
|
+
contactShadingQuality,
|
|
254
|
+
contactShadingIntensity,
|
|
255
|
+
contactShadingRadius,
|
|
256
|
+
separationLinesEnabled,
|
|
257
|
+
separationLinesQuality,
|
|
258
|
+
separationLinesIntensity,
|
|
259
|
+
separationLinesRadius,
|
|
260
|
+
]);
|
|
261
|
+
const visualEnhancementRef = useRef<VisualEnhancementOptions>(visualEnhancement);
|
|
218
262
|
|
|
219
263
|
// Animation frame ref
|
|
220
264
|
const animationFrameRef = useRef<number | null>(null);
|
|
@@ -330,6 +374,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
330
374
|
useEffect(() => { measurementConstraintEdgeRef.current = measurementConstraintEdge; }, [measurementConstraintEdge]);
|
|
331
375
|
useEffect(() => { sectionPlaneRef.current = sectionPlane; }, [sectionPlane]);
|
|
332
376
|
useEffect(() => { sectionRangeRef.current = sectionRange; }, [sectionRange]);
|
|
377
|
+
useEffect(() => { visualEnhancementRef.current = visualEnhancement; }, [visualEnhancement]);
|
|
333
378
|
useEffect(() => {
|
|
334
379
|
geometryRef.current = geometry;
|
|
335
380
|
}, [geometry]);
|
|
@@ -459,6 +504,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
459
504
|
selectedId: selectedEntityIdRef.current,
|
|
460
505
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
461
506
|
clearColor: clearColorRef.current,
|
|
507
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
462
508
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
463
509
|
...sectionPlaneRef.current,
|
|
464
510
|
min: sectionRangeRef.current?.min,
|
|
@@ -485,6 +531,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
485
531
|
selectedId: selectedEntityIdRef.current,
|
|
486
532
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
487
533
|
clearColor: clearColorRef.current,
|
|
534
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
488
535
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
489
536
|
...sectionPlaneRef.current,
|
|
490
537
|
min: sectionRangeRef.current?.min,
|
|
@@ -501,6 +548,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
501
548
|
selectedId: selectedEntityIdRef.current,
|
|
502
549
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
503
550
|
clearColor: clearColorRef.current,
|
|
551
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
504
552
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
505
553
|
...sectionPlaneRef.current,
|
|
506
554
|
min: sectionRangeRef.current?.min,
|
|
@@ -534,6 +582,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
534
582
|
selectedId: selectedEntityIdRef.current,
|
|
535
583
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
536
584
|
clearColor: clearColorRef.current,
|
|
585
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
537
586
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
538
587
|
...sectionPlaneRef.current,
|
|
539
588
|
min: sectionRangeRef.current?.min,
|
|
@@ -557,6 +606,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
557
606
|
selectedId: selectedEntityIdRef.current,
|
|
558
607
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
559
608
|
clearColor: clearColorRef.current,
|
|
609
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
560
610
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
561
611
|
...sectionPlaneRef.current,
|
|
562
612
|
min: sectionRangeRef.current?.min,
|
|
@@ -573,6 +623,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
573
623
|
selectedId: selectedEntityIdRef.current,
|
|
574
624
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
575
625
|
clearColor: clearColorRef.current,
|
|
626
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
576
627
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
577
628
|
...sectionPlaneRef.current,
|
|
578
629
|
min: sectionRangeRef.current?.min,
|
|
@@ -598,6 +649,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
598
649
|
selectedId: selectedEntityIdRef.current,
|
|
599
650
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
600
651
|
clearColor: clearColorRef.current,
|
|
652
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
601
653
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
602
654
|
...sectionPlaneRef.current,
|
|
603
655
|
min: sectionRangeRef.current?.min,
|
|
@@ -614,6 +666,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
614
666
|
selectedId: selectedEntityIdRef.current,
|
|
615
667
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
616
668
|
clearColor: clearColorRef.current,
|
|
669
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
617
670
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
618
671
|
...sectionPlaneRef.current,
|
|
619
672
|
min: sectionRangeRef.current?.min,
|
|
@@ -767,6 +820,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
767
820
|
clearColorRef,
|
|
768
821
|
sectionPlaneRef,
|
|
769
822
|
sectionRangeRef,
|
|
823
|
+
visualEnhancementRef,
|
|
770
824
|
lastCameraStateRef,
|
|
771
825
|
updateCameraRotationRealtime,
|
|
772
826
|
calculateScale,
|
|
@@ -783,6 +837,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
783
837
|
geometryBoundsRef,
|
|
784
838
|
pendingColorUpdates,
|
|
785
839
|
clearPendingColorUpdates,
|
|
840
|
+
clearColorRef,
|
|
786
841
|
});
|
|
787
842
|
|
|
788
843
|
useRenderUpdates({
|
|
@@ -790,6 +845,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
790
845
|
isInitialized,
|
|
791
846
|
theme,
|
|
792
847
|
clearColorRef,
|
|
848
|
+
visualEnhancementRef,
|
|
793
849
|
hiddenEntities,
|
|
794
850
|
isolatedEntities,
|
|
795
851
|
selectedEntityId,
|
|
@@ -132,7 +132,9 @@ function buildSpatialNodes(
|
|
|
132
132
|
id: nodeId,
|
|
133
133
|
expressIds: [spatialNode.expressId],
|
|
134
134
|
modelIds: [modelId],
|
|
135
|
-
name: spatialNode.name
|
|
135
|
+
name: (spatialNode.name && spatialNode.name.toLowerCase() !== 'unknown')
|
|
136
|
+
? spatialNode.name
|
|
137
|
+
: nodeType,
|
|
136
138
|
type: nodeType,
|
|
137
139
|
depth,
|
|
138
140
|
hasChildren,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useEffect, type MutableRefObject, type RefObject } from 'react';
|
|
11
|
-
import type { Renderer } from '@ifc-lite/renderer';
|
|
11
|
+
import type { Renderer, VisualEnhancementOptions } from '@ifc-lite/renderer';
|
|
12
12
|
import type { SectionPlane } from '@/store';
|
|
13
13
|
|
|
14
14
|
export interface UseAnimationLoopParams {
|
|
@@ -24,6 +24,7 @@ export interface UseAnimationLoopParams {
|
|
|
24
24
|
selectedEntityIdRef: MutableRefObject<number | null>;
|
|
25
25
|
selectedModelIndexRef: MutableRefObject<number | undefined>;
|
|
26
26
|
clearColorRef: MutableRefObject<[number, number, number, number]>;
|
|
27
|
+
visualEnhancementRef: MutableRefObject<VisualEnhancementOptions>;
|
|
27
28
|
sectionPlaneRef: MutableRefObject<SectionPlane>;
|
|
28
29
|
sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
|
|
29
30
|
lastCameraStateRef: MutableRefObject<{
|
|
@@ -53,6 +54,7 @@ export function useAnimationLoop(params: UseAnimationLoopParams): void {
|
|
|
53
54
|
selectedEntityIdRef,
|
|
54
55
|
selectedModelIndexRef,
|
|
55
56
|
clearColorRef,
|
|
57
|
+
visualEnhancementRef,
|
|
56
58
|
sectionPlaneRef,
|
|
57
59
|
sectionRangeRef,
|
|
58
60
|
lastCameraStateRef,
|
|
@@ -87,6 +89,7 @@ export function useAnimationLoop(params: UseAnimationLoopParams): void {
|
|
|
87
89
|
selectedId: selectedEntityIdRef.current,
|
|
88
90
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
89
91
|
clearColor: clearColorRef.current,
|
|
92
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
90
93
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
91
94
|
...sectionPlaneRef.current,
|
|
92
95
|
min: sectionRangeRef.current?.min,
|
|
@@ -20,6 +20,8 @@ export interface UseGeometryStreamingParams {
|
|
|
20
20
|
geometryBoundsRef: MutableRefObject<{ min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } }>;
|
|
21
21
|
pendingColorUpdates: Map<number, [number, number, number, number]> | null;
|
|
22
22
|
clearPendingColorUpdates: () => void;
|
|
23
|
+
// Clear color ref — color update renders must preserve theme background
|
|
24
|
+
clearColorRef: MutableRefObject<[number, number, number, number]>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
@@ -32,6 +34,7 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
32
34
|
geometryBoundsRef,
|
|
33
35
|
pendingColorUpdates,
|
|
34
36
|
clearPendingColorUpdates,
|
|
37
|
+
clearColorRef,
|
|
35
38
|
} = params;
|
|
36
39
|
|
|
37
40
|
// Track processed meshes for incremental updates
|
|
@@ -397,7 +400,16 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
397
400
|
// Non-empty map = set color overrides
|
|
398
401
|
scene.setColorOverrides(pendingColorUpdates, device, pipeline);
|
|
399
402
|
}
|
|
400
|
-
|
|
403
|
+
// Re-render with current theme background — render() without options
|
|
404
|
+
// defaults to black background. Do NOT pass hiddenIds/isolatedIds here:
|
|
405
|
+
// visibility filtering causes partial batches which write depth only for
|
|
406
|
+
// visible elements, but overlay batches cover all geometry. Without
|
|
407
|
+
// filtering, all original batches write depth for every entity, ensuring
|
|
408
|
+
// depthCompare 'equal' matches exactly for the overlay pass.
|
|
409
|
+
// The next render from useRenderUpdates will apply the correct visibility.
|
|
410
|
+
renderer.render({
|
|
411
|
+
clearColor: clearColorRef.current,
|
|
412
|
+
});
|
|
401
413
|
clearPendingColorUpdates();
|
|
402
414
|
}
|
|
403
415
|
}, [pendingColorUpdates, isInitialized, clearPendingColorUpdates]);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useEffect, type MutableRefObject } from 'react';
|
|
11
|
-
import type { Renderer, CutPolygon2D, DrawingLine2D } from '@ifc-lite/renderer';
|
|
11
|
+
import type { Renderer, CutPolygon2D, DrawingLine2D, VisualEnhancementOptions } from '@ifc-lite/renderer';
|
|
12
12
|
import type { CoordinateInfo } from '@ifc-lite/geometry';
|
|
13
13
|
import type { Drawing2D } from '@ifc-lite/drawing-2d';
|
|
14
14
|
import type { SectionPlane } from '@/store';
|
|
@@ -21,6 +21,7 @@ export interface UseRenderUpdatesParams {
|
|
|
21
21
|
// Theme
|
|
22
22
|
theme: string;
|
|
23
23
|
clearColorRef: MutableRefObject<[number, number, number, number]>;
|
|
24
|
+
visualEnhancementRef: MutableRefObject<VisualEnhancementOptions>;
|
|
24
25
|
|
|
25
26
|
// Visibility/selection state (reactive values, not refs)
|
|
26
27
|
hiddenEntities: Set<number>;
|
|
@@ -54,6 +55,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
54
55
|
isInitialized,
|
|
55
56
|
theme,
|
|
56
57
|
clearColorRef,
|
|
58
|
+
visualEnhancementRef,
|
|
57
59
|
hiddenEntities,
|
|
58
60
|
isolatedEntities,
|
|
59
61
|
selectedEntityId,
|
|
@@ -88,6 +90,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
88
90
|
selectedId: selectedEntityIdRef.current,
|
|
89
91
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
90
92
|
clearColor: clearColorRef.current,
|
|
93
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
91
94
|
});
|
|
92
95
|
}
|
|
93
96
|
}, [theme, isInitialized]);
|
|
@@ -132,6 +135,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
132
135
|
selectedIds: selectedEntityIdsRef.current,
|
|
133
136
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
134
137
|
clearColor: clearColorRef.current,
|
|
138
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
135
139
|
sectionPlane: activeTool === 'section' ? {
|
|
136
140
|
...sectionPlane,
|
|
137
141
|
min: sectionRangeRef.current?.min,
|
|
@@ -152,6 +156,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
152
156
|
selectedIds: selectedEntityIds,
|
|
153
157
|
selectedModelIndex,
|
|
154
158
|
clearColor: clearColorRef.current,
|
|
159
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
155
160
|
sectionPlane: activeTool === 'section' ? {
|
|
156
161
|
...sectionPlane,
|
|
157
162
|
min: sectionRange?.min,
|
|
@@ -279,5 +279,6 @@ export const KEYBOARD_SHORTCUTS = [
|
|
|
279
279
|
{ key: '1-6', description: 'Preset views', category: 'Camera' },
|
|
280
280
|
{ key: 'T', description: 'Toggle theme', category: 'UI' },
|
|
281
281
|
{ key: 'Esc', description: 'Reset all (clear selection, basket, isolation)', category: 'Selection' },
|
|
282
|
+
{ key: 'Ctrl+K', description: 'Command palette', category: 'UI' },
|
|
282
283
|
{ key: '?', description: 'Show info panel', category: 'Help' },
|
|
283
284
|
] as const;
|
package/src/hooks/useLens.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { useEffect, useRef, useMemo } from 'react';
|
|
26
26
|
import { evaluateLens, evaluateAutoColorLens, rgbaToHex, isGhostColor } from '@ifc-lite/lens';
|
|
27
|
+
import type { AutoColorEvaluationResult } from '@ifc-lite/lens';
|
|
27
28
|
import { useViewerStore } from '@/store';
|
|
28
29
|
import { createLensDataProvider } from '@/lib/lens';
|
|
29
30
|
import { useLensDiscovery } from './useLensDiscovery';
|
|
@@ -95,7 +96,7 @@ export function useLens() {
|
|
|
95
96
|
|
|
96
97
|
// Store auto-color legend entries for UI display
|
|
97
98
|
if (isAutoColor && 'legend' in result) {
|
|
98
|
-
useViewerStore.getState().setLensAutoColorLegend(result.legend);
|
|
99
|
+
useViewerStore.getState().setLensAutoColorLegend((result as AutoColorEvaluationResult).legend);
|
|
99
100
|
} else {
|
|
100
101
|
useViewerStore.getState().setLensAutoColorLegend([]);
|
|
101
102
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
* useSandbox — React hook for executing scripts in a QuickJS sandbox.
|
|
7
|
+
*
|
|
8
|
+
* Creates a fresh sandbox context per execution for full isolation.
|
|
9
|
+
* The WASM module is cached across the session (cheap to reuse),
|
|
10
|
+
* but each script runs in a clean context with no leaked state.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
14
|
+
import { useBim } from '../sdk/BimProvider.js';
|
|
15
|
+
import { useViewerStore } from '../store/index.js';
|
|
16
|
+
import type { Sandbox, ScriptResult, SandboxConfig } from '@ifc-lite/sandbox';
|
|
17
|
+
|
|
18
|
+
/** Type guard for ScriptError shape (has logs + durationMs) */
|
|
19
|
+
function isScriptError(err: unknown): err is { message: string; logs: Array<{ level: string; args: unknown[]; timestamp: number }>; durationMs: number } {
|
|
20
|
+
return (
|
|
21
|
+
err !== null &&
|
|
22
|
+
typeof err === 'object' &&
|
|
23
|
+
'logs' in err &&
|
|
24
|
+
Array.isArray((err as Record<string, unknown>).logs) &&
|
|
25
|
+
'durationMs' in err &&
|
|
26
|
+
typeof (err as Record<string, unknown>).durationMs === 'number'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook that provides a sandbox execution interface.
|
|
32
|
+
*
|
|
33
|
+
* Each execute() call creates a fresh QuickJS context for full isolation —
|
|
34
|
+
* scripts cannot leak global state between runs. The WASM module itself
|
|
35
|
+
* is cached (loaded once per app lifetime, ~1ms context creation overhead).
|
|
36
|
+
*/
|
|
37
|
+
export function useSandbox(config?: SandboxConfig) {
|
|
38
|
+
const bim = useBim();
|
|
39
|
+
const activeSandboxRef = useRef<Sandbox | null>(null);
|
|
40
|
+
|
|
41
|
+
const setExecutionState = useViewerStore((s) => s.setScriptExecutionState);
|
|
42
|
+
const setResult = useViewerStore((s) => s.setScriptResult);
|
|
43
|
+
const setError = useViewerStore((s) => s.setScriptError);
|
|
44
|
+
|
|
45
|
+
/** Execute a script in an isolated sandbox context */
|
|
46
|
+
const execute = useCallback(async (code: string): Promise<ScriptResult | null> => {
|
|
47
|
+
setExecutionState('running');
|
|
48
|
+
setError(null);
|
|
49
|
+
|
|
50
|
+
let sandbox: Sandbox | null = null;
|
|
51
|
+
try {
|
|
52
|
+
// Create a fresh sandbox for every execution — full isolation
|
|
53
|
+
const { createSandbox } = await import('@ifc-lite/sandbox');
|
|
54
|
+
sandbox = await createSandbox(bim, {
|
|
55
|
+
permissions: { model: true, query: true, viewer: true, mutate: true, lens: true, export: true, ...config?.permissions },
|
|
56
|
+
limits: { timeoutMs: 30_000, ...config?.limits },
|
|
57
|
+
});
|
|
58
|
+
activeSandboxRef.current = sandbox;
|
|
59
|
+
|
|
60
|
+
const result = await sandbox.eval(code);
|
|
61
|
+
setResult({
|
|
62
|
+
value: result.value,
|
|
63
|
+
logs: result.logs,
|
|
64
|
+
durationMs: result.durationMs,
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
} catch (err: unknown) {
|
|
68
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
69
|
+
setError(message);
|
|
70
|
+
|
|
71
|
+
// If the error is a ScriptError with captured logs, preserve them
|
|
72
|
+
if (isScriptError(err)) {
|
|
73
|
+
setResult({
|
|
74
|
+
value: undefined,
|
|
75
|
+
logs: err.logs as ScriptResult['logs'],
|
|
76
|
+
durationMs: err.durationMs,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
} finally {
|
|
81
|
+
// Always dispose the sandbox after execution
|
|
82
|
+
if (sandbox) {
|
|
83
|
+
sandbox.dispose();
|
|
84
|
+
}
|
|
85
|
+
if (activeSandboxRef.current === sandbox) {
|
|
86
|
+
activeSandboxRef.current = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, [bim, config?.permissions, config?.limits, setExecutionState, setResult, setError]);
|
|
90
|
+
|
|
91
|
+
/** Reset clears any active sandbox (no-op if none running) */
|
|
92
|
+
const reset = useCallback(() => {
|
|
93
|
+
if (activeSandboxRef.current) {
|
|
94
|
+
activeSandboxRef.current.dispose();
|
|
95
|
+
activeSandboxRef.current = null;
|
|
96
|
+
}
|
|
97
|
+
setExecutionState('idle');
|
|
98
|
+
setResult(null);
|
|
99
|
+
setError(null);
|
|
100
|
+
}, [setExecutionState, setResult, setError]);
|
|
101
|
+
|
|
102
|
+
// Cleanup on unmount
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
return () => {
|
|
105
|
+
if (activeSandboxRef.current) {
|
|
106
|
+
activeSandboxRef.current.dispose();
|
|
107
|
+
activeSandboxRef.current = null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
return { execute, reset };
|
|
113
|
+
}
|
|
@@ -157,9 +157,31 @@ export function useHoverState() {
|
|
|
157
157
|
*/
|
|
158
158
|
export function useThemeState() {
|
|
159
159
|
const theme = useViewerStore((state) => state.theme);
|
|
160
|
+
const isMobile = useViewerStore((state) => state.isMobile);
|
|
161
|
+
const visualEnhancementsEnabled = useViewerStore((state) => state.visualEnhancementsEnabled);
|
|
162
|
+
const edgeContrastEnabled = useViewerStore((state) => state.edgeContrastEnabled);
|
|
163
|
+
const edgeContrastIntensity = useViewerStore((state) => state.edgeContrastIntensity);
|
|
164
|
+
const contactShadingQuality = useViewerStore((state) => state.contactShadingQuality);
|
|
165
|
+
const contactShadingIntensity = useViewerStore((state) => state.contactShadingIntensity);
|
|
166
|
+
const contactShadingRadius = useViewerStore((state) => state.contactShadingRadius);
|
|
167
|
+
const separationLinesEnabled = useViewerStore((state) => state.separationLinesEnabled);
|
|
168
|
+
const separationLinesQuality = useViewerStore((state) => state.separationLinesQuality);
|
|
169
|
+
const separationLinesIntensity = useViewerStore((state) => state.separationLinesIntensity);
|
|
170
|
+
const separationLinesRadius = useViewerStore((state) => state.separationLinesRadius);
|
|
160
171
|
|
|
161
172
|
return {
|
|
162
173
|
theme,
|
|
174
|
+
isMobile,
|
|
175
|
+
visualEnhancementsEnabled,
|
|
176
|
+
edgeContrastEnabled,
|
|
177
|
+
edgeContrastIntensity,
|
|
178
|
+
contactShadingQuality,
|
|
179
|
+
contactShadingIntensity,
|
|
180
|
+
contactShadingRadius,
|
|
181
|
+
separationLinesEnabled,
|
|
182
|
+
separationLinesQuality,
|
|
183
|
+
separationLinesIntensity,
|
|
184
|
+
separationLinesRadius,
|
|
163
185
|
};
|
|
164
186
|
}
|
|
165
187
|
|
package/src/index.css
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
@import "tailwindcss";
|
|
6
6
|
|
|
7
|
+
/* Override Tailwind v4's default media-query dark variant so all dark: utilities
|
|
8
|
+
respond to the .dark class on <html> instead of prefers-color-scheme.
|
|
9
|
+
Without this, toggling light mode while the OS is in dark mode leaves all
|
|
10
|
+
dark: Tailwind utilities active, creating a mixed dark/light UI. */
|
|
11
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
12
|
+
|
|
7
13
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
8
14
|
TOKYO NIGHT THEME - Dark Stormy Cyberpunk Vibes
|
|
9
15
|
═══════════════════════════════════════════════════════════════════════════ */
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
* Track recently opened model files via localStorage + IndexedDB.
|
|
7
|
+
*
|
|
8
|
+
* localStorage: metadata (name, size, timestamp) — for palette display.
|
|
9
|
+
* IndexedDB: actual file blobs — so recent files can be loaded instantly
|
|
10
|
+
* without the user re-selecting them from the file picker.
|
|
11
|
+
*
|
|
12
|
+
* Shared between MainToolbar (writes) and CommandPalette (reads).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const KEY = 'ifc-lite:recent-files';
|
|
16
|
+
const DB_NAME = 'ifc-lite-file-cache';
|
|
17
|
+
const DB_VERSION = 1;
|
|
18
|
+
const STORE_NAME = 'files';
|
|
19
|
+
const MAX_CACHED_FILES = 5;
|
|
20
|
+
/** Max file size to cache (150 MB) — avoids filling IndexedDB quota */
|
|
21
|
+
const MAX_CACHE_SIZE = 150 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
export interface RecentFileEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
size: number;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── localStorage (metadata) ─────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export function getRecentFiles(): RecentFileEntry[] {
|
|
32
|
+
try { return JSON.parse(localStorage.getItem(KEY) ?? '[]'); }
|
|
33
|
+
catch { return []; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function recordRecentFiles(files: { name: string; size: number }[]) {
|
|
37
|
+
try {
|
|
38
|
+
const names = new Set(files.map(f => f.name));
|
|
39
|
+
const existing = getRecentFiles().filter(f => !names.has(f.name));
|
|
40
|
+
const entries: RecentFileEntry[] = files.map(f => ({
|
|
41
|
+
name: f.name,
|
|
42
|
+
size: f.size,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
}));
|
|
45
|
+
localStorage.setItem(KEY, JSON.stringify([...entries, ...existing].slice(0, 10)));
|
|
46
|
+
} catch { /* noop */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Format bytes into human-readable size */
|
|
50
|
+
export function formatFileSize(bytes: number): string {
|
|
51
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
52
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
53
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── IndexedDB (file blob cache) ─────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function openDB(): Promise<IDBDatabase> {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
61
|
+
req.onupgradeneeded = () => {
|
|
62
|
+
const db = req.result;
|
|
63
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
64
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'name' });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
req.onsuccess = () => resolve(req.result);
|
|
68
|
+
req.onerror = () => reject(req.error);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Cache file blobs in IndexedDB for instant reload from palette. */
|
|
73
|
+
export async function cacheFileBlobs(files: File[]): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
const db = await openDB();
|
|
76
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
77
|
+
const store = tx.objectStore(STORE_NAME);
|
|
78
|
+
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
if (file.size > MAX_CACHE_SIZE) continue; // skip oversized files
|
|
81
|
+
const blob = await file.arrayBuffer();
|
|
82
|
+
store.put({ name: file.name, blob, size: file.size, type: file.type, timestamp: Date.now() });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Evict old entries beyond MAX_CACHED_FILES
|
|
86
|
+
const allReq = store.getAll();
|
|
87
|
+
allReq.onsuccess = () => {
|
|
88
|
+
const all = allReq.result as { name: string; timestamp: number }[];
|
|
89
|
+
if (all.length > MAX_CACHED_FILES) {
|
|
90
|
+
all.sort((a, b) => b.timestamp - a.timestamp);
|
|
91
|
+
for (let i = MAX_CACHED_FILES; i < all.length; i++) {
|
|
92
|
+
store.delete(all[i].name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
tx.oncomplete = () => resolve();
|
|
99
|
+
tx.onerror = () => reject(tx.error);
|
|
100
|
+
});
|
|
101
|
+
db.close();
|
|
102
|
+
} catch { /* IndexedDB unavailable — degrade gracefully */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Retrieve a cached file blob and reconstruct a File object. */
|
|
106
|
+
export async function getCachedFile(name: string): Promise<File | null> {
|
|
107
|
+
try {
|
|
108
|
+
const db = await openDB();
|
|
109
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
110
|
+
const store = tx.objectStore(STORE_NAME);
|
|
111
|
+
const req = store.get(name);
|
|
112
|
+
const result = await new Promise<{ name: string; blob: ArrayBuffer; size: number; type: string } | undefined>((resolve, reject) => {
|
|
113
|
+
req.onsuccess = () => resolve(req.result);
|
|
114
|
+
req.onerror = () => reject(req.error);
|
|
115
|
+
});
|
|
116
|
+
db.close();
|
|
117
|
+
if (!result) return null;
|
|
118
|
+
return new File([result.blob], result.name, { type: result.type || 'application/octet-stream' });
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|