@ifc-lite/viewer 1.13.0 → 1.14.1
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 +48 -0
- package/dist/assets/{Arrow.dom-VW5W1XFO.js → Arrow.dom-HLSMJR_v.js} +1 -1
- package/dist/assets/{browser-C6mwD6n0.js → browser-Ch0OnmZN.js} +1 -1
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/{index-BzoX4cQC.js → index-DJbbSLF9.js} +27195 -24454
- package/dist/assets/{index-DQE23JyT.js → index-JPFMj8C9.js} +4 -4
- package/dist/assets/index-Qp8stcGO.css +1 -0
- package/dist/assets/{native-bridge-BibEEmFV.js → native-bridge-BzC7HkDs.js} +1 -1
- package/dist/assets/{wasm-bridge-CYzUd3Io.js → wasm-bridge-B_7dPwOa.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +19 -19
- package/src/components/viewer/BulkPropertyEditor.tsx +8 -1
- package/src/components/viewer/DataConnector.tsx +8 -1
- package/src/components/viewer/ExportChangesButton.tsx +8 -1
- package/src/components/viewer/ExportDialog.tsx +8 -1
- package/src/components/viewer/HierarchyPanel.tsx +7 -1
- package/src/components/viewer/MainToolbar.tsx +51 -18
- package/src/components/viewer/PropertiesPanel.tsx +209 -15
- package/src/components/viewer/Viewport.tsx +5 -1
- package/src/components/viewer/ViewportContainer.tsx +59 -37
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +8 -4
- package/src/components/viewer/properties/BsddCard.tsx +507 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +1 -0
- package/src/components/viewer/useGeometryStreaming.ts +189 -55
- package/src/components/viewer/useMouseControls.ts +55 -14
- package/src/components/viewer/useTouchControls.ts +2 -0
- package/src/hooks/useIfc.ts +19 -1
- package/src/hooks/useIfcCache.ts +6 -1
- package/src/hooks/useIfcFederation.ts +16 -1
- package/src/hooks/useIfcLoader.ts +16 -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/model-adapter.ts +8 -0
- package/src/services/bsdd.ts +262 -0
- package/src/store/index.ts +2 -2
- package/src/store/slices/dataSlice.ts +9 -4
- package/src/store/slices/mutationSlice.ts +155 -1
- package/src/utils/localParsingUtils.ts +3 -1
- package/tsconfig.json +12 -1
- package/vite.config.ts +7 -0
- package/dist/assets/ifc-lite_bg-B6s-pcv0.wasm +0 -0
- package/dist/assets/index-Cx134arv.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 { type PropertyValue, 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): PropertyValue {
|
|
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`;
|