@ifc-lite/viewer 1.17.4 → 1.18.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 +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
* Cap-surface appearance controls shown inside the expanded Section panel.
|
|
7
|
+
*
|
|
8
|
+
* Layout principle (tight compact panel, ≤ 260 px wide):
|
|
9
|
+
* [Display] Surfaces ⬛ Lines ⬛
|
|
10
|
+
* ───────────────────────────────────────────────
|
|
11
|
+
* [Hatch] <pattern select>
|
|
12
|
+
* [Colours] Fill ▣ Hatch ▣
|
|
13
|
+
* [Shape] Spacing __px Angle __° Width __px
|
|
14
|
+
*
|
|
15
|
+
* Surfaces and Lines toggle independently so users can get a clean
|
|
16
|
+
* "architectural drawing" look (outlines only), a pure hatched fill, or
|
|
17
|
+
* the combination. All style inputs are hidden when Surfaces is off.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useCallback, useId } from 'react';
|
|
21
|
+
import { useViewerStore } from '@/store';
|
|
22
|
+
import type { SectionCapHatchId } from '@/store/types';
|
|
23
|
+
|
|
24
|
+
const PATTERN_LABELS: Record<SectionCapHatchId, string> = {
|
|
25
|
+
solid: 'Solid fill',
|
|
26
|
+
diagonal: 'Diagonal',
|
|
27
|
+
crossHatch: 'Cross-hatch',
|
|
28
|
+
horizontal: 'Horizontal',
|
|
29
|
+
vertical: 'Vertical',
|
|
30
|
+
concrete: 'Concrete',
|
|
31
|
+
brick: 'Brick',
|
|
32
|
+
insulation: 'Insulation',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const PATTERN_IDS: SectionCapHatchId[] = [
|
|
36
|
+
'diagonal', 'crossHatch', 'horizontal', 'vertical',
|
|
37
|
+
'concrete', 'brick', 'insulation', 'solid',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function rgbaToHex(c: [number, number, number, number]): string {
|
|
41
|
+
const to2 = (v: number) => Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16).padStart(2, '0');
|
|
42
|
+
return `#${to2(c[0])}${to2(c[1])}${to2(c[2])}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hexToRgba(hex: string, alpha: number): [number, number, number, number] {
|
|
46
|
+
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
47
|
+
if (!m) return [1, 1, 1, alpha];
|
|
48
|
+
return [
|
|
49
|
+
parseInt(m[1], 16) / 255,
|
|
50
|
+
parseInt(m[2], 16) / 255,
|
|
51
|
+
parseInt(m[3], 16) / 255,
|
|
52
|
+
alpha,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface DisplayToggleProps {
|
|
57
|
+
active: boolean;
|
|
58
|
+
label: string;
|
|
59
|
+
onToggle: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function DisplayToggle({ active, label, onToggle }: DisplayToggleProps): React.JSX.Element {
|
|
63
|
+
return (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={onToggle}
|
|
67
|
+
aria-pressed={active}
|
|
68
|
+
className={`flex items-center justify-center gap-1.5 px-2 py-1 text-[10px] font-mono uppercase tracking-wide border rounded transition-colors ${
|
|
69
|
+
active
|
|
70
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
71
|
+
: 'bg-muted text-muted-foreground border-muted hover:border-foreground/20'
|
|
72
|
+
}`}
|
|
73
|
+
title={`${active ? 'Hide' : 'Show'} ${label.toLowerCase()}`}
|
|
74
|
+
>
|
|
75
|
+
<span
|
|
76
|
+
aria-hidden
|
|
77
|
+
className={`inline-block h-1.5 w-1.5 rounded-full ${active ? 'bg-primary-foreground' : 'bg-muted-foreground'}`}
|
|
78
|
+
/>
|
|
79
|
+
{label}
|
|
80
|
+
</button>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function SectionCapControls(): React.JSX.Element {
|
|
85
|
+
const sectionPlane = useViewerStore((s) => s.sectionPlane);
|
|
86
|
+
const setShowCap = useViewerStore((s) => s.setSectionShowCap);
|
|
87
|
+
const setShowOutlines = useViewerStore((s) => s.setSectionShowOutlines);
|
|
88
|
+
const setCapStyle = useViewerStore((s) => s.setSectionCapStyle);
|
|
89
|
+
|
|
90
|
+
const onPattern = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
91
|
+
setCapStyle({ pattern: e.target.value as SectionCapHatchId });
|
|
92
|
+
}, [setCapStyle]);
|
|
93
|
+
|
|
94
|
+
const onFillColor = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
95
|
+
setCapStyle({ fillColor: hexToRgba(e.target.value, sectionPlane.capStyle.fillColor[3]) });
|
|
96
|
+
}, [setCapStyle, sectionPlane.capStyle.fillColor]);
|
|
97
|
+
|
|
98
|
+
const onStrokeColor = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
+
setCapStyle({ strokeColor: hexToRgba(e.target.value, sectionPlane.capStyle.strokeColor[3]) });
|
|
100
|
+
}, [setCapStyle, sectionPlane.capStyle.strokeColor]);
|
|
101
|
+
|
|
102
|
+
const onSpacing = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
103
|
+
const v = Number(e.target.value);
|
|
104
|
+
if (Number.isFinite(v)) setCapStyle({ spacingPx: Math.max(2, v) });
|
|
105
|
+
}, [setCapStyle]);
|
|
106
|
+
|
|
107
|
+
const onAngle = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
108
|
+
const deg = Number(e.target.value);
|
|
109
|
+
if (Number.isFinite(deg)) setCapStyle({ angleRad: (deg * Math.PI) / 180 });
|
|
110
|
+
}, [setCapStyle]);
|
|
111
|
+
|
|
112
|
+
const onWidth = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
113
|
+
const v = Number(e.target.value);
|
|
114
|
+
if (Number.isFinite(v)) setCapStyle({ widthPx: Math.max(1, v) });
|
|
115
|
+
}, [setCapStyle]);
|
|
116
|
+
|
|
117
|
+
const onToggleCap = useCallback(() => setShowCap(!sectionPlane.showCap), [setShowCap, sectionPlane.showCap]);
|
|
118
|
+
const onToggleOutlines = useCallback(() => setShowOutlines(!sectionPlane.showOutlines), [setShowOutlines, sectionPlane.showOutlines]);
|
|
119
|
+
|
|
120
|
+
const angleDeg = Math.round((sectionPlane.capStyle.angleRad * 180) / Math.PI);
|
|
121
|
+
|
|
122
|
+
// Stable ids for label/control association. Multiple instances of the
|
|
123
|
+
// panel (rare, but possible during HMR) each get their own id namespace.
|
|
124
|
+
const baseId = useId();
|
|
125
|
+
const patternId = `${baseId}-pattern`;
|
|
126
|
+
const fillId = `${baseId}-fill`;
|
|
127
|
+
const strokeId = `${baseId}-stroke`;
|
|
128
|
+
const spacingId = `${baseId}-spacing`;
|
|
129
|
+
const angleId = `${baseId}-angle`;
|
|
130
|
+
const widthId = `${baseId}-width`;
|
|
131
|
+
|
|
132
|
+
const hatchInputsDisabled = !sectionPlane.showCap;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="mt-3 border-t pt-3 space-y-3">
|
|
136
|
+
{/* Display toggles — surfaces and lines independently. */}
|
|
137
|
+
<div>
|
|
138
|
+
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1.5">Display</div>
|
|
139
|
+
<div className="grid grid-cols-2 gap-2">
|
|
140
|
+
<DisplayToggle active={sectionPlane.showCap} label="Surfaces" onToggle={onToggleCap} />
|
|
141
|
+
<DisplayToggle active={sectionPlane.showOutlines} label="Lines" onToggle={onToggleOutlines} />
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Hatch style — disabled visually when surfaces are off. */}
|
|
146
|
+
<fieldset
|
|
147
|
+
disabled={hatchInputsDisabled}
|
|
148
|
+
className={`space-y-2 ${hatchInputsDisabled ? 'opacity-50 pointer-events-none' : ''}`}
|
|
149
|
+
>
|
|
150
|
+
<div>
|
|
151
|
+
<label htmlFor={patternId} className="text-[10px] uppercase tracking-wider text-muted-foreground block mb-1">
|
|
152
|
+
Hatch pattern
|
|
153
|
+
</label>
|
|
154
|
+
<select
|
|
155
|
+
id={patternId}
|
|
156
|
+
value={sectionPlane.capStyle.pattern}
|
|
157
|
+
onChange={onPattern}
|
|
158
|
+
className="w-full text-xs bg-muted px-2 py-1.5 rounded border-none"
|
|
159
|
+
>
|
|
160
|
+
{PATTERN_IDS.map((id) => (
|
|
161
|
+
<option key={id} value={id}>{PATTERN_LABELS[id]}</option>
|
|
162
|
+
))}
|
|
163
|
+
</select>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="grid grid-cols-2 gap-2">
|
|
167
|
+
<label htmlFor={fillId} className="flex items-center gap-2 text-[10px] uppercase tracking-wider text-muted-foreground">
|
|
168
|
+
<input
|
|
169
|
+
id={fillId}
|
|
170
|
+
type="color"
|
|
171
|
+
value={rgbaToHex(sectionPlane.capStyle.fillColor)}
|
|
172
|
+
onChange={onFillColor}
|
|
173
|
+
className="h-5 w-5 rounded cursor-pointer border border-muted"
|
|
174
|
+
aria-label="Fill colour"
|
|
175
|
+
/>
|
|
176
|
+
Fill
|
|
177
|
+
</label>
|
|
178
|
+
<label htmlFor={strokeId} className="flex items-center gap-2 text-[10px] uppercase tracking-wider text-muted-foreground">
|
|
179
|
+
<input
|
|
180
|
+
id={strokeId}
|
|
181
|
+
type="color"
|
|
182
|
+
value={rgbaToHex(sectionPlane.capStyle.strokeColor)}
|
|
183
|
+
onChange={onStrokeColor}
|
|
184
|
+
className="h-5 w-5 rounded cursor-pointer border border-muted"
|
|
185
|
+
aria-label="Hatch colour"
|
|
186
|
+
/>
|
|
187
|
+
Hatch
|
|
188
|
+
</label>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="grid grid-cols-3 gap-2">
|
|
192
|
+
<div>
|
|
193
|
+
<label htmlFor={spacingId} className="text-[10px] text-muted-foreground block mb-1">Spacing (px)</label>
|
|
194
|
+
<input
|
|
195
|
+
id={spacingId}
|
|
196
|
+
type="number"
|
|
197
|
+
min="2"
|
|
198
|
+
max="64"
|
|
199
|
+
step="1"
|
|
200
|
+
value={sectionPlane.capStyle.spacingPx}
|
|
201
|
+
onChange={onSpacing}
|
|
202
|
+
className="w-full text-xs bg-muted px-1.5 py-0.5 rounded border-none text-right"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
<div>
|
|
206
|
+
<label htmlFor={angleId} className="text-[10px] text-muted-foreground block mb-1">Angle (°)</label>
|
|
207
|
+
<input
|
|
208
|
+
id={angleId}
|
|
209
|
+
type="number"
|
|
210
|
+
min="-180"
|
|
211
|
+
max="180"
|
|
212
|
+
step="5"
|
|
213
|
+
value={angleDeg}
|
|
214
|
+
onChange={onAngle}
|
|
215
|
+
className="w-full text-xs bg-muted px-1.5 py-0.5 rounded border-none text-right"
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<label htmlFor={widthId} className="text-[10px] text-muted-foreground block mb-1">Width (px)</label>
|
|
220
|
+
<input
|
|
221
|
+
id={widthId}
|
|
222
|
+
type="number"
|
|
223
|
+
min="1"
|
|
224
|
+
max="16"
|
|
225
|
+
step="0.5"
|
|
226
|
+
value={sectionPlane.capStyle.widthPx}
|
|
227
|
+
onChange={onWidth}
|
|
228
|
+
className="w-full text-xs bg-muted px-1.5 py-0.5 rounded border-none text-right"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</fieldset>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default SectionCapControls;
|
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { useCallback, useState } from 'react';
|
|
10
|
-
import { X, Slice, ChevronDown, FileImage } from 'lucide-react';
|
|
10
|
+
import { X, Slice, ChevronDown, FileImage, FlipHorizontal2 } from 'lucide-react';
|
|
11
11
|
import { Button } from '@/components/ui/button';
|
|
12
12
|
import { useViewerStore } from '@/store';
|
|
13
13
|
import { AXIS_INFO } from './sectionConstants';
|
|
14
14
|
import { SectionPlaneVisualization } from './SectionVisualization';
|
|
15
|
+
import { SectionCapControls } from './SectionCapControls';
|
|
15
16
|
|
|
16
17
|
export function SectionOverlay() {
|
|
17
18
|
const sectionPlane = useViewerStore((s) => s.sectionPlane);
|
|
18
19
|
const setSectionPlaneAxis = useViewerStore((s) => s.setSectionPlaneAxis);
|
|
19
20
|
const setSectionPlanePosition = useViewerStore((s) => s.setSectionPlanePosition);
|
|
20
21
|
const toggleSectionPlane = useViewerStore((s) => s.toggleSectionPlane);
|
|
22
|
+
const flipSectionPlane = useViewerStore((s) => s.flipSectionPlane);
|
|
21
23
|
const setActiveTool = useViewerStore((s) => s.setActiveTool);
|
|
22
24
|
const setDrawingPanelVisible = useViewerStore((s) => s.setDrawing2DPanelVisible);
|
|
23
25
|
const drawingPanelVisible = useViewerStore((s) => s.drawing2DPanelVisible);
|
|
@@ -83,10 +85,10 @@ export function SectionOverlay() {
|
|
|
83
85
|
|
|
84
86
|
{/* Expandable content */}
|
|
85
87
|
{!isPanelCollapsed && (
|
|
86
|
-
<div className="border-t px-3 pb-3 min-w-
|
|
88
|
+
<div className="border-t px-3 pb-3 min-w-72">
|
|
87
89
|
{/* Direction Selection */}
|
|
88
90
|
<div className="mt-3">
|
|
89
|
-
<
|
|
91
|
+
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1.5">Direction</div>
|
|
90
92
|
<div className="flex gap-1">
|
|
91
93
|
{(['down', 'front', 'side'] as const).map((axis) => (
|
|
92
94
|
<Button
|
|
@@ -105,16 +107,29 @@ export function SectionOverlay() {
|
|
|
105
107
|
{/* Position Slider */}
|
|
106
108
|
<div className="mt-3">
|
|
107
109
|
<div className="flex items-center justify-between mb-1">
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">Position</div>
|
|
111
|
+
<div className="flex items-center gap-1">
|
|
112
|
+
<Button
|
|
113
|
+
variant={sectionPlane.flipped ? 'default' : 'ghost'}
|
|
114
|
+
size="icon-sm"
|
|
115
|
+
onClick={flipSectionPlane}
|
|
116
|
+
aria-pressed={sectionPlane.flipped}
|
|
117
|
+
aria-label={sectionPlane.flipped ? 'Unflip cut direction' : 'Flip cut direction'}
|
|
118
|
+
title={sectionPlane.flipped ? 'Cut direction is flipped' : 'Flip cut direction'}
|
|
119
|
+
>
|
|
120
|
+
<FlipHorizontal2 className="h-3 w-3" />
|
|
121
|
+
</Button>
|
|
122
|
+
<input
|
|
123
|
+
type="number"
|
|
124
|
+
min="0"
|
|
125
|
+
max="100"
|
|
126
|
+
step="0.1"
|
|
127
|
+
value={sectionPlane.position}
|
|
128
|
+
onChange={handlePositionChange}
|
|
129
|
+
aria-label="Section plane position percentage"
|
|
130
|
+
className="w-16 text-xs font-mono bg-muted px-1.5 py-0.5 rounded border-none text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
118
133
|
</div>
|
|
119
134
|
<input
|
|
120
135
|
type="range"
|
|
@@ -123,10 +138,14 @@ export function SectionOverlay() {
|
|
|
123
138
|
step="0.1"
|
|
124
139
|
value={sectionPlane.position}
|
|
125
140
|
onChange={handlePositionChange}
|
|
141
|
+
aria-label="Section plane position slider"
|
|
126
142
|
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary"
|
|
127
143
|
/>
|
|
128
144
|
</div>
|
|
129
145
|
|
|
146
|
+
{/* Cap surface controls (hatch, colour, spacing) */}
|
|
147
|
+
<SectionCapControls />
|
|
148
|
+
|
|
130
149
|
{/* Show 2D panel button - only when panel is closed */}
|
|
131
150
|
{!drawingPanelVisible && (
|
|
132
151
|
<div className="mt-3 pt-3 border-t">
|
|
@@ -156,12 +175,14 @@ export function SectionOverlay() {
|
|
|
156
175
|
>
|
|
157
176
|
<span className="font-mono text-xs uppercase tracking-wide">
|
|
158
177
|
{sectionPlane.enabled
|
|
159
|
-
? `
|
|
160
|
-
: '
|
|
178
|
+
? `Cut ${AXIS_INFO[sectionPlane.axis].label.toLowerCase()} at ${sectionPlane.position.toFixed(1)}%${sectionPlane.flipped ? ' (flipped)' : ''}`
|
|
179
|
+
: 'Clip off — drag slider to cut'}
|
|
161
180
|
</span>
|
|
162
181
|
</div>
|
|
163
182
|
|
|
164
|
-
{/* Enable toggle
|
|
183
|
+
{/* Enable toggle — when OFF the model is not clipped even though the
|
|
184
|
+
plane visual is shown. Label is explicit so users don't mistake
|
|
185
|
+
"Preview" for "nothing will happen". */}
|
|
165
186
|
<div className="pointer-events-auto absolute bottom-4 left-1/2 -translate-x-1/2 z-30">
|
|
166
187
|
<button
|
|
167
188
|
onClick={toggleSectionPlane}
|
|
@@ -170,9 +191,9 @@ export function SectionOverlay() {
|
|
|
170
191
|
? 'bg-primary text-primary-foreground border-primary'
|
|
171
192
|
: 'bg-zinc-100 dark:bg-zinc-900 text-zinc-500 border-zinc-300 dark:border-zinc-700'
|
|
172
193
|
}`}
|
|
173
|
-
title=
|
|
194
|
+
title={sectionPlane.enabled ? 'Click to disable the cut' : 'Click to enable the cut'}
|
|
174
195
|
>
|
|
175
|
-
{sectionPlane.enabled ? '
|
|
196
|
+
{sectionPlane.enabled ? 'Clipping' : 'Clip off'}
|
|
176
197
|
</button>
|
|
177
198
|
</div>
|
|
178
199
|
|
|
@@ -170,7 +170,15 @@ export function useAnimationLoop(params: UseAnimationLoopParams): void {
|
|
|
170
170
|
isInteracting: isInteractingRef.current || isAnimating,
|
|
171
171
|
buildingRotation: coordinateInfoRef.current?.buildingRotation,
|
|
172
172
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
173
|
-
|
|
173
|
+
axis: sectionPlaneRef.current.axis,
|
|
174
|
+
position: sectionPlaneRef.current.position,
|
|
175
|
+
enabled: sectionPlaneRef.current.enabled,
|
|
176
|
+
flipped: sectionPlaneRef.current.flipped,
|
|
177
|
+
// Cap rendering settings — the renderer reads these to draw the
|
|
178
|
+
// filled, hatched cut surfaces.
|
|
179
|
+
showCap: sectionPlaneRef.current.showCap,
|
|
180
|
+
showOutlines: sectionPlaneRef.current.showOutlines,
|
|
181
|
+
capStyle: sectionPlaneRef.current.capStyle,
|
|
174
182
|
min: sectionRangeRef.current?.min,
|
|
175
183
|
max: sectionRangeRef.current?.max,
|
|
176
184
|
} : undefined,
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
* Global ⌘D / Ctrl+D shortcut for duplicating the selected entity.
|
|
7
|
+
*
|
|
8
|
+
* Lives outside `useKeyboardControls` so the camera-movement loop
|
|
9
|
+
* stays focused on its job; the duplicate flow doesn't need
|
|
10
|
+
* keyState tracking or per-frame work, just a one-shot trigger.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the right-click menu's gating: only fires when there's a
|
|
13
|
+
* selection and the active model has a live mutation view.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect } from 'react';
|
|
17
|
+
import { useViewerStore, resolveEntityRef } from '@/store';
|
|
18
|
+
import { toast } from '@/components/ui/toast';
|
|
19
|
+
|
|
20
|
+
export function useDuplicateShortcut() {
|
|
21
|
+
const duplicateEntity = useViewerStore((s) => s.duplicateEntity);
|
|
22
|
+
const getMutationView = useViewerStore((s) => s.getMutationView);
|
|
23
|
+
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const handler = (e: KeyboardEvent) => {
|
|
27
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
28
|
+
if (e.key !== 'd' && e.key !== 'D') return;
|
|
29
|
+
|
|
30
|
+
// Ignore when the user is typing somewhere — Ctrl+D in an
|
|
31
|
+
// input usually means "delete word forward" or browser-bookmark.
|
|
32
|
+
const target = e.target as HTMLElement | null;
|
|
33
|
+
if (
|
|
34
|
+
target?.tagName === 'INPUT' ||
|
|
35
|
+
target?.tagName === 'TEXTAREA' ||
|
|
36
|
+
target?.isContentEditable
|
|
37
|
+
) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const state = useViewerStore.getState();
|
|
42
|
+
const selectedId = state.selectedEntityId;
|
|
43
|
+
if (selectedId === null) return;
|
|
44
|
+
|
|
45
|
+
const ref = resolveEntityRef(selectedId);
|
|
46
|
+
if (!ref) return;
|
|
47
|
+
|
|
48
|
+
// Suppress the browser's bookmark default for any duplicate
|
|
49
|
+
// shortcut we recognise — even when the model has no editable
|
|
50
|
+
// mutation view, otherwise Ctrl/⌘+D opens the bookmark dialog
|
|
51
|
+
// while we're "silently no-op'ing" below.
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
|
|
55
|
+
// Match the menu's canEdit gating — silently no-op on
|
|
56
|
+
// native-metadata models.
|
|
57
|
+
const view = getMutationView(ref.modelId);
|
|
58
|
+
if (!view) return;
|
|
59
|
+
|
|
60
|
+
// ⌘D + Shift = +Z (up), ⌘D + Alt = +Y (north), default = +X (east).
|
|
61
|
+
// Power users can chain modifiers without leaving the keyboard;
|
|
62
|
+
// the menu's chip row covers everyone else.
|
|
63
|
+
const direction = e.shiftKey ? '+Z' : e.altKey ? '+Y' : '+X';
|
|
64
|
+
|
|
65
|
+
const result = duplicateEntity(ref.modelId, ref.expressId, direction);
|
|
66
|
+
if ('error' in result) {
|
|
67
|
+
toast.error(`Couldn't duplicate: ${result.error}`);
|
|
68
|
+
} else {
|
|
69
|
+
setSelectedEntityId(result.globalId);
|
|
70
|
+
toast.success(`Duplicated as #${result.expressId} (${direction}) — undo to remove`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
window.addEventListener('keydown', handler);
|
|
75
|
+
return () => window.removeEventListener('keydown', handler);
|
|
76
|
+
}, [duplicateEntity, getMutationView, setSelectedEntityId]);
|
|
77
|
+
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import type { MeasurementConstraintEdge, OrthogonalAxis, Vec3 } from '@/store/types.js';
|
|
23
23
|
import { getEntityCenter } from '../../utils/viewportUtils.js';
|
|
24
24
|
import type { MouseHandlerContext } from './mouseHandlerTypes.js';
|
|
25
|
+
import { useViewerStore } from '@/store';
|
|
25
26
|
import {
|
|
26
27
|
handleMeasureDown,
|
|
27
28
|
handleMeasureDrag,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
handleMeasureUp,
|
|
30
31
|
updateMeasureScreenCoords,
|
|
31
32
|
} from './measureHandlers.js';
|
|
32
|
-
import { handleSelectionClick, handleContextMenu as handleContextMenuSelection } from './selectionHandlers.js';
|
|
33
|
+
import { handleSelectionClick, handleContextMenu as handleContextMenuSelection, handleAddElementHover } from './selectionHandlers.js';
|
|
33
34
|
|
|
34
35
|
export interface MouseState {
|
|
35
36
|
isDragging: boolean;
|
|
@@ -368,6 +369,13 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
368
369
|
if (handleMeasureHover(ctx, x, y)) return;
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
// Add-element tool hover preview. Always runs (regardless of
|
|
373
|
+
// snap toggle) so the live edge/rectangle/polygon overlay can
|
|
374
|
+
// track the cursor; magnetic snap is layered on when enabled.
|
|
375
|
+
if (tool === 'addElement' && !mouseState.isDragging) {
|
|
376
|
+
if (handleAddElementHover(ctx, x, y)) return;
|
|
377
|
+
}
|
|
378
|
+
|
|
371
379
|
// Handle orbit/pan for other tools (or measure tool with shift+drag or no active measurement)
|
|
372
380
|
if (mouseState.isDragging && (tool !== 'measure' || !activeMeasurementRef.current)) {
|
|
373
381
|
const dx = e.clientX - mouseState.lastX;
|
|
@@ -77,7 +77,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
77
77
|
|
|
78
78
|
// Theme-aware clear color update
|
|
79
79
|
useEffect(() => {
|
|
80
|
-
clearColorRef.current = getThemeClearColor(theme as 'light' | 'dark');
|
|
80
|
+
clearColorRef.current = getThemeClearColor(theme as 'light' | 'dark' | 'colorful');
|
|
81
81
|
rendererRef.current?.requestRender();
|
|
82
82
|
}, [theme, isInitialized]);
|
|
83
83
|
|
|
@@ -19,7 +19,11 @@ import type {
|
|
|
19
19
|
ParentInfo,
|
|
20
20
|
PartOfRelation,
|
|
21
21
|
} from '@ifc-lite/ids';
|
|
22
|
-
import
|
|
22
|
+
import {
|
|
23
|
+
type IfcDataStore,
|
|
24
|
+
extractAllEntityAttributes,
|
|
25
|
+
extractTypeEntityOwnProperties,
|
|
26
|
+
} from '@ifc-lite/parser';
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Create an IFCDataAccessor from an IfcDataStore
|
|
@@ -59,7 +63,26 @@ export function createDataAccessor(
|
|
|
59
63
|
},
|
|
60
64
|
|
|
61
65
|
getObjectType(expressId: number): string | undefined {
|
|
62
|
-
|
|
66
|
+
// Try the pre-computed ObjectType first (works for IfcObject subtypes)
|
|
67
|
+
const objectType = dataStore.entities?.getObjectType?.(expressId);
|
|
68
|
+
if (objectType) return objectType;
|
|
69
|
+
|
|
70
|
+
// For IfcTypeObject subtypes (IfcWallType, etc.), ObjectType doesn't exist —
|
|
71
|
+
// we need to extract PredefinedType from entity attributes instead.
|
|
72
|
+
// Also handles IfcObject subtypes that have PredefinedType at different positions.
|
|
73
|
+
const allAttrs = extractAllEntityAttributes(dataStore, expressId);
|
|
74
|
+
const predefinedType = allAttrs.find(a => a.name === 'PredefinedType');
|
|
75
|
+
if (predefinedType?.value && predefinedType.value !== 'NOTDEFINED') {
|
|
76
|
+
return predefinedType.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If PredefinedType is USERDEFINED or absent, check ObjectType from attributes
|
|
80
|
+
const objTypeAttr = allAttrs.find(a => a.name === 'ObjectType');
|
|
81
|
+
if (objTypeAttr?.value) {
|
|
82
|
+
return objTypeAttr.value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return undefined;
|
|
63
86
|
},
|
|
64
87
|
|
|
65
88
|
getEntitiesByType(typeName: string): number[] {
|
|
@@ -119,29 +142,42 @@ export function createDataAccessor(
|
|
|
119
142
|
|
|
120
143
|
getPropertySets(expressId: number): PropertySetInfo[] {
|
|
121
144
|
const propertiesStore = dataStore.properties;
|
|
122
|
-
if (!propertiesStore) return [];
|
|
123
145
|
|
|
124
|
-
//
|
|
125
|
-
const psets = propertiesStore
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
// Try relationship-based properties first (works for IfcObject instances)
|
|
147
|
+
const psets = propertiesStore?.getForEntity?.(expressId);
|
|
148
|
+
|
|
149
|
+
// Convert property sets to the IDS format
|
|
150
|
+
const mapPsets = (rawPsets: Array<{ name: string; properties: Array<{ name: string; value: unknown; type: unknown }> }>): PropertySetInfo[] =>
|
|
151
|
+
rawPsets.map((pset) => ({
|
|
152
|
+
name: pset.name,
|
|
153
|
+
properties: (pset.properties || []).map((prop) => {
|
|
154
|
+
let value: string | number | boolean | null = null;
|
|
155
|
+
if (Array.isArray(prop.value)) {
|
|
156
|
+
value = JSON.stringify(prop.value);
|
|
157
|
+
} else {
|
|
158
|
+
value = prop.value as string | number | boolean | null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
name: prop.name,
|
|
162
|
+
value,
|
|
163
|
+
dataType: String(prop.type || 'IFCLABEL'),
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
if (psets && psets.length > 0) {
|
|
169
|
+
return mapPsets(psets);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// For IfcTypeObject subtypes (IfcWallType, IfcSlabType, etc.),
|
|
173
|
+
// properties come from the HasPropertySets attribute (index 5),
|
|
174
|
+
// not from IfcRelDefinesByProperties relationships.
|
|
175
|
+
const typePsets = extractTypeEntityOwnProperties(dataStore, expressId);
|
|
176
|
+
if (typePsets.length > 0) {
|
|
177
|
+
return mapPsets(typePsets);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return [];
|
|
145
181
|
},
|
|
146
182
|
|
|
147
183
|
getClassifications(expressId: number): ClassificationInfo[] {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
5
|
import { IfcParser, parseIfcx, type IfcDataStore } from '@ifc-lite/parser';
|
|
6
|
-
import { GeometryProcessor, GeometryQuality, type CoordinateInfo, type GeometryResult, type MeshData } from '@ifc-lite/geometry';
|
|
6
|
+
import { GeometryProcessor, GeometryQuality, type CoordinateInfo, type DynamicBatchConfig, type GeometryResult, type MeshData } from '@ifc-lite/geometry';
|
|
7
7
|
import { loadGLBToMeshData } from '@ifc-lite/cache';
|
|
8
8
|
import type { SchemaVersion } from '../../store/types.js';
|
|
9
9
|
import { calculateMeshBounds, calculateStoreyHeights, createCoordinateInfo, normalizeColor } from '../../utils/localParsingUtils.js';
|
|
@@ -44,13 +44,17 @@ export interface StepBufferIngestOptions {
|
|
|
44
44
|
fileName: string;
|
|
45
45
|
buffer: ArrayBuffer;
|
|
46
46
|
fileSizeMB: number;
|
|
47
|
-
getDynamicBatchSize: (fileSizeMB: number) => number |
|
|
47
|
+
getDynamicBatchSize: (fileSizeMB: number) => number | DynamicBatchConfig;
|
|
48
48
|
onProgress?: (progress: { phase: string; percent: number }) => void;
|
|
49
49
|
onBatch?: (event: StepBatchEvent) => void;
|
|
50
50
|
onColorUpdate?: (updates: Map<number, RgbaColor>) => void;
|
|
51
51
|
onSpatialReady?: (dataStore: IfcDataStore) => void;
|
|
52
52
|
onRtcOffset?: (event: StepRtcOffsetEvent) => void;
|
|
53
53
|
shouldAbort?: () => boolean;
|
|
54
|
+
/** Shared RTC offset from first federated model (IFC Z-up coords).
|
|
55
|
+
* When set, this model uses the same RTC as the first model instead of
|
|
56
|
+
* computing its own, ensuring all models share the same coordinate space. */
|
|
57
|
+
sharedRtcOffset?: { x: number; y: number; z: number };
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export interface StepBufferIngestResult extends ViewerModelPayload {
|
|
@@ -204,6 +208,7 @@ export async function parseStepBufferViewerModel(options: StepBufferIngestOption
|
|
|
204
208
|
for await (const event of geometryProcessor.processAdaptive(new Uint8Array(options.buffer), {
|
|
205
209
|
sizeThreshold: 2 * 1024 * 1024,
|
|
206
210
|
batchSize: options.getDynamicBatchSize(options.fileSizeMB),
|
|
211
|
+
sharedRtcOffset: options.sharedRtcOffset,
|
|
207
212
|
})) {
|
|
208
213
|
if (options.shouldAbort?.()) {
|
|
209
214
|
break;
|