@ifc-lite/viewer 1.17.6 → 1.19.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 -15
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +949 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/sandbox-Baez7n-t.js +9682 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +11 -10
- 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/ChatPanel.tsx +64 -2
- 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 -12
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- 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/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +29 -2
- package/src/components/viewer/ViewportContainer.tsx +45 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -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 +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- 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 +581 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +89 -13
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -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 +6 -0
- 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/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/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +8 -3
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +79 -1
- 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/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/pointCloudSlice.ts +102 -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/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/types.ts +7 -0
- package/src/store.ts +14 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
package/src/store/index.ts
CHANGED
|
@@ -34,6 +34,13 @@ import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
|
|
|
34
34
|
import { createChatSlice, type ChatSlice } from './slices/chatSlice.js';
|
|
35
35
|
import { createCesiumSlice, type CesiumSlice } from './slices/cesiumSlice.js';
|
|
36
36
|
import { createDesktopEntitlementSlice, type DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.js';
|
|
37
|
+
import { createScheduleSlice, type ScheduleSlice } from './slices/scheduleSlice.js';
|
|
38
|
+
import { createPlaybackSlice, type PlaybackSlice } from './slices/playbackSlice.js';
|
|
39
|
+
import { createOverlaySlice, type OverlaySlice } from './slices/overlaySlice.js';
|
|
40
|
+
import { createSearchSlice, type SearchSlice } from './slices/searchSlice.js';
|
|
41
|
+
import { createAnnotationsSlice, type AnnotationsSlice } from './slices/annotationsSlice.js';
|
|
42
|
+
import { createAddElementSlice, type AddElementSlice } from './slices/addElementSlice.js';
|
|
43
|
+
import { createPointCloudSlice, type PointCloudSlice, POINT_CLOUD_DEFAULTS } from './slices/pointCloudSlice.js';
|
|
37
44
|
import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
|
|
38
45
|
|
|
39
46
|
// Import constants for reset function
|
|
@@ -83,6 +90,21 @@ export type { DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.j
|
|
|
83
90
|
// Re-export Cesium types
|
|
84
91
|
export type { CesiumSlice, CesiumDataSource } from './slices/cesiumSlice.js';
|
|
85
92
|
|
|
93
|
+
// Re-export Schedule (4D) types + selectors
|
|
94
|
+
export type { ScheduleSlice, ScheduleTimeRange, GanttTimeScale } from './slices/scheduleSlice.js';
|
|
95
|
+
export type { PlaybackSlice } from './slices/playbackSlice.js';
|
|
96
|
+
export type { OverlaySlice, OverlayLayer, RGBA as OverlayRGBA } from './slices/overlaySlice.js';
|
|
97
|
+
export { composeLayers as composeOverlayLayers } from './slices/overlaySlice.js';
|
|
98
|
+
export {
|
|
99
|
+
computeScheduleRange,
|
|
100
|
+
computeHiddenProductIds,
|
|
101
|
+
computeActiveProductIds,
|
|
102
|
+
taskStartEpoch,
|
|
103
|
+
taskFinishEpoch,
|
|
104
|
+
parseIsoDate,
|
|
105
|
+
} from './slices/scheduleSlice.js';
|
|
106
|
+
export { resolveScheduleSourceModelId } from './slices/schedule-edit-helpers.js';
|
|
107
|
+
|
|
86
108
|
// Combined store type
|
|
87
109
|
export type ViewerState = LoadingSlice &
|
|
88
110
|
SelectionSlice &
|
|
@@ -105,7 +127,14 @@ export type ViewerState = LoadingSlice &
|
|
|
105
127
|
ScriptSlice &
|
|
106
128
|
ChatSlice &
|
|
107
129
|
CesiumSlice &
|
|
108
|
-
DesktopEntitlementSlice &
|
|
130
|
+
DesktopEntitlementSlice &
|
|
131
|
+
ScheduleSlice &
|
|
132
|
+
PlaybackSlice &
|
|
133
|
+
OverlaySlice &
|
|
134
|
+
SearchSlice &
|
|
135
|
+
AnnotationsSlice &
|
|
136
|
+
AddElementSlice &
|
|
137
|
+
PointCloudSlice & {
|
|
109
138
|
resetViewerState: () => void;
|
|
110
139
|
};
|
|
111
140
|
|
|
@@ -136,6 +165,13 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
136
165
|
...createChatSlice(...args),
|
|
137
166
|
...createCesiumSlice(...args),
|
|
138
167
|
...createDesktopEntitlementSlice(...args),
|
|
168
|
+
...createScheduleSlice(...args),
|
|
169
|
+
...createPlaybackSlice(...args),
|
|
170
|
+
...createOverlaySlice(...args),
|
|
171
|
+
...createSearchSlice(...args),
|
|
172
|
+
...createAnnotationsSlice(...args),
|
|
173
|
+
...createAddElementSlice(...args),
|
|
174
|
+
...createPointCloudSlice(...args),
|
|
139
175
|
|
|
140
176
|
// Reset all viewer state when loading new file
|
|
141
177
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
@@ -338,6 +374,21 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
338
374
|
chatError: null,
|
|
339
375
|
chatAbortController: null,
|
|
340
376
|
|
|
377
|
+
// Schedule (4D) - drop panel + data; definitions are re-extracted on
|
|
378
|
+
// next load. `playbackSpeed`, `playbackLoop`, and `ganttTimeScale` are
|
|
379
|
+
// intentionally preserved as user preferences that survive file loads.
|
|
380
|
+
ganttPanelVisible: false,
|
|
381
|
+
generateScheduleDialogOpen: false,
|
|
382
|
+
scheduleData: null,
|
|
383
|
+
scheduleRange: null,
|
|
384
|
+
activeWorkScheduleId: '',
|
|
385
|
+
expandedTaskGlobalIds: new Set<string>(),
|
|
386
|
+
hoveredTaskGlobalId: null,
|
|
387
|
+
selectedTaskGlobalIds: new Set<string>(),
|
|
388
|
+
animationEnabled: false,
|
|
389
|
+
playbackIsPlaying: false,
|
|
390
|
+
playbackTime: 0,
|
|
391
|
+
|
|
341
392
|
// Mutations - clear all mutation state so stale changes don't carry over
|
|
342
393
|
mutationViews: new Map(),
|
|
343
394
|
changeSets: new Map(),
|
|
@@ -346,6 +397,33 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
346
397
|
redoStacks: new Map(),
|
|
347
398
|
dirtyModels: new Set(),
|
|
348
399
|
mutationVersion: get().mutationVersion + 1,
|
|
400
|
+
|
|
401
|
+
// Search - results reference the previous model's expressIds, drop them.
|
|
402
|
+
searchQuery: '',
|
|
403
|
+
searchOpen: false,
|
|
404
|
+
searchHighlightIndex: 0,
|
|
405
|
+
searchIndexes: new Map(),
|
|
406
|
+
searchVimCycle: null,
|
|
407
|
+
searchModalOpen: false,
|
|
408
|
+
searchFieldFilter: 'all',
|
|
409
|
+
searchModelFilter: null,
|
|
410
|
+
searchFilterResult: null,
|
|
411
|
+
searchFilterRunning: false,
|
|
412
|
+
searchFilterError: null,
|
|
413
|
+
searchFilter: { rules: [], combinator: 'AND', limit: 500 },
|
|
414
|
+
searchFilterSchema: new Map(),
|
|
415
|
+
|
|
416
|
+
// Annotations — drop draft + selection so a new file doesn't
|
|
417
|
+
// inherit the previous file's pin authoring state. Persisted
|
|
418
|
+
// pins themselves stay in localStorage (cross-file workspace).
|
|
419
|
+
draft: null,
|
|
420
|
+
selectedAnnotationId: null,
|
|
421
|
+
|
|
422
|
+
// Point cloud — clear runtime fields so a new file doesn't
|
|
423
|
+
// inherit the previous file's color mode / size / EDL state.
|
|
424
|
+
// Single-source-of-truth defaults shared with createPointCloudSlice.
|
|
425
|
+
...POINT_CLOUD_DEFAULTS,
|
|
426
|
+
pointCloudFixedColor: [...POINT_CLOUD_DEFAULTS.pointCloudFixedColor] as [number, number, number, number],
|
|
349
427
|
});
|
|
350
428
|
},
|
|
351
429
|
}));
|
|
@@ -0,0 +1,365 @@
|
|
|
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
|
+
* Build simple "instant preview" meshes for newly-added elements so
|
|
7
|
+
* the user sees them in 3D the moment the builder commits — without
|
|
8
|
+
* waiting for an export+re-parse round-trip.
|
|
9
|
+
*
|
|
10
|
+
* Coordinate-system note (matches the rest of the viewer):
|
|
11
|
+
* - Builder params are in **IFC storey-local** metres (Z-up).
|
|
12
|
+
* - The renderer is **Y-up** with `viewer.y = ifc.z + storeyElevation`,
|
|
13
|
+
* `viewer.x = ifc.x`, `viewer.z = -ifc.y`.
|
|
14
|
+
* - We emit positions directly in renderer-frame so the mesh slots
|
|
15
|
+
* into the standard mesh-list pipeline.
|
|
16
|
+
*
|
|
17
|
+
* What we don't try to do here: cut openings, host walls, model door
|
|
18
|
+
* leaves correctly, tessellate non-convex polygons. The preview is a
|
|
19
|
+
* faithful extrusion of the user's parametric input, not a final
|
|
20
|
+
* presentation render — when the user exports, the IFC pipeline emits
|
|
21
|
+
* the proper sub-graph and a re-parse yields the canonical geometry.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { MeshData } from '@ifc-lite/geometry';
|
|
25
|
+
import type {
|
|
26
|
+
AddElementType,
|
|
27
|
+
AddElementWallParams,
|
|
28
|
+
AddElementSlabParams,
|
|
29
|
+
AddElementBeamParams,
|
|
30
|
+
AddElementColumnParams,
|
|
31
|
+
AddElementDoorParams,
|
|
32
|
+
AddElementWindowParams,
|
|
33
|
+
AddElementSpaceParams,
|
|
34
|
+
AddElementRoofParams,
|
|
35
|
+
AddElementPlateParams,
|
|
36
|
+
AddElementMemberParams,
|
|
37
|
+
} from './addElementSlice';
|
|
38
|
+
|
|
39
|
+
type Vec3 = [number, number, number];
|
|
40
|
+
|
|
41
|
+
/** Per-type colour palette for the preview mesh. RGBA, 0..1. */
|
|
42
|
+
const COLORS: Record<AddElementType, [number, number, number, number]> = {
|
|
43
|
+
wall: [0.85, 0.85, 0.82, 1.0],
|
|
44
|
+
slab: [0.78, 0.78, 0.78, 1.0],
|
|
45
|
+
beam: [0.65, 0.50, 0.35, 1.0],
|
|
46
|
+
column: [0.65, 0.50, 0.35, 1.0],
|
|
47
|
+
door: [0.55, 0.35, 0.20, 1.0],
|
|
48
|
+
window: [0.45, 0.65, 0.85, 0.45],
|
|
49
|
+
space: [0.30, 0.85, 0.55, 0.18],
|
|
50
|
+
roof: [0.55, 0.35, 0.30, 1.0],
|
|
51
|
+
plate: [0.70, 0.70, 0.72, 1.0],
|
|
52
|
+
member: [0.55, 0.55, 0.50, 1.0],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const IFC_TYPE: Record<AddElementType, string> = {
|
|
56
|
+
wall: 'IfcWall',
|
|
57
|
+
slab: 'IfcSlab',
|
|
58
|
+
beam: 'IfcBeam',
|
|
59
|
+
column: 'IfcColumn',
|
|
60
|
+
door: 'IfcDoor',
|
|
61
|
+
window: 'IfcWindow',
|
|
62
|
+
space: 'IfcSpace',
|
|
63
|
+
roof: 'IfcRoof',
|
|
64
|
+
plate: 'IfcPlate',
|
|
65
|
+
member: 'IfcMember',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface ElementBuildContext {
|
|
69
|
+
type: AddElementType;
|
|
70
|
+
/** New entity's globalId (federation-aware). Tags every vertex. */
|
|
71
|
+
globalId: number;
|
|
72
|
+
/** Storey elevation in IFC Z (metres) — added to vertex Y in renderer. */
|
|
73
|
+
storeyElevation: number;
|
|
74
|
+
/** Per-element-type discriminated params + click points. */
|
|
75
|
+
payload: ElementMeshPayload;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ElementMeshPayload =
|
|
79
|
+
| { type: 'wall'; params: AddElementWallParams; start: Vec3; end: Vec3 }
|
|
80
|
+
| { type: 'beam'; params: AddElementBeamParams; start: Vec3; end: Vec3 }
|
|
81
|
+
| { type: 'member'; params: AddElementMemberParams; start: Vec3; end: Vec3 }
|
|
82
|
+
| { type: 'column'; params: AddElementColumnParams; position: Vec3 }
|
|
83
|
+
| { type: 'door'; params: AddElementDoorParams; position: Vec3 }
|
|
84
|
+
| { type: 'window'; params: AddElementWindowParams; position: Vec3 }
|
|
85
|
+
| { type: 'slab'; params: AddElementSlabParams; corners: Vec3[] }
|
|
86
|
+
| { type: 'space'; params: AddElementSpaceParams; corners: Vec3[] }
|
|
87
|
+
| { type: 'roof'; params: AddElementRoofParams; corners: Vec3[] }
|
|
88
|
+
| { type: 'plate'; params: AddElementPlateParams; corners: Vec3[] };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build a renderer-frame `MeshData` for a freshly-added element.
|
|
92
|
+
* Returns `null` when the payload is degenerate (zero-length wall etc.).
|
|
93
|
+
*/
|
|
94
|
+
export function buildElementMesh(ctx: ElementBuildContext): MeshData | null {
|
|
95
|
+
const { type, globalId, storeyElevation, payload } = ctx;
|
|
96
|
+
switch (payload.type) {
|
|
97
|
+
case 'wall':
|
|
98
|
+
case 'beam':
|
|
99
|
+
case 'member': {
|
|
100
|
+
// Linear extrusion: a (length × thickness × height) box centred
|
|
101
|
+
// along the click→click axis, sitting on the storey floor.
|
|
102
|
+
const thickness = 'Thickness' in payload.params
|
|
103
|
+
? payload.params.Thickness
|
|
104
|
+
: payload.params.Width;
|
|
105
|
+
const height = 'Height' in payload.params ? payload.params.Height : 0.1;
|
|
106
|
+
return buildLinearBox(globalId, type, payload.start, payload.end, thickness, height, storeyElevation);
|
|
107
|
+
}
|
|
108
|
+
case 'column': {
|
|
109
|
+
const { Width, Depth, Height } = payload.params;
|
|
110
|
+
return buildAxisBox(globalId, type, payload.position, Width, Depth, Height, storeyElevation);
|
|
111
|
+
}
|
|
112
|
+
case 'door': {
|
|
113
|
+
const { Width, Height, FrameThickness } = payload.params;
|
|
114
|
+
return buildAxisBox(globalId, type, payload.position, Width, FrameThickness, Height, storeyElevation);
|
|
115
|
+
}
|
|
116
|
+
case 'window': {
|
|
117
|
+
const { Width, Height, FrameThickness } = payload.params;
|
|
118
|
+
return buildAxisBox(globalId, type, payload.position, Width, FrameThickness, Height, storeyElevation);
|
|
119
|
+
}
|
|
120
|
+
case 'slab':
|
|
121
|
+
case 'roof':
|
|
122
|
+
case 'plate': {
|
|
123
|
+
const thickness = payload.params.Thickness;
|
|
124
|
+
return buildPolygonExtrusion(globalId, type, payload.corners, thickness, storeyElevation, /* extrudeUp */ true);
|
|
125
|
+
}
|
|
126
|
+
case 'space': {
|
|
127
|
+
const height = payload.params.Height;
|
|
128
|
+
return buildPolygonExtrusion(globalId, type, payload.corners, height, storeyElevation, /* extrudeUp */ true);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Linear segment extruded into a thickness × height box (wall / beam /
|
|
135
|
+
* member shape). The bottom ring follows the segment's actual start/end
|
|
136
|
+
* Z so a sloped beam previews as a sloped prism instead of pinning to
|
|
137
|
+
* `startIfc[2]`. Walls reject sloped axes upstream
|
|
138
|
+
* (`addWallToStore` enforces planar XY); beams / members do not, which
|
|
139
|
+
* is why this routine has to honour both endpoints' Z.
|
|
140
|
+
*/
|
|
141
|
+
function buildLinearBox(
|
|
142
|
+
globalId: number,
|
|
143
|
+
type: AddElementType,
|
|
144
|
+
startIfc: Vec3,
|
|
145
|
+
endIfc: Vec3,
|
|
146
|
+
thickness: number,
|
|
147
|
+
height: number,
|
|
148
|
+
storeyElevation: number,
|
|
149
|
+
): MeshData | null {
|
|
150
|
+
const dx = endIfc[0] - startIfc[0];
|
|
151
|
+
const dy = endIfc[1] - startIfc[1];
|
|
152
|
+
// Cross-section plane is perpendicular to the ground-plane axis;
|
|
153
|
+
// even on sloped segments we keep the cross-section vertical so
|
|
154
|
+
// walls/beams/members read like building elements (extrusion is
|
|
155
|
+
// along +Z, not perpendicular to the segment direction).
|
|
156
|
+
const lenXY = Math.hypot(dx, dy);
|
|
157
|
+
if (lenXY < 1e-6) return null;
|
|
158
|
+
const ax = dx / lenXY;
|
|
159
|
+
const ay = dy / lenXY;
|
|
160
|
+
const nx = -ay;
|
|
161
|
+
const ny = ax;
|
|
162
|
+
const half = thickness / 2;
|
|
163
|
+
const startBaseZ = startIfc[2];
|
|
164
|
+
const endBaseZ = endIfc[2];
|
|
165
|
+
const startTopZ = startBaseZ + height;
|
|
166
|
+
const endTopZ = endBaseZ + height;
|
|
167
|
+
const ifcCorners: Vec3[] = [
|
|
168
|
+
[startIfc[0] + nx * half, startIfc[1] + ny * half, startBaseZ],
|
|
169
|
+
[endIfc[0] + nx * half, endIfc[1] + ny * half, endBaseZ],
|
|
170
|
+
[endIfc[0] - nx * half, endIfc[1] - ny * half, endBaseZ],
|
|
171
|
+
[startIfc[0] - nx * half, startIfc[1] - ny * half, startBaseZ],
|
|
172
|
+
[startIfc[0] + nx * half, startIfc[1] + ny * half, startTopZ],
|
|
173
|
+
[endIfc[0] + nx * half, endIfc[1] + ny * half, endTopZ],
|
|
174
|
+
[endIfc[0] - nx * half, endIfc[1] - ny * half, endTopZ],
|
|
175
|
+
[startIfc[0] - nx * half, startIfc[1] - ny * half, startTopZ],
|
|
176
|
+
];
|
|
177
|
+
return buildBoxFromIfcCorners(globalId, type, ifcCorners, storeyElevation);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Axis-aligned box centred on a single point (column / door / window shape). */
|
|
181
|
+
function buildAxisBox(
|
|
182
|
+
globalId: number,
|
|
183
|
+
type: AddElementType,
|
|
184
|
+
centerIfc: Vec3,
|
|
185
|
+
sizeX: number,
|
|
186
|
+
sizeY: number,
|
|
187
|
+
sizeZ: number,
|
|
188
|
+
storeyElevation: number,
|
|
189
|
+
): MeshData {
|
|
190
|
+
const hx = sizeX / 2;
|
|
191
|
+
const hy = sizeY / 2;
|
|
192
|
+
const baseZ = centerIfc[2];
|
|
193
|
+
const topZ = baseZ + sizeZ;
|
|
194
|
+
const cx = centerIfc[0];
|
|
195
|
+
const cy = centerIfc[1];
|
|
196
|
+
const ifcCorners: Vec3[] = [
|
|
197
|
+
[cx - hx, cy - hy, baseZ],
|
|
198
|
+
[cx + hx, cy - hy, baseZ],
|
|
199
|
+
[cx + hx, cy + hy, baseZ],
|
|
200
|
+
[cx - hx, cy + hy, baseZ],
|
|
201
|
+
[cx - hx, cy - hy, topZ],
|
|
202
|
+
[cx + hx, cy - hy, topZ],
|
|
203
|
+
[cx + hx, cy + hy, topZ],
|
|
204
|
+
[cx - hx, cy + hy, topZ],
|
|
205
|
+
];
|
|
206
|
+
return buildBoxFromIfcCorners(globalId, type, ifcCorners, storeyElevation);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Polygon footprint extruded vertically (slab / space / roof / plate). */
|
|
210
|
+
function buildPolygonExtrusion(
|
|
211
|
+
globalId: number,
|
|
212
|
+
type: AddElementType,
|
|
213
|
+
ifcFootprint: Vec3[],
|
|
214
|
+
thickness: number,
|
|
215
|
+
storeyElevation: number,
|
|
216
|
+
extrudeUp: boolean,
|
|
217
|
+
): MeshData | null {
|
|
218
|
+
const n = ifcFootprint.length;
|
|
219
|
+
if (n < 3 || thickness <= 0) return null;
|
|
220
|
+
const baseZ = ifcFootprint[0][2];
|
|
221
|
+
const topZ = baseZ + (extrudeUp ? thickness : -thickness);
|
|
222
|
+
|
|
223
|
+
// Vertex layout: bottom ring [0..n-1], top ring [n..2n-1]. Side
|
|
224
|
+
// quads built from consecutive ring pairs. Cap fans triangulate
|
|
225
|
+
// both rings around vertex 0 — fine for convex profiles, tolerable
|
|
226
|
+
// for slightly concave (the export still emits the proper polygon).
|
|
227
|
+
const vertCount = 2 * n;
|
|
228
|
+
const positions = new Float32Array(vertCount * 3);
|
|
229
|
+
const normals = new Float32Array(vertCount * 3);
|
|
230
|
+
for (let i = 0; i < n; i++) {
|
|
231
|
+
const [ix, iy] = ifcFootprint[i];
|
|
232
|
+
// Bottom ring (renderer-frame)
|
|
233
|
+
positions[i * 3 + 0] = ix;
|
|
234
|
+
positions[i * 3 + 1] = baseZ + storeyElevation;
|
|
235
|
+
positions[i * 3 + 2] = -iy;
|
|
236
|
+
normals[i * 3 + 0] = 0;
|
|
237
|
+
normals[i * 3 + 1] = -1;
|
|
238
|
+
normals[i * 3 + 2] = 0;
|
|
239
|
+
// Top ring
|
|
240
|
+
const j = (n + i) * 3;
|
|
241
|
+
positions[j + 0] = ix;
|
|
242
|
+
positions[j + 1] = topZ + storeyElevation;
|
|
243
|
+
positions[j + 2] = -iy;
|
|
244
|
+
normals[j + 0] = 0;
|
|
245
|
+
normals[j + 1] = 1;
|
|
246
|
+
normals[j + 2] = 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Triangle counts: 2(n-2) for caps + 2n for sides = 4n - 4 triangles.
|
|
250
|
+
const triCount = 4 * n - 4;
|
|
251
|
+
const indices = new Uint32Array(triCount * 3);
|
|
252
|
+
let k = 0;
|
|
253
|
+
|
|
254
|
+
// Bottom cap — fan around vertex 0 (CW so it faces -Y / down).
|
|
255
|
+
for (let i = 1; i < n - 1; i++) {
|
|
256
|
+
indices[k++] = 0;
|
|
257
|
+
indices[k++] = i + 1;
|
|
258
|
+
indices[k++] = i;
|
|
259
|
+
}
|
|
260
|
+
// Top cap — fan around vertex n (CCW so it faces +Y / up).
|
|
261
|
+
for (let i = 1; i < n - 1; i++) {
|
|
262
|
+
indices[k++] = n;
|
|
263
|
+
indices[k++] = n + i;
|
|
264
|
+
indices[k++] = n + i + 1;
|
|
265
|
+
}
|
|
266
|
+
// Side quads — two triangles per edge.
|
|
267
|
+
for (let i = 0; i < n; i++) {
|
|
268
|
+
const i0 = i;
|
|
269
|
+
const i1 = (i + 1) % n;
|
|
270
|
+
const t0 = n + i0;
|
|
271
|
+
const t1 = n + i1;
|
|
272
|
+
indices[k++] = i0;
|
|
273
|
+
indices[k++] = i1;
|
|
274
|
+
indices[k++] = t1;
|
|
275
|
+
indices[k++] = i0;
|
|
276
|
+
indices[k++] = t1;
|
|
277
|
+
indices[k++] = t0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const entityIds = new Uint32Array(vertCount);
|
|
281
|
+
entityIds.fill(globalId);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
expressId: globalId,
|
|
285
|
+
ifcType: IFC_TYPE[type],
|
|
286
|
+
positions,
|
|
287
|
+
normals,
|
|
288
|
+
indices,
|
|
289
|
+
color: COLORS[type],
|
|
290
|
+
entityIds,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Box mesh from 8 IFC-frame corners (bottom ring 0..3, top ring 4..7).
|
|
296
|
+
* Emits 12 triangles with face-aligned normals.
|
|
297
|
+
*/
|
|
298
|
+
function buildBoxFromIfcCorners(
|
|
299
|
+
globalId: number,
|
|
300
|
+
type: AddElementType,
|
|
301
|
+
ifcCorners: Vec3[],
|
|
302
|
+
storeyElevation: number,
|
|
303
|
+
): MeshData {
|
|
304
|
+
// Each face has 4 unique vertices (normal welded per face) → 24 verts.
|
|
305
|
+
// Faces: bottom, top, +U, +V, -U, -V (where U/V are the two sides).
|
|
306
|
+
const faces: Array<{ corners: number[]; normal: Vec3 }> = [
|
|
307
|
+
{ corners: [0, 1, 2, 3], normal: [0, 0, -1] }, // bottom (IFC -Z)
|
|
308
|
+
{ corners: [4, 7, 6, 5], normal: [0, 0, 1] }, // top (IFC +Z)
|
|
309
|
+
{ corners: [0, 4, 5, 1], normal: faceNormal(ifcCorners, 0, 4, 1) },
|
|
310
|
+
{ corners: [1, 5, 6, 2], normal: faceNormal(ifcCorners, 1, 5, 2) },
|
|
311
|
+
{ corners: [2, 6, 7, 3], normal: faceNormal(ifcCorners, 2, 6, 3) },
|
|
312
|
+
{ corners: [3, 7, 4, 0], normal: faceNormal(ifcCorners, 3, 7, 0) },
|
|
313
|
+
];
|
|
314
|
+
const positions = new Float32Array(24 * 3);
|
|
315
|
+
const normals = new Float32Array(24 * 3);
|
|
316
|
+
const indices = new Uint32Array(36);
|
|
317
|
+
let v = 0;
|
|
318
|
+
let i = 0;
|
|
319
|
+
for (const face of faces) {
|
|
320
|
+
const baseV = v;
|
|
321
|
+
for (const ci of face.corners) {
|
|
322
|
+
const [ix, iy, iz] = ifcCorners[ci];
|
|
323
|
+
positions[v * 3 + 0] = ix;
|
|
324
|
+
positions[v * 3 + 1] = iz + storeyElevation;
|
|
325
|
+
positions[v * 3 + 2] = -iy;
|
|
326
|
+
// IFC-frame normal → renderer-frame.
|
|
327
|
+
normals[v * 3 + 0] = face.normal[0];
|
|
328
|
+
normals[v * 3 + 1] = face.normal[2];
|
|
329
|
+
normals[v * 3 + 2] = -face.normal[1];
|
|
330
|
+
v++;
|
|
331
|
+
}
|
|
332
|
+
// Two triangles per face — split as (0,1,2) + (0,2,3).
|
|
333
|
+
indices[i++] = baseV;
|
|
334
|
+
indices[i++] = baseV + 1;
|
|
335
|
+
indices[i++] = baseV + 2;
|
|
336
|
+
indices[i++] = baseV;
|
|
337
|
+
indices[i++] = baseV + 2;
|
|
338
|
+
indices[i++] = baseV + 3;
|
|
339
|
+
}
|
|
340
|
+
const entityIds = new Uint32Array(24);
|
|
341
|
+
entityIds.fill(globalId);
|
|
342
|
+
return {
|
|
343
|
+
expressId: globalId,
|
|
344
|
+
ifcType: IFC_TYPE[type],
|
|
345
|
+
positions,
|
|
346
|
+
normals,
|
|
347
|
+
indices,
|
|
348
|
+
color: COLORS[type],
|
|
349
|
+
entityIds,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function faceNormal(corners: Vec3[], a: number, b: number, c: number): Vec3 {
|
|
354
|
+
const ux = corners[b][0] - corners[a][0];
|
|
355
|
+
const uy = corners[b][1] - corners[a][1];
|
|
356
|
+
const uz = corners[b][2] - corners[a][2];
|
|
357
|
+
const vx = corners[c][0] - corners[a][0];
|
|
358
|
+
const vy = corners[c][1] - corners[a][1];
|
|
359
|
+
const vz = corners[c][2] - corners[a][2];
|
|
360
|
+
const nx = uy * vz - uz * vy;
|
|
361
|
+
const ny = uz * vx - ux * vz;
|
|
362
|
+
const nz = ux * vy - uy * vx;
|
|
363
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
364
|
+
return [nx / len, ny / len, nz / len];
|
|
365
|
+
}
|