@ifc-lite/viewer 1.17.4 → 1.17.6

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.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +16 -16
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +117 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -1
  5. package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-86rgogji.js} +1 -1
  6. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  7. package/dist/assets/{exporters-ChAtBmlj.js → exporters-CcPS9MK5.js} +2274 -2227
  8. package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-BFUYA08u.js} +1 -1
  9. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  10. package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
  11. package/dist/assets/{index-Co8E2-FE.js → index-Bfms9I4A.js} +35160 -33084
  12. package/dist/assets/index-_bfZsDCC.css +1 -0
  13. package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-DUyLCMZS.js} +104 -104
  14. package/dist/assets/{sandbox-DZiNLNMk.js → sandbox-C8575tul.js} +4340 -4322
  15. package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-BuZK7OST.js} +1 -1
  16. package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-JsqEGDV8.js} +1 -1
  17. package/dist/index.html +8 -7
  18. package/index.html +1 -0
  19. package/package.json +7 -7
  20. package/src/App.tsx +16 -2
  21. package/src/components/viewer/CesiumOverlay.tsx +62 -19
  22. package/src/components/viewer/ChatPanel.tsx +195 -91
  23. package/src/components/viewer/MainToolbar.tsx +4 -3
  24. package/src/components/viewer/PropertiesPanel.tsx +16 -2
  25. package/src/components/viewer/SettingsPage.tsx +252 -101
  26. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  27. package/src/components/viewer/ViewerLayout.tsx +1 -0
  28. package/src/components/viewer/Viewport.tsx +14 -2
  29. package/src/components/viewer/ViewportContainer.tsx +49 -64
  30. package/src/components/viewer/ViewportOverlays.tsx +5 -2
  31. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  32. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  33. package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
  34. package/src/components/viewer/properties/LocationMap.tsx +9 -7
  35. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  36. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  37. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  38. package/src/components/viewer/useAnimationLoop.ts +9 -1
  39. package/src/components/viewer/useRenderUpdates.ts +1 -1
  40. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  41. package/src/hooks/ingest/viewerModelIngest.ts +7 -2
  42. package/src/hooks/useIfcFederation.ts +326 -71
  43. package/src/hooks/useIfcLoader.ts +1 -0
  44. package/src/hooks/useViewControls.ts +13 -5
  45. package/src/index.css +484 -10
  46. package/src/lib/desktop-entitlement.ts +2 -4
  47. package/src/lib/geo/cesium-bridge.ts +15 -7
  48. package/src/lib/geo/effective-georef.test.ts +73 -0
  49. package/src/lib/geo/effective-georef.ts +111 -0
  50. package/src/lib/geo/reproject.ts +105 -19
  51. package/src/lib/llm/byok-guard.test.ts +77 -0
  52. package/src/lib/llm/byok-guard.ts +39 -0
  53. package/src/lib/llm/free-models.test.ts +0 -6
  54. package/src/lib/llm/models.ts +104 -42
  55. package/src/lib/llm/stream-client.ts +74 -110
  56. package/src/lib/llm/stream-direct.test.ts +130 -0
  57. package/src/lib/llm/stream-direct.ts +316 -0
  58. package/src/lib/llm/types.ts +14 -2
  59. package/src/main.tsx +1 -10
  60. package/src/services/api-keys.ts +73 -0
  61. package/src/store/constants.ts +20 -2
  62. package/src/store/index.ts +12 -5
  63. package/src/store/slices/cesiumSlice.ts +5 -0
  64. package/src/store/slices/chatSlice.test.ts +6 -76
  65. package/src/store/slices/chatSlice.ts +17 -58
  66. package/src/store/slices/sectionSlice.test.ts +87 -7
  67. package/src/store/slices/sectionSlice.ts +151 -5
  68. package/src/store/slices/uiSlice.ts +28 -5
  69. package/src/store/types.ts +26 -0
  70. package/src/utils/nativeSpatialDataStore.ts +4 -1
  71. package/src/utils/viewportUtils.ts +7 -2
  72. package/src/vite-env.d.ts +0 -4
  73. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  74. package/dist/assets/ids-B4jTqB1O.js +0 -1
  75. package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
  76. package/dist/assets/index-DckuDqlv.css +0 -1
  77. package/src/components/viewer/UpgradePage.tsx +0 -71
  78. package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
  79. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  80. package/src/lib/llm/clerk-auth.ts +0 -62
@@ -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-64">
88
+ <div className="border-t px-3 pb-3 min-w-72">
87
89
  {/* Direction Selection */}
88
90
  <div className="mt-3">
89
- <label className="text-xs text-muted-foreground mb-2 block">Direction</label>
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
- <label className="text-xs text-muted-foreground">Position</label>
109
- <input
110
- type="number"
111
- min="0"
112
- max="100"
113
- step="0.1"
114
- value={sectionPlane.position}
115
- onChange={handlePositionChange}
116
- 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"
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
- ? `Cutting ${AXIS_INFO[sectionPlane.axis].label.toLowerCase()} at ${sectionPlane.position.toFixed(1)}%`
160
- : 'Preview mode'}
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 - brutalist style matching Measure tool */}
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="Toggle section plane"
194
+ title={sectionPlane.enabled ? 'Click to disable the cut' : 'Click to enable the cut'}
174
195
  >
175
- {sectionPlane.enabled ? 'Cutting' : 'Preview'}
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
- ...sectionPlaneRef.current,
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,
@@ -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 type { IfcDataStore } from '@ifc-lite/parser';
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
- return dataStore.entities?.getObjectType?.(expressId);
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
- // Use getForEntity (returns PropertySet[])
125
- const psets = propertiesStore.getForEntity?.(expressId);
126
- if (!psets) return [];
127
-
128
- return psets.map((pset) => ({
129
- name: pset.name,
130
- properties: (pset.properties || []).map((prop) => {
131
- // Convert value: ensure it's a primitive type (not array)
132
- let value: string | number | boolean | null = null;
133
- if (Array.isArray(prop.value)) {
134
- value = JSON.stringify(prop.value);
135
- } else {
136
- value = prop.value as string | number | boolean | null;
137
- }
138
- return {
139
- name: prop.name,
140
- value,
141
- dataType: String(prop.type || 'IFCLABEL'),
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 | { initial: number; subsequent: 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;