@ifc-lite/viewer 1.11.5 → 1.14.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 +91 -0
- package/dist/assets/{Arrow.dom-HFGUoQyp.js → Arrow.dom-CNguvlQi.js} +1 -1
- package/dist/assets/{browser-CllJKxsx.js → browser-D6lgLpkA.js} +1 -1
- package/dist/assets/{index-CQd80vMv.js → index-BMwpw264.js} +4 -4
- package/dist/assets/index-Qp8stcGO.css +1 -0
- package/dist/assets/{index-B69WAU-m.js → index-UaDsJsCR.js} +26434 -23023
- package/dist/assets/{native-bridge-Bu4SptAa.js → native-bridge-DqELq4X0.js} +1 -1
- package/dist/assets/{wasm-bridge-CR2KvcQN.js → wasm-bridge-CVWvHlfH.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +19 -19
- package/src/App.tsx +2 -0
- package/src/components/ui/toast.tsx +121 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +8 -1
- package/src/components/viewer/DataConnector.tsx +8 -1
- package/src/components/viewer/ExportChangesButton.tsx +11 -2
- package/src/components/viewer/ExportDialog.tsx +224 -132
- package/src/components/viewer/MainToolbar.tsx +9 -2
- package/src/components/viewer/PropertiesPanel.tsx +300 -15
- package/src/components/viewer/properties/BsddCard.tsx +507 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +1 -0
- package/src/components/viewer/useGeometryStreaming.ts +4 -4
- package/src/index.css +7 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +33 -0
- package/src/lib/scripts/templates/create-building.ts +491 -0
- package/src/lib/scripts/templates.ts +8 -0
- package/src/sdk/adapters/export-adapter.ts +84 -0
- package/src/sdk/adapters/lens-adapter.ts +1 -1
- package/src/sdk/adapters/model-adapter.ts +8 -0
- package/src/sdk/adapters/viewer-adapter.ts +1 -1
- package/src/services/bsdd.ts +262 -0
- package/src/store/index.ts +2 -2
- package/src/store/slices/measurementSlice.test.ts +22 -22
- package/src/store/slices/modelSlice.test.ts +2 -0
- package/src/store/slices/mutationSlice.ts +155 -1
- package/vite.config.ts +7 -0
- package/dist/assets/index-BoYyWYAu.css +0 -1
|
@@ -0,0 +1,507 @@
|
|
|
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
|
+
* bSDD (buildingSMART Data Dictionary) integration card.
|
|
7
|
+
*
|
|
8
|
+
* Shows schema-defined property sets and properties for the selected
|
|
9
|
+
* IFC entity type, fetched live from the bSDD API. Users can add
|
|
10
|
+
* properties to the element in one click.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
14
|
+
import { BookOpen, Plus, Check, Loader2, ExternalLink, ChevronDown, ChevronRight } from 'lucide-react';
|
|
15
|
+
import { Button } from '@/components/ui/button';
|
|
16
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
17
|
+
import { Badge } from '@/components/ui/badge';
|
|
18
|
+
import { useViewerStore } from '@/store';
|
|
19
|
+
import { PropertyValueType, QuantityType } from '@ifc-lite/data';
|
|
20
|
+
import {
|
|
21
|
+
fetchClassInfo,
|
|
22
|
+
bsddDataTypeLabel,
|
|
23
|
+
type BsddClassInfo,
|
|
24
|
+
type BsddClassProperty,
|
|
25
|
+
} from '@/services/bsdd';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers for Qto_* (quantity set) detection and mapping
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Returns true when the property set name denotes a quantity set */
|
|
32
|
+
function isQuantitySet(psetName: string): boolean {
|
|
33
|
+
return psetName.startsWith('Qto_');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Infer QuantityType from bSDD unit strings */
|
|
37
|
+
function inferQuantityType(units: string[] | null): QuantityType {
|
|
38
|
+
if (!units || units.length === 0) return QuantityType.Count;
|
|
39
|
+
const u = units[0].toLowerCase();
|
|
40
|
+
if (u === 'm' || u === 'mm' || u === 'cm') return QuantityType.Length;
|
|
41
|
+
if (u.includes('m²') || u.includes('m2')) return QuantityType.Area;
|
|
42
|
+
if (u.includes('m³') || u.includes('m3')) return QuantityType.Volume;
|
|
43
|
+
if (u === 'kg' || u === 'g' || u === 't') return QuantityType.Weight;
|
|
44
|
+
if (u === 's' || u === 'h' || u === 'min') return QuantityType.Time;
|
|
45
|
+
return QuantityType.Count;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// bSDD data type → PropertyValueType mapping
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function toPropertyValueType(bsddType: string | null): PropertyValueType {
|
|
53
|
+
if (!bsddType) return PropertyValueType.String;
|
|
54
|
+
const lower = bsddType.toLowerCase();
|
|
55
|
+
if (lower === 'boolean') return PropertyValueType.Boolean;
|
|
56
|
+
if (lower === 'real' || lower === 'number') return PropertyValueType.Real;
|
|
57
|
+
if (lower === 'integer') return PropertyValueType.Integer;
|
|
58
|
+
if (lower === 'character' || lower === 'string') return PropertyValueType.String;
|
|
59
|
+
return PropertyValueType.Label;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function defaultValue(_bsddType: string | null): unknown {
|
|
63
|
+
// Always return empty string – user fills in values manually
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** bSDD properties with null propertySet are IFC entity-level attributes */
|
|
68
|
+
const BSDD_ATTRIBUTES_GROUP = 'Attributes';
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Component props
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
export interface BsddCardProps {
|
|
75
|
+
/** IFC type name of the selected entity, e.g. "IfcWall" */
|
|
76
|
+
entityType: string;
|
|
77
|
+
/** Model ID for mutations */
|
|
78
|
+
modelId: string;
|
|
79
|
+
/** Express ID of the entity to add properties to */
|
|
80
|
+
entityId: number;
|
|
81
|
+
/** Names of property sets already present on the entity */
|
|
82
|
+
existingPsets: string[];
|
|
83
|
+
/** Names of properties already present on the entity (flat list: "PsetName:PropName") */
|
|
84
|
+
existingProps: Set<string>;
|
|
85
|
+
/** Names of quantity sets already present on the entity */
|
|
86
|
+
existingQsets?: string[];
|
|
87
|
+
/** Names of quantities already present (flat list: "QsetName:QuantName") */
|
|
88
|
+
existingQuants?: Set<string>;
|
|
89
|
+
/** Names of entity-level attributes that already have values */
|
|
90
|
+
existingAttributes?: Set<string>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Main component
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
export function BsddCard({
|
|
98
|
+
entityType,
|
|
99
|
+
modelId,
|
|
100
|
+
entityId,
|
|
101
|
+
existingPsets,
|
|
102
|
+
existingProps,
|
|
103
|
+
existingQsets = [],
|
|
104
|
+
existingQuants = new Set<string>(),
|
|
105
|
+
existingAttributes = new Set<string>(),
|
|
106
|
+
}: BsddCardProps) {
|
|
107
|
+
const [classInfo, setClassInfo] = useState<BsddClassInfo | null>(null);
|
|
108
|
+
const [loading, setLoading] = useState(false);
|
|
109
|
+
const [error, setError] = useState<string | null>(null);
|
|
110
|
+
const [expandedPsets, setExpandedPsets] = useState<Set<string>>(new Set());
|
|
111
|
+
const [addedKeys, setAddedKeys] = useState<Set<string>>(new Set());
|
|
112
|
+
|
|
113
|
+
const setProperty = useViewerStore((s) => s.setProperty);
|
|
114
|
+
const createPropertySet = useViewerStore((s) => s.createPropertySet);
|
|
115
|
+
const setQuantity = useViewerStore((s) => s.setQuantity);
|
|
116
|
+
const createQuantitySet = useViewerStore((s) => s.createQuantitySet);
|
|
117
|
+
const storeSetAttribute = useViewerStore((s) => s.setAttribute);
|
|
118
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
119
|
+
|
|
120
|
+
// Fetch class info from bSDD when entity type changes
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
let cancelled = false;
|
|
123
|
+
setClassInfo(null);
|
|
124
|
+
setError(null);
|
|
125
|
+
setAddedKeys(new Set());
|
|
126
|
+
|
|
127
|
+
if (!entityType) return;
|
|
128
|
+
|
|
129
|
+
setLoading(true);
|
|
130
|
+
fetchClassInfo(entityType).then(
|
|
131
|
+
(info) => {
|
|
132
|
+
if (cancelled) return;
|
|
133
|
+
setLoading(false);
|
|
134
|
+
if (info && info.classProperties.length > 0) {
|
|
135
|
+
setClassInfo(info);
|
|
136
|
+
} else {
|
|
137
|
+
setClassInfo(null);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
(err) => {
|
|
141
|
+
if (cancelled) return;
|
|
142
|
+
setLoading(false);
|
|
143
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch bSDD data');
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
cancelled = true;
|
|
149
|
+
};
|
|
150
|
+
}, [entityType]);
|
|
151
|
+
|
|
152
|
+
// Group properties by property set name
|
|
153
|
+
const groupedProps = useMemo(() => {
|
|
154
|
+
if (!classInfo) return new Map<string, BsddClassProperty[]>();
|
|
155
|
+
const map = new Map<string, BsddClassProperty[]>();
|
|
156
|
+
for (const prop of classInfo.classProperties) {
|
|
157
|
+
// Null propertySet → IFC entity attributes (Name, Description, etc.)
|
|
158
|
+
const psetName = prop.propertySet || BSDD_ATTRIBUTES_GROUP;
|
|
159
|
+
let list = map.get(psetName);
|
|
160
|
+
if (!list) {
|
|
161
|
+
list = [];
|
|
162
|
+
map.set(psetName, list);
|
|
163
|
+
}
|
|
164
|
+
list.push(prop);
|
|
165
|
+
}
|
|
166
|
+
return map;
|
|
167
|
+
}, [classInfo]);
|
|
168
|
+
|
|
169
|
+
const togglePset = useCallback((name: string) => {
|
|
170
|
+
setExpandedPsets((prev) => {
|
|
171
|
+
const next = new Set(prev);
|
|
172
|
+
if (next.has(name)) next.delete(name);
|
|
173
|
+
else next.add(name);
|
|
174
|
+
return next;
|
|
175
|
+
});
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const handleAddProperty = useCallback(
|
|
179
|
+
(psetName: string, prop: BsddClassProperty) => {
|
|
180
|
+
let normalizedModelId = modelId;
|
|
181
|
+
if (modelId === 'legacy') normalizedModelId = '__legacy__';
|
|
182
|
+
|
|
183
|
+
if (psetName === BSDD_ATTRIBUTES_GROUP) {
|
|
184
|
+
// Route entity-level attributes (Name, Description, ObjectType, Tag, etc.)
|
|
185
|
+
storeSetAttribute(normalizedModelId, entityId, prop.name, '');
|
|
186
|
+
} else if (isQuantitySet(psetName)) {
|
|
187
|
+
// Route Qto_* through quantity creation
|
|
188
|
+
const qType = inferQuantityType(prop.units);
|
|
189
|
+
const qsetExists = existingQsets.includes(psetName);
|
|
190
|
+
|
|
191
|
+
if (!qsetExists) {
|
|
192
|
+
createQuantitySet(normalizedModelId, entityId, psetName, [
|
|
193
|
+
{ name: prop.name, value: NaN, quantityType: qType, unit: prop.units?.[0] },
|
|
194
|
+
]);
|
|
195
|
+
} else {
|
|
196
|
+
setQuantity(
|
|
197
|
+
normalizedModelId,
|
|
198
|
+
entityId,
|
|
199
|
+
psetName,
|
|
200
|
+
prop.name,
|
|
201
|
+
NaN,
|
|
202
|
+
qType,
|
|
203
|
+
prop.units?.[0],
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Route Pset_* / other through property creation
|
|
208
|
+
const valueType = toPropertyValueType(prop.dataType);
|
|
209
|
+
const value = defaultValue(prop.dataType);
|
|
210
|
+
const psetExists = existingPsets.includes(psetName);
|
|
211
|
+
|
|
212
|
+
if (!psetExists) {
|
|
213
|
+
createPropertySet(normalizedModelId, entityId, psetName, [
|
|
214
|
+
{ name: prop.name, value, type: valueType },
|
|
215
|
+
]);
|
|
216
|
+
} else {
|
|
217
|
+
setProperty(
|
|
218
|
+
normalizedModelId,
|
|
219
|
+
entityId,
|
|
220
|
+
psetName,
|
|
221
|
+
prop.name,
|
|
222
|
+
value,
|
|
223
|
+
valueType,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
bumpMutationVersion();
|
|
229
|
+
setAddedKeys((prev) => new Set(prev).add(`${psetName}:${prop.name}`));
|
|
230
|
+
},
|
|
231
|
+
[modelId, entityId, existingPsets, existingQsets, setProperty, createPropertySet, setQuantity, createQuantitySet, storeSetAttribute, bumpMutationVersion],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const handleAddAllInPset = useCallback(
|
|
235
|
+
(psetName: string, props: BsddClassProperty[]) => {
|
|
236
|
+
let normalizedModelId = modelId;
|
|
237
|
+
if (modelId === 'legacy') normalizedModelId = '__legacy__';
|
|
238
|
+
|
|
239
|
+
const isAttrGroup = psetName === BSDD_ATTRIBUTES_GROUP;
|
|
240
|
+
|
|
241
|
+
// Determine which "existing" set to check against
|
|
242
|
+
const existingSet = isAttrGroup
|
|
243
|
+
? existingAttributes
|
|
244
|
+
: isQuantitySet(psetName)
|
|
245
|
+
? existingQuants
|
|
246
|
+
: existingProps;
|
|
247
|
+
|
|
248
|
+
// For attributes, key is just the name; for props/quants, key is "PsetName:PropName"
|
|
249
|
+
const toAdd = props.filter(
|
|
250
|
+
(p) => {
|
|
251
|
+
const key = isAttrGroup ? p.name : `${psetName}:${p.name}`;
|
|
252
|
+
const addedKey = `${psetName}:${p.name}`;
|
|
253
|
+
return !existingSet.has(key) && !addedKeys.has(addedKey);
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
if (toAdd.length === 0) return;
|
|
257
|
+
|
|
258
|
+
if (isAttrGroup) {
|
|
259
|
+
// Route entity-level attributes
|
|
260
|
+
for (const p of toAdd) {
|
|
261
|
+
storeSetAttribute(normalizedModelId, entityId, p.name, '');
|
|
262
|
+
}
|
|
263
|
+
} else if (isQuantitySet(psetName)) {
|
|
264
|
+
// Route Qto_* through quantity creation
|
|
265
|
+
const qsetExists = existingQsets.includes(psetName);
|
|
266
|
+
|
|
267
|
+
if (!qsetExists) {
|
|
268
|
+
createQuantitySet(
|
|
269
|
+
normalizedModelId,
|
|
270
|
+
entityId,
|
|
271
|
+
psetName,
|
|
272
|
+
toAdd.map((p) => ({
|
|
273
|
+
name: p.name,
|
|
274
|
+
value: NaN,
|
|
275
|
+
quantityType: inferQuantityType(p.units),
|
|
276
|
+
unit: p.units?.[0],
|
|
277
|
+
})),
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
for (const p of toAdd) {
|
|
281
|
+
setQuantity(
|
|
282
|
+
normalizedModelId,
|
|
283
|
+
entityId,
|
|
284
|
+
psetName,
|
|
285
|
+
p.name,
|
|
286
|
+
NaN,
|
|
287
|
+
inferQuantityType(p.units),
|
|
288
|
+
p.units?.[0],
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
const psetExists = existingPsets.includes(psetName);
|
|
294
|
+
|
|
295
|
+
if (!psetExists) {
|
|
296
|
+
createPropertySet(
|
|
297
|
+
normalizedModelId,
|
|
298
|
+
entityId,
|
|
299
|
+
psetName,
|
|
300
|
+
toAdd.map((p) => ({
|
|
301
|
+
name: p.name,
|
|
302
|
+
value: defaultValue(p.dataType),
|
|
303
|
+
type: toPropertyValueType(p.dataType),
|
|
304
|
+
})),
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
for (const p of toAdd) {
|
|
308
|
+
setProperty(
|
|
309
|
+
normalizedModelId,
|
|
310
|
+
entityId,
|
|
311
|
+
psetName,
|
|
312
|
+
p.name,
|
|
313
|
+
defaultValue(p.dataType),
|
|
314
|
+
toPropertyValueType(p.dataType),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
bumpMutationVersion();
|
|
321
|
+
setAddedKeys((prev) => {
|
|
322
|
+
const next = new Set(prev);
|
|
323
|
+
for (const p of toAdd) next.add(`${psetName}:${p.name}`);
|
|
324
|
+
return next;
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
[modelId, entityId, existingPsets, existingQsets, existingProps, existingQuants, existingAttributes, addedKeys, setProperty, createPropertySet, setQuantity, createQuantitySet, storeSetAttribute, bumpMutationVersion],
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Loading state
|
|
331
|
+
if (loading) {
|
|
332
|
+
return (
|
|
333
|
+
<div className="flex items-center gap-2 px-3 py-6 text-xs text-zinc-400">
|
|
334
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
335
|
+
<span>Loading bSDD data for {entityType}...</span>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Error state
|
|
341
|
+
if (error) {
|
|
342
|
+
return (
|
|
343
|
+
<div className="px-3 py-4 text-xs text-red-500/70">
|
|
344
|
+
<p>Could not load bSDD data: {error}</p>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// No data
|
|
350
|
+
if (!classInfo || groupedProps.size === 0) {
|
|
351
|
+
return (
|
|
352
|
+
<div className="flex flex-col items-center justify-center text-center px-4 py-8 text-xs text-zinc-400 gap-2">
|
|
353
|
+
<BookOpen className="h-6 w-6 text-zinc-300 dark:text-zinc-600" />
|
|
354
|
+
<p>No bSDD data available for <span className="font-mono font-medium">{entityType}</span></p>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div className="space-y-2 w-full min-w-0 overflow-hidden">
|
|
361
|
+
{/* Header with class description */}
|
|
362
|
+
{classInfo.definition && (
|
|
363
|
+
<div className="px-1 pb-1 text-[11px] text-zinc-500 dark:text-zinc-400 leading-relaxed">
|
|
364
|
+
{classInfo.definition}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* Property sets from bSDD */}
|
|
369
|
+
{Array.from(groupedProps.entries()).map(([psetName, props]) => {
|
|
370
|
+
const isExpanded = expandedPsets.has(psetName);
|
|
371
|
+
const isAttrGroup = psetName === BSDD_ATTRIBUTES_GROUP;
|
|
372
|
+
const isQto = isQuantitySet(psetName);
|
|
373
|
+
// For attributes, check against existingAttributes (keyed by name only);
|
|
374
|
+
// for quants/props, check against existingQuants/existingProps (keyed by "PsetName:PropName")
|
|
375
|
+
const existingSet = isAttrGroup ? existingAttributes : isQto ? existingQuants : existingProps;
|
|
376
|
+
const makeKey = (p: BsddClassProperty) => isAttrGroup ? p.name : `${psetName}:${p.name}`;
|
|
377
|
+
const allAlreadyExist = props.every(
|
|
378
|
+
(p) =>
|
|
379
|
+
existingSet.has(makeKey(p)) ||
|
|
380
|
+
addedKeys.has(`${psetName}:${p.name}`),
|
|
381
|
+
);
|
|
382
|
+
const psetExistsOnEntity = isAttrGroup
|
|
383
|
+
? true // Attributes section always exists on the entity
|
|
384
|
+
: isQto
|
|
385
|
+
? existingQsets.includes(psetName)
|
|
386
|
+
: existingPsets.includes(psetName);
|
|
387
|
+
const addableCount = props.filter(
|
|
388
|
+
(p) =>
|
|
389
|
+
!existingSet.has(makeKey(p)) &&
|
|
390
|
+
!addedKeys.has(`${psetName}:${p.name}`),
|
|
391
|
+
).length;
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div
|
|
395
|
+
key={psetName}
|
|
396
|
+
className="border-2 border-sky-200/60 dark:border-sky-800/40 bg-sky-50/20 dark:bg-sky-950/10 w-full overflow-hidden"
|
|
397
|
+
>
|
|
398
|
+
{/* Pset header */}
|
|
399
|
+
<button
|
|
400
|
+
className="flex items-center gap-1.5 w-full p-2 hover:bg-sky-50 dark:hover:bg-sky-900/20 text-left transition-colors overflow-hidden"
|
|
401
|
+
onClick={() => togglePset(psetName)}
|
|
402
|
+
>
|
|
403
|
+
{isExpanded ? (
|
|
404
|
+
<ChevronDown className="h-3 w-3 text-sky-500 shrink-0" />
|
|
405
|
+
) : (
|
|
406
|
+
<ChevronRight className="h-3 w-3 text-sky-500 shrink-0" />
|
|
407
|
+
)}
|
|
408
|
+
<span className="font-bold text-xs text-sky-800 dark:text-sky-300 truncate flex-1 min-w-0">
|
|
409
|
+
{psetName}
|
|
410
|
+
</span>
|
|
411
|
+
<span className="text-[10px] font-mono bg-sky-100 dark:bg-sky-900/50 px-1 py-0.5 border border-sky-200 dark:border-sky-800 text-sky-600 dark:text-sky-400 shrink-0">
|
|
412
|
+
{props.length}
|
|
413
|
+
</span>
|
|
414
|
+
{addableCount > 0 && (
|
|
415
|
+
<Tooltip>
|
|
416
|
+
<TooltipTrigger asChild>
|
|
417
|
+
<Button
|
|
418
|
+
variant="ghost"
|
|
419
|
+
size="icon"
|
|
420
|
+
className="h-5 w-5 p-0 shrink-0 hover:bg-sky-200 dark:hover:bg-sky-800"
|
|
421
|
+
onClick={(e) => {
|
|
422
|
+
e.stopPropagation();
|
|
423
|
+
handleAddAllInPset(psetName, props);
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
<Plus className="h-3 w-3 text-sky-600 dark:text-sky-400" />
|
|
427
|
+
</Button>
|
|
428
|
+
</TooltipTrigger>
|
|
429
|
+
<TooltipContent>Add all {addableCount} properties</TooltipContent>
|
|
430
|
+
</Tooltip>
|
|
431
|
+
)}
|
|
432
|
+
{allAlreadyExist && (
|
|
433
|
+
<Check className="h-3 w-3 text-emerald-500 shrink-0" />
|
|
434
|
+
)}
|
|
435
|
+
</button>
|
|
436
|
+
|
|
437
|
+
{/* Properties */}
|
|
438
|
+
{isExpanded && (
|
|
439
|
+
<div className="border-t-2 border-sky-200/60 dark:border-sky-800/40 divide-y divide-sky-100 dark:divide-sky-900/30">
|
|
440
|
+
{props.map((prop) => {
|
|
441
|
+
const existKey = makeKey(prop);
|
|
442
|
+
const addedKey = `${psetName}:${prop.name}`;
|
|
443
|
+
const alreadyExists = existingSet.has(existKey) || addedKeys.has(addedKey);
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div
|
|
447
|
+
key={prop.name}
|
|
448
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs overflow-hidden ${
|
|
449
|
+
alreadyExists
|
|
450
|
+
? 'bg-emerald-50/30 dark:bg-emerald-950/10'
|
|
451
|
+
: 'hover:bg-sky-50/50 dark:hover:bg-sky-900/20'
|
|
452
|
+
}`}
|
|
453
|
+
>
|
|
454
|
+
<Tooltip>
|
|
455
|
+
<TooltipTrigger asChild>
|
|
456
|
+
<span className="font-medium text-zinc-600 dark:text-zinc-400 cursor-help truncate flex-1 min-w-0">
|
|
457
|
+
{prop.name}
|
|
458
|
+
</span>
|
|
459
|
+
</TooltipTrigger>
|
|
460
|
+
<TooltipContent side="top" className="max-w-xs text-[10px]">
|
|
461
|
+
<p className="font-medium">{prop.name}</p>
|
|
462
|
+
{prop.description && <p className="mt-0.5 text-zinc-400">{prop.description}</p>}
|
|
463
|
+
{prop.dataType && <p className="mt-0.5 text-sky-400">{bsddDataTypeLabel(prop.dataType)}</p>}
|
|
464
|
+
</TooltipContent>
|
|
465
|
+
</Tooltip>
|
|
466
|
+
{/* Add button - always visible on right */}
|
|
467
|
+
{alreadyExists ? (
|
|
468
|
+
<Check className="h-3.5 w-3.5 text-emerald-500 shrink-0" />
|
|
469
|
+
) : (
|
|
470
|
+
<Tooltip>
|
|
471
|
+
<TooltipTrigger asChild>
|
|
472
|
+
<Button
|
|
473
|
+
variant="ghost"
|
|
474
|
+
size="icon"
|
|
475
|
+
className="h-5 w-5 p-0 shrink-0 hover:bg-sky-200 dark:hover:bg-sky-800"
|
|
476
|
+
onClick={() => handleAddProperty(psetName, prop)}
|
|
477
|
+
>
|
|
478
|
+
<Plus className="h-3 w-3 text-sky-600 dark:text-sky-400" />
|
|
479
|
+
</Button>
|
|
480
|
+
</TooltipTrigger>
|
|
481
|
+
<TooltipContent>Add to element</TooltipContent>
|
|
482
|
+
</Tooltip>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
486
|
+
})}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
})}
|
|
492
|
+
|
|
493
|
+
{/* Footer link */}
|
|
494
|
+
<div className="flex items-center justify-center pt-1 pb-1">
|
|
495
|
+
<a
|
|
496
|
+
href={`https://search.bsdd.buildingsmart.org/uri/buildingsmart/ifc/4.3/class/${entityType}`}
|
|
497
|
+
target="_blank"
|
|
498
|
+
rel="noopener noreferrer"
|
|
499
|
+
className="flex items-center gap-1 text-[10px] text-sky-500/70 hover:text-sky-600 transition-colors"
|
|
500
|
+
>
|
|
501
|
+
<ExternalLink className="h-2.5 w-2.5" />
|
|
502
|
+
View on bSDD
|
|
503
|
+
</a>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
@@ -23,6 +23,7 @@ const QUANTITY_TYPE_NAMES: Record<number, string> = {
|
|
|
23
23
|
|
|
24
24
|
export function QuantitySetCard({ qset }: { qset: QuantitySet }) {
|
|
25
25
|
const formatValue = (value: number, type: number): string => {
|
|
26
|
+
if (isNaN(value)) return '\u2014'; // em-dash for empty values
|
|
26
27
|
const formatted = value.toLocaleString(undefined, { maximumFractionDigits: 3 });
|
|
27
28
|
switch (type) {
|
|
28
29
|
case 0: return `${formatted} m`;
|
|
@@ -362,15 +362,15 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
362
362
|
const renderer = rendererRef.current;
|
|
363
363
|
if (!renderer || !isInitialized) return;
|
|
364
364
|
|
|
365
|
-
// If streaming just completed (was streaming, now not),
|
|
365
|
+
// If streaming just completed (was streaming, now not), finalize and render
|
|
366
366
|
if (prevIsStreamingRef.current && !isStreaming) {
|
|
367
367
|
const device = renderer.getGPUDevice();
|
|
368
368
|
const pipeline = renderer.getPipeline();
|
|
369
369
|
const scene = renderer.getScene();
|
|
370
370
|
|
|
371
|
-
//
|
|
372
|
-
if (device && pipeline
|
|
373
|
-
scene.
|
|
371
|
+
// Finalize streaming: destroy temporary fragments and do one O(N) full merge
|
|
372
|
+
if (device && pipeline) {
|
|
373
|
+
scene.finalizeStreaming(device, pipeline);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
renderer.render();
|
package/src/index.css
CHANGED
|
@@ -316,6 +316,13 @@ body {
|
|
|
316
316
|
background-color: rgba(115, 218, 202, 0.05) !important;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
/* Fix Radix ScrollArea viewport inner div: by default it uses display:table
|
|
320
|
+
which expands to content width, preventing text truncation in flex children.
|
|
321
|
+
Force block layout so width constraints cascade correctly. */
|
|
322
|
+
[data-radix-scroll-area-viewport] > div {
|
|
323
|
+
display: block !important;
|
|
324
|
+
}
|
|
325
|
+
|
|
319
326
|
/* Custom scrollbar */
|
|
320
327
|
.scrollbar-thin {
|
|
321
328
|
scrollbar-width: thin;
|
|
@@ -50,6 +50,8 @@ declare const bim: {
|
|
|
50
50
|
active(): BimModelInfo | null;
|
|
51
51
|
/** Get active model ID */
|
|
52
52
|
activeId(): string | null;
|
|
53
|
+
/** Load IFC content into the 3D viewer for preview */
|
|
54
|
+
loadIfc(content: string, filename?: string): void;
|
|
53
55
|
};
|
|
54
56
|
/** Query entities */
|
|
55
57
|
query: {
|
|
@@ -101,11 +103,42 @@ declare const bim: {
|
|
|
101
103
|
/** Get built-in lens presets */
|
|
102
104
|
presets(): unknown[];
|
|
103
105
|
};
|
|
106
|
+
/** IFC creation from scratch */
|
|
107
|
+
create: {
|
|
108
|
+
/** Create a new IFC project. Returns a creator handle (number). */
|
|
109
|
+
project(params?: { Name?: string; Description?: string; Schema?: string; LengthUnit?: string; Author?: string; Organization?: string }): number;
|
|
110
|
+
/** Add a building storey. Returns storey expressId. */
|
|
111
|
+
addIfcBuildingStorey(handle: number, params: { Name?: string; Description?: string; Elevation: number }): number;
|
|
112
|
+
/** Add a wall to a storey. Returns wall expressId. */
|
|
113
|
+
addIfcWall(handle: number, storeyId: number, params: { Start: [number,number,number]; End: [number,number,number]; Thickness: number; Height: number; Name?: string; Openings?: Array<{ Width: number; Height: number; Position: [number,number,number]; Name?: string }> }): number;
|
|
114
|
+
/** Add a slab to a storey. Returns slab expressId. */
|
|
115
|
+
addIfcSlab(handle: number, storeyId: number, params: { Position: [number,number,number]; Thickness: number; Width?: number; Depth?: number; Profile?: [number,number][]; Name?: string; Openings?: Array<{ Width: number; Height: number; Position: [number,number,number]; Name?: string }> }): number;
|
|
116
|
+
/** Add a column to a storey. Returns column expressId. */
|
|
117
|
+
addIfcColumn(handle: number, storeyId: number, params: { Position: [number,number,number]; Width: number; Depth: number; Height: number; Name?: string }): number;
|
|
118
|
+
/** Add a beam to a storey. Returns beam expressId. */
|
|
119
|
+
addIfcBeam(handle: number, storeyId: number, params: { Start: [number,number,number]; End: [number,number,number]; Width: number; Height: number; Name?: string }): number;
|
|
120
|
+
/** Add a stair to a storey. Returns stair expressId. */
|
|
121
|
+
addIfcStair(handle: number, storeyId: number, params: { Position: [number,number,number]; NumberOfRisers: number; RiserHeight: number; TreadLength: number; Width: number; Direction?: number; Name?: string }): number;
|
|
122
|
+
/** Add a roof to a storey. Returns roof expressId. */
|
|
123
|
+
addIfcRoof(handle: number, storeyId: number, params: { Position: [number,number,number]; Width: number; Depth: number; Thickness: number; Slope?: number; Name?: string }): number;
|
|
124
|
+
/** Assign a named colour to an element. Call before toIfc(). */
|
|
125
|
+
setColor(handle: number, elementId: number, name: string, rgb: [number, number, number]): void;
|
|
126
|
+
/** Assign an IFC material (simple or layered) to an element. */
|
|
127
|
+
addIfcMaterial(handle: number, elementId: number, material: { Name: string; Category?: string; Layers?: Array<{ Name: string; Thickness: number; Category?: string; IsVentilated?: boolean }> }): void;
|
|
128
|
+
/** Attach a property set to an element. Returns pset expressId. */
|
|
129
|
+
addIfcPropertySet(handle: number, elementId: number, pset: { Name: string; Properties: Array<{ Name: string; NominalValue: string | number | boolean; Type?: string }> }): number;
|
|
130
|
+
/** Attach element quantities to an element. Returns qset expressId. */
|
|
131
|
+
addIfcElementQuantity(handle: number, elementId: number, qset: { Name: string; Quantities: Array<{ Name: string; Value: number; Kind: 'IfcQuantityLength' | 'IfcQuantityArea' | 'IfcQuantityVolume' | 'IfcQuantityCount' | 'IfcQuantityWeight' }> }): number;
|
|
132
|
+
/** Generate the IFC STEP file content. Returns { content, entities, stats }. */
|
|
133
|
+
toIfc(handle: number): { content: string; entities: Array<{ expressId: number; type: string; Name?: string }>; stats: { entityCount: number; fileSize: number } };
|
|
134
|
+
};
|
|
104
135
|
/** Data export */
|
|
105
136
|
export: {
|
|
106
137
|
/** Export entities to CSV string */
|
|
107
138
|
csv(entities: BimEntity[], options: { columns: string[]; filename?: string; separator?: string }): string;
|
|
108
139
|
/** Export entities to JSON array */
|
|
109
140
|
json(entities: BimEntity[], columns: string[]): Record<string, unknown>[];
|
|
141
|
+
/** Trigger a browser file download with raw content */
|
|
142
|
+
download(content: string, filename: string, mimeType?: string): void;
|
|
110
143
|
};
|
|
111
144
|
};
|