@ifc-lite/viewer 1.6.0 → 1.7.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 +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Property Editor component for editing IFC property values inline.
|
|
7
|
-
*
|
|
7
|
+
* Includes schema-aware property addition with IFC4 standard validation.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
11
11
|
import {
|
|
12
|
-
Save,
|
|
13
12
|
X,
|
|
14
13
|
Plus,
|
|
15
14
|
Trash2,
|
|
@@ -17,6 +16,10 @@ import {
|
|
|
17
16
|
Undo,
|
|
18
17
|
Redo,
|
|
19
18
|
Check,
|
|
19
|
+
BookOpen,
|
|
20
|
+
Tag,
|
|
21
|
+
Layers,
|
|
22
|
+
Ruler,
|
|
20
23
|
} from 'lucide-react';
|
|
21
24
|
import { Button } from '@/components/ui/button';
|
|
22
25
|
import { Input } from '@/components/ui/input';
|
|
@@ -39,9 +42,24 @@ import {
|
|
|
39
42
|
} from '@/components/ui/dialog';
|
|
40
43
|
import { Switch } from '@/components/ui/switch';
|
|
41
44
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
45
|
+
import { Badge } from '@/components/ui/badge';
|
|
42
46
|
import { useViewerStore } from '@/store';
|
|
43
|
-
import { PropertyValueType } from '@ifc-lite/data';
|
|
47
|
+
import { PropertyValueType, QuantityType } from '@ifc-lite/data';
|
|
44
48
|
import type { PropertyValue } from '@ifc-lite/mutations';
|
|
49
|
+
import {
|
|
50
|
+
getPsetDefinitionsForType,
|
|
51
|
+
getPropertiesForPset,
|
|
52
|
+
CLASSIFICATION_SYSTEMS,
|
|
53
|
+
type PsetPropertyDef,
|
|
54
|
+
type PsetDefinition,
|
|
55
|
+
} from '@/lib/ifc4-pset-definitions';
|
|
56
|
+
import {
|
|
57
|
+
getQtoDefinitionsForType,
|
|
58
|
+
getQuantitiesForQto,
|
|
59
|
+
getQuantityUnit,
|
|
60
|
+
type QtoQuantityDef,
|
|
61
|
+
type QtoDefinition,
|
|
62
|
+
} from '@/lib/ifc4-qto-definitions';
|
|
45
63
|
|
|
46
64
|
interface PropertyEditorProps {
|
|
47
65
|
modelId: string;
|
|
@@ -258,42 +276,84 @@ export function PropertyEditor({
|
|
|
258
276
|
);
|
|
259
277
|
}
|
|
260
278
|
|
|
279
|
+
// ============================================================================
|
|
280
|
+
// Schema-Aware Property Dialog
|
|
281
|
+
// ============================================================================
|
|
282
|
+
|
|
261
283
|
interface NewPropertyDialogProps {
|
|
262
284
|
modelId: string;
|
|
263
285
|
entityId: number;
|
|
286
|
+
entityType: string;
|
|
264
287
|
existingPsets: string[];
|
|
288
|
+
schemaVersion?: string;
|
|
265
289
|
}
|
|
266
290
|
|
|
267
291
|
/**
|
|
268
|
-
*
|
|
292
|
+
* Schema-aware dialog for adding new properties.
|
|
293
|
+
* Filters available property sets based on IFC entity type.
|
|
294
|
+
* Shows property suggestions with correct types from IFC4 standard.
|
|
269
295
|
*/
|
|
270
|
-
export function NewPropertyDialog({ modelId, entityId, existingPsets }: NewPropertyDialogProps) {
|
|
296
|
+
export function NewPropertyDialog({ modelId, entityId, entityType, existingPsets, schemaVersion }: NewPropertyDialogProps) {
|
|
271
297
|
const setProperty = useViewerStore((s) => s.setProperty);
|
|
272
298
|
const createPropertySet = useViewerStore((s) => s.createPropertySet);
|
|
273
299
|
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
274
300
|
|
|
275
301
|
const [open, setOpen] = useState(false);
|
|
276
302
|
const [psetName, setPsetName] = useState('');
|
|
277
|
-
const [
|
|
303
|
+
const [isCustomPset, setIsCustomPset] = useState(false);
|
|
304
|
+
const [customPsetName, setCustomPsetName] = useState('');
|
|
278
305
|
const [propName, setPropName] = useState('');
|
|
306
|
+
const [customPropName, setCustomPropName] = useState('');
|
|
279
307
|
const [value, setValue] = useState('');
|
|
280
308
|
const [valueType, setValueType] = useState<PropertyValueType>(PropertyValueType.String);
|
|
281
309
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
// Get schema-valid property sets for this entity type
|
|
311
|
+
const validPsetDefs = useMemo(() => {
|
|
312
|
+
return getPsetDefinitionsForType(entityType, schemaVersion);
|
|
313
|
+
}, [entityType, schemaVersion]);
|
|
314
|
+
|
|
315
|
+
// Split into: already on entity vs available to add
|
|
316
|
+
const { existingStandardPsets, availableStandardPsets } = useMemo(() => {
|
|
317
|
+
const existing: PsetDefinition[] = [];
|
|
318
|
+
const available: PsetDefinition[] = [];
|
|
319
|
+
for (const def of validPsetDefs) {
|
|
320
|
+
if (existingPsets.includes(def.name)) {
|
|
321
|
+
existing.push(def);
|
|
322
|
+
} else {
|
|
323
|
+
available.push(def);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { existingStandardPsets: existing, availableStandardPsets: available };
|
|
327
|
+
}, [validPsetDefs, existingPsets]);
|
|
328
|
+
|
|
329
|
+
// Get property suggestions for selected pset
|
|
330
|
+
const propertySuggestions = useMemo((): PsetPropertyDef[] => {
|
|
331
|
+
if (!psetName || isCustomPset) return [];
|
|
332
|
+
return getPropertiesForPset(psetName);
|
|
333
|
+
}, [psetName, isCustomPset]);
|
|
334
|
+
|
|
335
|
+
// Determine effective property name and type
|
|
336
|
+
const effectivePsetName = isCustomPset ? customPsetName : psetName;
|
|
337
|
+
const effectivePropName = propName || customPropName;
|
|
338
|
+
|
|
339
|
+
// Auto-update type when selecting a standard property
|
|
340
|
+
const handlePropertySelect = useCallback((name: string) => {
|
|
341
|
+
setPropName(name);
|
|
342
|
+
setCustomPropName('');
|
|
343
|
+
// Auto-set type from schema
|
|
344
|
+
const propDef = propertySuggestions.find(p => p.name === name);
|
|
345
|
+
if (propDef) {
|
|
346
|
+
setValueType(propDef.type);
|
|
347
|
+
// Set sensible defaults for boolean properties
|
|
348
|
+
if (propDef.type === PropertyValueType.Boolean) {
|
|
349
|
+
setValue('false');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}, [propertySuggestions]);
|
|
292
353
|
|
|
293
354
|
const handleSubmit = useCallback(() => {
|
|
294
|
-
if (!
|
|
355
|
+
if (!effectivePsetName || !effectivePropName) return;
|
|
295
356
|
|
|
296
|
-
// Normalize model ID for legacy models
|
|
297
357
|
let normalizedModelId = modelId;
|
|
298
358
|
if (modelId === 'legacy') {
|
|
299
359
|
normalizedModelId = '__legacy__';
|
|
@@ -301,85 +361,189 @@ export function NewPropertyDialog({ modelId, entityId, existingPsets }: NewPrope
|
|
|
301
361
|
|
|
302
362
|
const parsedValue = parseValue(value, valueType);
|
|
303
363
|
|
|
304
|
-
if
|
|
305
|
-
|
|
306
|
-
|
|
364
|
+
// Check if pset exists on entity already
|
|
365
|
+
const psetExists = existingPsets.includes(effectivePsetName);
|
|
366
|
+
|
|
367
|
+
if (!psetExists) {
|
|
368
|
+
createPropertySet(normalizedModelId, entityId, effectivePsetName, [
|
|
369
|
+
{ name: effectivePropName, value: parsedValue, type: valueType },
|
|
307
370
|
]);
|
|
308
371
|
} else {
|
|
309
|
-
setProperty(normalizedModelId, entityId,
|
|
372
|
+
setProperty(normalizedModelId, entityId, effectivePsetName, effectivePropName, parsedValue, valueType);
|
|
310
373
|
}
|
|
311
374
|
|
|
312
375
|
bumpMutationVersion();
|
|
313
376
|
|
|
314
377
|
// Reset form
|
|
315
378
|
setPsetName('');
|
|
379
|
+
setCustomPsetName('');
|
|
316
380
|
setPropName('');
|
|
381
|
+
setCustomPropName('');
|
|
317
382
|
setValue('');
|
|
318
383
|
setValueType(PropertyValueType.String);
|
|
319
|
-
|
|
384
|
+
setIsCustomPset(false);
|
|
320
385
|
setOpen(false);
|
|
321
|
-
}, [modelId, entityId,
|
|
386
|
+
}, [modelId, entityId, effectivePsetName, effectivePropName, value, valueType, existingPsets, setProperty, createPropertySet, bumpMutationVersion]);
|
|
387
|
+
|
|
388
|
+
const resetForm = useCallback(() => {
|
|
389
|
+
setPsetName('');
|
|
390
|
+
setCustomPsetName('');
|
|
391
|
+
setPropName('');
|
|
392
|
+
setCustomPropName('');
|
|
393
|
+
setValue('');
|
|
394
|
+
setValueType(PropertyValueType.String);
|
|
395
|
+
setIsCustomPset(false);
|
|
396
|
+
}, []);
|
|
322
397
|
|
|
323
398
|
return (
|
|
324
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
399
|
+
<Dialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) resetForm(); }}>
|
|
325
400
|
<DialogTrigger asChild>
|
|
326
401
|
<Button variant="outline" size="sm" className="h-7">
|
|
327
402
|
<Plus className="h-3 w-3 mr-1" />
|
|
328
|
-
|
|
403
|
+
Property
|
|
329
404
|
</Button>
|
|
330
405
|
</DialogTrigger>
|
|
331
|
-
<DialogContent className="sm:max-w-
|
|
406
|
+
<DialogContent className="sm:max-w-lg">
|
|
332
407
|
<DialogHeader>
|
|
333
|
-
<DialogTitle
|
|
408
|
+
<DialogTitle className="flex items-center gap-2">
|
|
409
|
+
<BookOpen className="h-4 w-4" />
|
|
410
|
+
Add Property
|
|
411
|
+
</DialogTitle>
|
|
334
412
|
<DialogDescription>
|
|
335
|
-
Add a
|
|
413
|
+
Add a property to this <span className="font-mono font-medium text-zinc-700 dark:text-zinc-300">{entityType}</span> element.
|
|
414
|
+
{validPsetDefs.length > 0 && (
|
|
415
|
+
<span className="block mt-1 text-emerald-600 dark:text-emerald-400">
|
|
416
|
+
{schemaVersion || 'IFC4'} schema: {validPsetDefs.length} standard property set{validPsetDefs.length !== 1 ? 's' : ''} available
|
|
417
|
+
</span>
|
|
418
|
+
)}
|
|
336
419
|
</DialogDescription>
|
|
337
420
|
</DialogHeader>
|
|
338
421
|
<div className="grid gap-4 py-4">
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
422
|
+
{/* Property Set Selection */}
|
|
423
|
+
<div className="space-y-2">
|
|
424
|
+
<div className="flex items-center justify-between">
|
|
425
|
+
<Label className="text-sm font-medium">Property Set</Label>
|
|
426
|
+
<Button
|
|
427
|
+
variant="ghost"
|
|
428
|
+
size="sm"
|
|
429
|
+
className="h-6 px-2 text-[10px]"
|
|
430
|
+
onClick={() => { setIsCustomPset(!isCustomPset); setPsetName(''); setCustomPsetName(''); setPropName(''); setCustomPropName(''); }}
|
|
431
|
+
>
|
|
432
|
+
{isCustomPset ? 'Use standard' : 'Custom name'}
|
|
433
|
+
</Button>
|
|
434
|
+
</div>
|
|
435
|
+
{isCustomPset ? (
|
|
346
436
|
<Input
|
|
347
|
-
value={
|
|
348
|
-
onChange={(e) =>
|
|
349
|
-
placeholder="e.g.,
|
|
437
|
+
value={customPsetName}
|
|
438
|
+
onChange={(e) => setCustomPsetName(e.target.value)}
|
|
439
|
+
placeholder="e.g., Pset_MyCustomProperties"
|
|
440
|
+
className="font-mono text-sm"
|
|
350
441
|
/>
|
|
351
442
|
) : (
|
|
352
|
-
<Select value={psetName} onValueChange={setPsetName}>
|
|
353
|
-
<SelectTrigger>
|
|
354
|
-
<SelectValue placeholder="Select property set" />
|
|
443
|
+
<Select value={psetName} onValueChange={(v) => { setPsetName(v); setPropName(''); setCustomPropName(''); setValue(''); }}>
|
|
444
|
+
<SelectTrigger className="font-mono text-sm">
|
|
445
|
+
<SelectValue placeholder="Select property set..." />
|
|
355
446
|
</SelectTrigger>
|
|
356
447
|
<SelectContent>
|
|
357
|
-
{
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
448
|
+
{/* Existing psets on this entity */}
|
|
449
|
+
{existingStandardPsets.length > 0 && (
|
|
450
|
+
<>
|
|
451
|
+
<div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-zinc-400">
|
|
452
|
+
On this element
|
|
453
|
+
</div>
|
|
454
|
+
{existingStandardPsets.map((def) => (
|
|
455
|
+
<SelectItem key={def.name} value={def.name}>
|
|
456
|
+
<div className="flex items-center gap-2">
|
|
457
|
+
<span>{def.name}</span>
|
|
458
|
+
<Badge variant="secondary" className="h-4 px-1 text-[9px]">existing</Badge>
|
|
459
|
+
</div>
|
|
460
|
+
</SelectItem>
|
|
461
|
+
))}
|
|
462
|
+
</>
|
|
463
|
+
)}
|
|
464
|
+
{/* Non-standard existing psets */}
|
|
465
|
+
{existingPsets.filter(p => !existingStandardPsets.some(d => d.name === p)).length > 0 && (
|
|
466
|
+
<>
|
|
467
|
+
<div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-zinc-400">
|
|
468
|
+
Existing (custom)
|
|
469
|
+
</div>
|
|
470
|
+
{existingPsets.filter(p => !existingStandardPsets.some(d => d.name === p)).map((name) => (
|
|
471
|
+
<SelectItem key={name} value={name}>
|
|
472
|
+
<span>{name}</span>
|
|
473
|
+
</SelectItem>
|
|
474
|
+
))}
|
|
475
|
+
</>
|
|
476
|
+
)}
|
|
477
|
+
{/* Available standard psets for this type */}
|
|
478
|
+
{availableStandardPsets.length > 0 && (
|
|
479
|
+
<>
|
|
480
|
+
<div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
|
|
481
|
+
{schemaVersion || 'IFC4'} Standard — {entityType}
|
|
482
|
+
</div>
|
|
483
|
+
{availableStandardPsets.map((def) => (
|
|
484
|
+
<SelectItem key={def.name} value={def.name}>
|
|
485
|
+
<div className="flex flex-col">
|
|
486
|
+
<div className="flex items-center gap-2">
|
|
487
|
+
<span className="font-medium">{def.name}</span>
|
|
488
|
+
<Badge variant="outline" className="h-4 px-1 text-[9px] border-emerald-300 text-emerald-600">new</Badge>
|
|
489
|
+
</div>
|
|
490
|
+
<span className="text-[10px] text-zinc-400">{def.description}</span>
|
|
491
|
+
</div>
|
|
492
|
+
</SelectItem>
|
|
493
|
+
))}
|
|
494
|
+
</>
|
|
495
|
+
)}
|
|
369
496
|
</SelectContent>
|
|
370
497
|
</Select>
|
|
371
498
|
)}
|
|
372
499
|
</div>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
500
|
+
|
|
501
|
+
{/* Property Selection */}
|
|
502
|
+
<div className="space-y-2">
|
|
503
|
+
<Label className="text-sm font-medium">Property</Label>
|
|
504
|
+
{propertySuggestions.length > 0 ? (
|
|
505
|
+
<div className="space-y-2">
|
|
506
|
+
<Select value={propName} onValueChange={handlePropertySelect}>
|
|
507
|
+
<SelectTrigger className="font-mono text-sm">
|
|
508
|
+
<SelectValue placeholder="Select property..." />
|
|
509
|
+
</SelectTrigger>
|
|
510
|
+
<SelectContent>
|
|
511
|
+
{propertySuggestions.map((prop) => (
|
|
512
|
+
<SelectItem key={prop.name} value={prop.name}>
|
|
513
|
+
<div className="flex flex-col">
|
|
514
|
+
<div className="flex items-center gap-2">
|
|
515
|
+
<span className="font-medium">{prop.name}</span>
|
|
516
|
+
<Badge variant="secondary" className="h-4 px-1 text-[9px]">{getTypeName(prop.type)}</Badge>
|
|
517
|
+
</div>
|
|
518
|
+
<span className="text-[10px] text-zinc-400">{prop.description}</span>
|
|
519
|
+
</div>
|
|
520
|
+
</SelectItem>
|
|
521
|
+
))}
|
|
522
|
+
</SelectContent>
|
|
523
|
+
</Select>
|
|
524
|
+
{/* Allow custom property name even for standard psets */}
|
|
525
|
+
{!propName && (
|
|
526
|
+
<Input
|
|
527
|
+
value={customPropName}
|
|
528
|
+
onChange={(e) => setCustomPropName(e.target.value)}
|
|
529
|
+
placeholder="Or type custom property name..."
|
|
530
|
+
className="font-mono text-sm"
|
|
531
|
+
/>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
) : (
|
|
535
|
+
<Input
|
|
536
|
+
value={customPropName}
|
|
537
|
+
onChange={(e) => setCustomPropName(e.target.value)}
|
|
538
|
+
placeholder="e.g., FireRating"
|
|
539
|
+
className="font-mono text-sm"
|
|
540
|
+
/>
|
|
541
|
+
)}
|
|
380
542
|
</div>
|
|
381
|
-
|
|
382
|
-
|
|
543
|
+
|
|
544
|
+
{/* Type selector */}
|
|
545
|
+
<div className="space-y-2">
|
|
546
|
+
<Label className="text-sm font-medium">Type</Label>
|
|
383
547
|
<Select
|
|
384
548
|
value={valueType.toString()}
|
|
385
549
|
onValueChange={(v) => setValueType(parseInt(v) as PropertyValueType)}
|
|
@@ -397,28 +561,34 @@ export function NewPropertyDialog({ modelId, entityId, existingPsets }: NewPrope
|
|
|
397
561
|
</SelectContent>
|
|
398
562
|
</Select>
|
|
399
563
|
</div>
|
|
400
|
-
|
|
401
|
-
|
|
564
|
+
|
|
565
|
+
{/* Value input */}
|
|
566
|
+
<div className="space-y-2">
|
|
567
|
+
<Label className="text-sm font-medium">Value</Label>
|
|
402
568
|
{valueType === PropertyValueType.Boolean ? (
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
569
|
+
<div className="flex items-center gap-3">
|
|
570
|
+
<Switch
|
|
571
|
+
checked={value === 'true'}
|
|
572
|
+
onCheckedChange={(checked) => setValue(checked ? 'true' : 'false')}
|
|
573
|
+
/>
|
|
574
|
+
<span className="text-sm text-zinc-500">{value === 'true' ? 'True' : 'False'}</span>
|
|
575
|
+
</div>
|
|
407
576
|
) : (
|
|
408
577
|
<Input
|
|
409
578
|
value={value}
|
|
410
579
|
onChange={(e) => setValue(e.target.value)}
|
|
411
580
|
placeholder="Property value"
|
|
412
581
|
type={valueType === PropertyValueType.Real || valueType === PropertyValueType.Integer ? 'number' : 'text'}
|
|
582
|
+
className="font-mono text-sm"
|
|
413
583
|
/>
|
|
414
584
|
)}
|
|
415
585
|
</div>
|
|
416
586
|
</div>
|
|
417
587
|
<DialogFooter>
|
|
418
|
-
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
588
|
+
<Button variant="outline" onClick={() => { setOpen(false); resetForm(); }}>
|
|
419
589
|
Cancel
|
|
420
590
|
</Button>
|
|
421
|
-
<Button onClick={handleSubmit} disabled={!
|
|
591
|
+
<Button onClick={handleSubmit} disabled={!effectivePsetName || !effectivePropName}>
|
|
422
592
|
Add Property
|
|
423
593
|
</Button>
|
|
424
594
|
</DialogFooter>
|
|
@@ -427,6 +597,622 @@ export function NewPropertyDialog({ modelId, entityId, existingPsets }: NewPrope
|
|
|
427
597
|
);
|
|
428
598
|
}
|
|
429
599
|
|
|
600
|
+
// ============================================================================
|
|
601
|
+
// Classification Dialog
|
|
602
|
+
// ============================================================================
|
|
603
|
+
|
|
604
|
+
interface AddClassificationDialogProps {
|
|
605
|
+
modelId: string;
|
|
606
|
+
entityId: number;
|
|
607
|
+
entityType: string;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Dialog for adding a classification reference to an entity.
|
|
612
|
+
* Supports common classification systems (Uniclass, OmniClass, MasterFormat, etc.).
|
|
613
|
+
* Stored as a special property set for mutation tracking.
|
|
614
|
+
*/
|
|
615
|
+
export function AddClassificationDialog({ modelId, entityId, entityType }: AddClassificationDialogProps) {
|
|
616
|
+
const createPropertySet = useViewerStore((s) => s.createPropertySet);
|
|
617
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
618
|
+
|
|
619
|
+
const [open, setOpen] = useState(false);
|
|
620
|
+
const [system, setSystem] = useState('');
|
|
621
|
+
const [customSystem, setCustomSystem] = useState('');
|
|
622
|
+
const [identification, setIdentification] = useState('');
|
|
623
|
+
const [name, setName] = useState('');
|
|
624
|
+
|
|
625
|
+
const effectiveSystem = system === '__custom__' ? customSystem : system;
|
|
626
|
+
|
|
627
|
+
const handleSubmit = useCallback(() => {
|
|
628
|
+
if (!effectiveSystem || !identification) return;
|
|
629
|
+
|
|
630
|
+
let normalizedModelId = modelId;
|
|
631
|
+
if (modelId === 'legacy') {
|
|
632
|
+
normalizedModelId = '__legacy__';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Store classification as a property set named "Classification [SystemName]"
|
|
636
|
+
const psetName = `Classification [${effectiveSystem}]`;
|
|
637
|
+
createPropertySet(normalizedModelId, entityId, psetName, [
|
|
638
|
+
{ name: 'System', value: effectiveSystem, type: PropertyValueType.Label },
|
|
639
|
+
{ name: 'Identification', value: identification, type: PropertyValueType.Identifier },
|
|
640
|
+
{ name: 'Name', value: name || identification, type: PropertyValueType.Label },
|
|
641
|
+
]);
|
|
642
|
+
|
|
643
|
+
bumpMutationVersion();
|
|
644
|
+
|
|
645
|
+
// Reset form
|
|
646
|
+
setSystem('');
|
|
647
|
+
setCustomSystem('');
|
|
648
|
+
setIdentification('');
|
|
649
|
+
setName('');
|
|
650
|
+
setOpen(false);
|
|
651
|
+
}, [modelId, entityId, effectiveSystem, identification, name, createPropertySet, bumpMutationVersion]);
|
|
652
|
+
|
|
653
|
+
return (
|
|
654
|
+
<Dialog open={open} onOpenChange={(o) => {
|
|
655
|
+
setOpen(o);
|
|
656
|
+
if (!o) { setSystem(''); setCustomSystem(''); setIdentification(''); setName(''); }
|
|
657
|
+
}}>
|
|
658
|
+
<DialogTrigger asChild>
|
|
659
|
+
<Button variant="outline" size="sm" className="h-7">
|
|
660
|
+
<Tag className="h-3 w-3 mr-1" />
|
|
661
|
+
Classification
|
|
662
|
+
</Button>
|
|
663
|
+
</DialogTrigger>
|
|
664
|
+
<DialogContent className="sm:max-w-md">
|
|
665
|
+
<DialogHeader>
|
|
666
|
+
<DialogTitle className="flex items-center gap-2">
|
|
667
|
+
<Tag className="h-4 w-4" />
|
|
668
|
+
Add Classification
|
|
669
|
+
</DialogTitle>
|
|
670
|
+
<DialogDescription>
|
|
671
|
+
Assign a classification reference to this <span className="font-mono font-medium text-zinc-700 dark:text-zinc-300">{entityType}</span>.
|
|
672
|
+
</DialogDescription>
|
|
673
|
+
</DialogHeader>
|
|
674
|
+
<div className="grid gap-4 py-4">
|
|
675
|
+
{/* Classification System */}
|
|
676
|
+
<div className="space-y-2">
|
|
677
|
+
<Label className="text-sm font-medium">Classification System</Label>
|
|
678
|
+
<Select value={system} onValueChange={setSystem}>
|
|
679
|
+
<SelectTrigger>
|
|
680
|
+
<SelectValue placeholder="Select system..." />
|
|
681
|
+
</SelectTrigger>
|
|
682
|
+
<SelectContent>
|
|
683
|
+
{CLASSIFICATION_SYSTEMS.map((cs) => (
|
|
684
|
+
<SelectItem key={cs.name} value={cs.name}>
|
|
685
|
+
<div className="flex flex-col">
|
|
686
|
+
<span className="font-medium">{cs.name}</span>
|
|
687
|
+
<span className="text-[10px] text-zinc-400">{cs.description}</span>
|
|
688
|
+
</div>
|
|
689
|
+
</SelectItem>
|
|
690
|
+
))}
|
|
691
|
+
<SelectItem value="__custom__">
|
|
692
|
+
<span className="text-zinc-500">Custom system...</span>
|
|
693
|
+
</SelectItem>
|
|
694
|
+
</SelectContent>
|
|
695
|
+
</Select>
|
|
696
|
+
{system === '__custom__' && (
|
|
697
|
+
<Input
|
|
698
|
+
value={customSystem}
|
|
699
|
+
onChange={(e) => setCustomSystem(e.target.value)}
|
|
700
|
+
placeholder="Classification system name"
|
|
701
|
+
className="mt-2"
|
|
702
|
+
/>
|
|
703
|
+
)}
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
{/* Identification (code) */}
|
|
707
|
+
<div className="space-y-2">
|
|
708
|
+
<Label className="text-sm font-medium">Identification Code</Label>
|
|
709
|
+
<Input
|
|
710
|
+
value={identification}
|
|
711
|
+
onChange={(e) => setIdentification(e.target.value)}
|
|
712
|
+
placeholder="e.g., Ss_25_10_30 or 03 30 00"
|
|
713
|
+
className="font-mono"
|
|
714
|
+
/>
|
|
715
|
+
<p className="text-[10px] text-zinc-400">The classification code or reference number</p>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
{/* Name (optional) */}
|
|
719
|
+
<div className="space-y-2">
|
|
720
|
+
<Label className="text-sm font-medium">Name (optional)</Label>
|
|
721
|
+
<Input
|
|
722
|
+
value={name}
|
|
723
|
+
onChange={(e) => setName(e.target.value)}
|
|
724
|
+
placeholder="e.g., Cast-in-place concrete walls"
|
|
725
|
+
/>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
<DialogFooter>
|
|
729
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
730
|
+
Cancel
|
|
731
|
+
</Button>
|
|
732
|
+
<Button onClick={handleSubmit} disabled={!effectiveSystem || !identification}>
|
|
733
|
+
Add Classification
|
|
734
|
+
</Button>
|
|
735
|
+
</DialogFooter>
|
|
736
|
+
</DialogContent>
|
|
737
|
+
</Dialog>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ============================================================================
|
|
742
|
+
// Material Dialog
|
|
743
|
+
// ============================================================================
|
|
744
|
+
|
|
745
|
+
interface AddMaterialDialogProps {
|
|
746
|
+
modelId: string;
|
|
747
|
+
entityId: number;
|
|
748
|
+
entityType: string;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Dialog for assigning a material to an entity.
|
|
753
|
+
* Stored as a special property set for mutation tracking.
|
|
754
|
+
*/
|
|
755
|
+
export function AddMaterialDialog({ modelId, entityId, entityType }: AddMaterialDialogProps) {
|
|
756
|
+
const createPropertySet = useViewerStore((s) => s.createPropertySet);
|
|
757
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
758
|
+
|
|
759
|
+
const [open, setOpen] = useState(false);
|
|
760
|
+
const [materialName, setMaterialName] = useState('');
|
|
761
|
+
const [category, setCategory] = useState('');
|
|
762
|
+
const [description, setDescription] = useState('');
|
|
763
|
+
|
|
764
|
+
const handleSubmit = useCallback(() => {
|
|
765
|
+
if (!materialName) return;
|
|
766
|
+
|
|
767
|
+
let normalizedModelId = modelId;
|
|
768
|
+
if (modelId === 'legacy') {
|
|
769
|
+
normalizedModelId = '__legacy__';
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Store material as a property set named "Material"
|
|
773
|
+
const psetName = `Material [${materialName}]`;
|
|
774
|
+
const properties: Array<{ name: string; value: string; type: PropertyValueType }> = [
|
|
775
|
+
{ name: 'Name', value: materialName, type: PropertyValueType.Label },
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
if (category) {
|
|
779
|
+
properties.push({ name: 'Category', value: category, type: PropertyValueType.Label });
|
|
780
|
+
}
|
|
781
|
+
if (description) {
|
|
782
|
+
properties.push({ name: 'Description', value: description, type: PropertyValueType.Label });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
createPropertySet(normalizedModelId, entityId, psetName, properties);
|
|
786
|
+
bumpMutationVersion();
|
|
787
|
+
|
|
788
|
+
// Reset form
|
|
789
|
+
setMaterialName('');
|
|
790
|
+
setCategory('');
|
|
791
|
+
setDescription('');
|
|
792
|
+
setOpen(false);
|
|
793
|
+
}, [modelId, entityId, materialName, category, description, createPropertySet, bumpMutationVersion]);
|
|
794
|
+
|
|
795
|
+
// Common material categories (module-level constant used below)
|
|
796
|
+
const materialCategories = MATERIAL_CATEGORIES;
|
|
797
|
+
|
|
798
|
+
return (
|
|
799
|
+
<Dialog open={open} onOpenChange={(o) => {
|
|
800
|
+
setOpen(o);
|
|
801
|
+
if (!o) { setMaterialName(''); setCategory(''); setDescription(''); }
|
|
802
|
+
}}>
|
|
803
|
+
<DialogTrigger asChild>
|
|
804
|
+
<Button variant="outline" size="sm" className="h-7">
|
|
805
|
+
<Layers className="h-3 w-3 mr-1" />
|
|
806
|
+
Material
|
|
807
|
+
</Button>
|
|
808
|
+
</DialogTrigger>
|
|
809
|
+
<DialogContent className="sm:max-w-md">
|
|
810
|
+
<DialogHeader>
|
|
811
|
+
<DialogTitle className="flex items-center gap-2">
|
|
812
|
+
<Layers className="h-4 w-4" />
|
|
813
|
+
Add Material
|
|
814
|
+
</DialogTitle>
|
|
815
|
+
<DialogDescription>
|
|
816
|
+
Assign a material to this <span className="font-mono font-medium text-zinc-700 dark:text-zinc-300">{entityType}</span>.
|
|
817
|
+
</DialogDescription>
|
|
818
|
+
</DialogHeader>
|
|
819
|
+
<div className="grid gap-4 py-4">
|
|
820
|
+
{/* Material Name */}
|
|
821
|
+
<div className="space-y-2">
|
|
822
|
+
<Label className="text-sm font-medium">Material Name</Label>
|
|
823
|
+
<Input
|
|
824
|
+
value={materialName}
|
|
825
|
+
onChange={(e) => setMaterialName(e.target.value)}
|
|
826
|
+
placeholder="e.g., Concrete C30/37"
|
|
827
|
+
className="font-mono"
|
|
828
|
+
/>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
{/* Category */}
|
|
832
|
+
<div className="space-y-2">
|
|
833
|
+
<Label className="text-sm font-medium">Category</Label>
|
|
834
|
+
<Select value={category} onValueChange={setCategory}>
|
|
835
|
+
<SelectTrigger>
|
|
836
|
+
<SelectValue placeholder="Select category..." />
|
|
837
|
+
</SelectTrigger>
|
|
838
|
+
<SelectContent>
|
|
839
|
+
{materialCategories.map((cat) => (
|
|
840
|
+
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
|
841
|
+
))}
|
|
842
|
+
</SelectContent>
|
|
843
|
+
</Select>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
{/* Description */}
|
|
847
|
+
<div className="space-y-2">
|
|
848
|
+
<Label className="text-sm font-medium">Description (optional)</Label>
|
|
849
|
+
<Input
|
|
850
|
+
value={description}
|
|
851
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
852
|
+
placeholder="Additional details about the material"
|
|
853
|
+
/>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
<DialogFooter>
|
|
857
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
858
|
+
Cancel
|
|
859
|
+
</Button>
|
|
860
|
+
<Button onClick={handleSubmit} disabled={!materialName}>
|
|
861
|
+
Add Material
|
|
862
|
+
</Button>
|
|
863
|
+
</DialogFooter>
|
|
864
|
+
</DialogContent>
|
|
865
|
+
</Dialog>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Common material categories - static, hoisted to module scope
|
|
870
|
+
const MATERIAL_CATEGORIES = [
|
|
871
|
+
'Concrete', 'Steel', 'Wood', 'Masonry', 'Glass', 'Aluminium',
|
|
872
|
+
'Insulation', 'Gypsum', 'Stone', 'Ceramic', 'Plastic', 'Composite',
|
|
873
|
+
] as const;
|
|
874
|
+
|
|
875
|
+
// ============================================================================
|
|
876
|
+
// Quantity Dialog
|
|
877
|
+
// ============================================================================
|
|
878
|
+
|
|
879
|
+
interface AddQuantityDialogProps {
|
|
880
|
+
modelId: string;
|
|
881
|
+
entityId: number;
|
|
882
|
+
entityType: string;
|
|
883
|
+
existingQtos: string[];
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Schema-aware dialog for adding quantities.
|
|
888
|
+
* Filters available quantity sets based on IFC entity type.
|
|
889
|
+
* Shows quantity suggestions with correct types from IFC4 standard.
|
|
890
|
+
*/
|
|
891
|
+
export function AddQuantityDialog({ modelId, entityId, entityType, existingQtos }: AddQuantityDialogProps) {
|
|
892
|
+
const createPropertySet = useViewerStore((s) => s.createPropertySet);
|
|
893
|
+
const setProperty = useViewerStore((s) => s.setProperty);
|
|
894
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
895
|
+
|
|
896
|
+
const [open, setOpen] = useState(false);
|
|
897
|
+
const [qtoName, setQtoName] = useState('');
|
|
898
|
+
const [isCustomQto, setIsCustomQto] = useState(false);
|
|
899
|
+
const [customQtoName, setCustomQtoName] = useState('');
|
|
900
|
+
const [quantityName, setQuantityName] = useState('');
|
|
901
|
+
const [customQuantityName, setCustomQuantityName] = useState('');
|
|
902
|
+
const [value, setValue] = useState('');
|
|
903
|
+
const [quantityType, setQuantityType] = useState<QuantityType>(QuantityType.Length);
|
|
904
|
+
|
|
905
|
+
// Get schema-valid quantity sets for this entity type
|
|
906
|
+
const validQtoDefs = useMemo(() => {
|
|
907
|
+
return getQtoDefinitionsForType(entityType);
|
|
908
|
+
}, [entityType]);
|
|
909
|
+
|
|
910
|
+
// Split into: already on entity vs available to add
|
|
911
|
+
const { existingStandardQtos, availableStandardQtos } = useMemo(() => {
|
|
912
|
+
const existing: QtoDefinition[] = [];
|
|
913
|
+
const available: QtoDefinition[] = [];
|
|
914
|
+
for (const def of validQtoDefs) {
|
|
915
|
+
if (existingQtos.includes(def.name)) {
|
|
916
|
+
existing.push(def);
|
|
917
|
+
} else {
|
|
918
|
+
available.push(def);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return { existingStandardQtos: existing, availableStandardQtos: available };
|
|
922
|
+
}, [validQtoDefs, existingQtos]);
|
|
923
|
+
|
|
924
|
+
// Get quantity suggestions for selected qto set
|
|
925
|
+
const quantitySuggestions = useMemo((): QtoQuantityDef[] => {
|
|
926
|
+
if (!qtoName || isCustomQto) return [];
|
|
927
|
+
return getQuantitiesForQto(qtoName);
|
|
928
|
+
}, [qtoName, isCustomQto]);
|
|
929
|
+
|
|
930
|
+
const effectiveQtoName = isCustomQto ? customQtoName : qtoName;
|
|
931
|
+
const effectiveQuantityName = quantityName || customQuantityName;
|
|
932
|
+
|
|
933
|
+
// Auto-update type when selecting a standard quantity
|
|
934
|
+
const handleQuantitySelect = useCallback((name: string) => {
|
|
935
|
+
setQuantityName(name);
|
|
936
|
+
setCustomQuantityName('');
|
|
937
|
+
const qtyDef = quantitySuggestions.find(q => q.name === name);
|
|
938
|
+
if (qtyDef) {
|
|
939
|
+
setQuantityType(qtyDef.type);
|
|
940
|
+
}
|
|
941
|
+
}, [quantitySuggestions]);
|
|
942
|
+
|
|
943
|
+
const handleSubmit = useCallback(() => {
|
|
944
|
+
if (!effectiveQtoName || !effectiveQuantityName) return;
|
|
945
|
+
|
|
946
|
+
let normalizedModelId = modelId;
|
|
947
|
+
if (modelId === 'legacy') {
|
|
948
|
+
normalizedModelId = '__legacy__';
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const parsedValue = parseFloat(value) || 0;
|
|
952
|
+
|
|
953
|
+
// Store quantity as a property set (mutation system uses property sets)
|
|
954
|
+
const qtoExists = existingQtos.includes(effectiveQtoName);
|
|
955
|
+
|
|
956
|
+
if (!qtoExists) {
|
|
957
|
+
createPropertySet(normalizedModelId, entityId, effectiveQtoName, [
|
|
958
|
+
{ name: effectiveQuantityName, value: parsedValue, type: PropertyValueType.Real },
|
|
959
|
+
]);
|
|
960
|
+
} else {
|
|
961
|
+
setProperty(normalizedModelId, entityId, effectiveQtoName, effectiveQuantityName, parsedValue, PropertyValueType.Real);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
bumpMutationVersion();
|
|
965
|
+
|
|
966
|
+
// Reset form
|
|
967
|
+
setQtoName('');
|
|
968
|
+
setCustomQtoName('');
|
|
969
|
+
setQuantityName('');
|
|
970
|
+
setCustomQuantityName('');
|
|
971
|
+
setValue('');
|
|
972
|
+
setQuantityType(QuantityType.Length);
|
|
973
|
+
setIsCustomQto(false);
|
|
974
|
+
setOpen(false);
|
|
975
|
+
}, [modelId, entityId, effectiveQtoName, effectiveQuantityName, value, existingQtos, setProperty, createPropertySet, bumpMutationVersion]);
|
|
976
|
+
|
|
977
|
+
const resetForm = useCallback(() => {
|
|
978
|
+
setQtoName('');
|
|
979
|
+
setCustomQtoName('');
|
|
980
|
+
setQuantityName('');
|
|
981
|
+
setCustomQuantityName('');
|
|
982
|
+
setValue('');
|
|
983
|
+
setQuantityType(QuantityType.Length);
|
|
984
|
+
setIsCustomQto(false);
|
|
985
|
+
}, []);
|
|
986
|
+
|
|
987
|
+
return (
|
|
988
|
+
<Dialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) resetForm(); }}>
|
|
989
|
+
<DialogTrigger asChild>
|
|
990
|
+
<Button variant="outline" size="sm" className="h-7">
|
|
991
|
+
<Ruler className="h-3 w-3 mr-1" />
|
|
992
|
+
Quantity
|
|
993
|
+
</Button>
|
|
994
|
+
</DialogTrigger>
|
|
995
|
+
<DialogContent className="sm:max-w-lg">
|
|
996
|
+
<DialogHeader>
|
|
997
|
+
<DialogTitle className="flex items-center gap-2">
|
|
998
|
+
<Ruler className="h-4 w-4" />
|
|
999
|
+
Add Quantity
|
|
1000
|
+
</DialogTitle>
|
|
1001
|
+
<DialogDescription>
|
|
1002
|
+
Add a quantity to this <span className="font-mono font-medium text-zinc-700 dark:text-zinc-300">{entityType}</span> element.
|
|
1003
|
+
{validQtoDefs.length > 0 && (
|
|
1004
|
+
<span className="block mt-1 text-emerald-600 dark:text-emerald-400">
|
|
1005
|
+
IFC4 schema: {validQtoDefs.length} standard quantity set{validQtoDefs.length !== 1 ? 's' : ''} available
|
|
1006
|
+
</span>
|
|
1007
|
+
)}
|
|
1008
|
+
</DialogDescription>
|
|
1009
|
+
</DialogHeader>
|
|
1010
|
+
<div className="grid gap-4 py-4">
|
|
1011
|
+
{/* Quantity Set Selection */}
|
|
1012
|
+
<div className="space-y-2">
|
|
1013
|
+
<div className="flex items-center justify-between">
|
|
1014
|
+
<Label className="text-sm font-medium">Quantity Set</Label>
|
|
1015
|
+
<Button
|
|
1016
|
+
variant="ghost"
|
|
1017
|
+
size="sm"
|
|
1018
|
+
className="h-6 px-2 text-[10px]"
|
|
1019
|
+
onClick={() => { setIsCustomQto(!isCustomQto); setQtoName(''); setCustomQtoName(''); setQuantityName(''); setCustomQuantityName(''); }}
|
|
1020
|
+
>
|
|
1021
|
+
{isCustomQto ? 'Use standard' : 'Custom name'}
|
|
1022
|
+
</Button>
|
|
1023
|
+
</div>
|
|
1024
|
+
{isCustomQto ? (
|
|
1025
|
+
<Input
|
|
1026
|
+
value={customQtoName}
|
|
1027
|
+
onChange={(e) => setCustomQtoName(e.target.value)}
|
|
1028
|
+
placeholder="e.g., Qto_MyCustomQuantities"
|
|
1029
|
+
className="font-mono text-sm"
|
|
1030
|
+
/>
|
|
1031
|
+
) : (
|
|
1032
|
+
<Select value={qtoName} onValueChange={(v) => { setQtoName(v); setQuantityName(''); setCustomQuantityName(''); setValue(''); }}>
|
|
1033
|
+
<SelectTrigger className="font-mono text-sm">
|
|
1034
|
+
<SelectValue placeholder="Select quantity set..." />
|
|
1035
|
+
</SelectTrigger>
|
|
1036
|
+
<SelectContent>
|
|
1037
|
+
{existingStandardQtos.length > 0 && (
|
|
1038
|
+
<>
|
|
1039
|
+
<div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-zinc-400">
|
|
1040
|
+
On this element
|
|
1041
|
+
</div>
|
|
1042
|
+
{existingStandardQtos.map((def) => (
|
|
1043
|
+
<SelectItem key={def.name} value={def.name}>
|
|
1044
|
+
<div className="flex items-center gap-2">
|
|
1045
|
+
<span>{def.name}</span>
|
|
1046
|
+
<Badge variant="secondary" className="h-4 px-1 text-[9px]">existing</Badge>
|
|
1047
|
+
</div>
|
|
1048
|
+
</SelectItem>
|
|
1049
|
+
))}
|
|
1050
|
+
</>
|
|
1051
|
+
)}
|
|
1052
|
+
{existingQtos.filter(q => !existingStandardQtos.some(d => d.name === q)).length > 0 && (
|
|
1053
|
+
<>
|
|
1054
|
+
<div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-zinc-400">
|
|
1055
|
+
Existing (custom)
|
|
1056
|
+
</div>
|
|
1057
|
+
{existingQtos.filter(q => !existingStandardQtos.some(d => d.name === q)).map((name) => (
|
|
1058
|
+
<SelectItem key={name} value={name}>
|
|
1059
|
+
<span>{name}</span>
|
|
1060
|
+
</SelectItem>
|
|
1061
|
+
))}
|
|
1062
|
+
</>
|
|
1063
|
+
)}
|
|
1064
|
+
{availableStandardQtos.length > 0 && (
|
|
1065
|
+
<>
|
|
1066
|
+
<div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
|
|
1067
|
+
IFC4 Standard — {entityType}
|
|
1068
|
+
</div>
|
|
1069
|
+
{availableStandardQtos.map((def) => (
|
|
1070
|
+
<SelectItem key={def.name} value={def.name}>
|
|
1071
|
+
<div className="flex flex-col">
|
|
1072
|
+
<div className="flex items-center gap-2">
|
|
1073
|
+
<span className="font-medium">{def.name}</span>
|
|
1074
|
+
<Badge variant="outline" className="h-4 px-1 text-[9px] border-emerald-300 text-emerald-600">new</Badge>
|
|
1075
|
+
</div>
|
|
1076
|
+
<span className="text-[10px] text-zinc-400">{def.description}</span>
|
|
1077
|
+
</div>
|
|
1078
|
+
</SelectItem>
|
|
1079
|
+
))}
|
|
1080
|
+
</>
|
|
1081
|
+
)}
|
|
1082
|
+
</SelectContent>
|
|
1083
|
+
</Select>
|
|
1084
|
+
)}
|
|
1085
|
+
</div>
|
|
1086
|
+
|
|
1087
|
+
{/* Quantity Selection */}
|
|
1088
|
+
<div className="space-y-2">
|
|
1089
|
+
<Label className="text-sm font-medium">Quantity</Label>
|
|
1090
|
+
{quantitySuggestions.length > 0 ? (
|
|
1091
|
+
<div className="space-y-2">
|
|
1092
|
+
<Select value={quantityName} onValueChange={handleQuantitySelect}>
|
|
1093
|
+
<SelectTrigger className="font-mono text-sm">
|
|
1094
|
+
<SelectValue placeholder="Select quantity..." />
|
|
1095
|
+
</SelectTrigger>
|
|
1096
|
+
<SelectContent>
|
|
1097
|
+
{quantitySuggestions.map((qty) => (
|
|
1098
|
+
<SelectItem key={qty.name} value={qty.name}>
|
|
1099
|
+
<div className="flex flex-col">
|
|
1100
|
+
<div className="flex items-center gap-2">
|
|
1101
|
+
<span className="font-medium">{qty.name}</span>
|
|
1102
|
+
<Badge variant="secondary" className="h-4 px-1 text-[9px]">{qty.unit}</Badge>
|
|
1103
|
+
</div>
|
|
1104
|
+
<span className="text-[10px] text-zinc-400">{qty.description}</span>
|
|
1105
|
+
</div>
|
|
1106
|
+
</SelectItem>
|
|
1107
|
+
))}
|
|
1108
|
+
</SelectContent>
|
|
1109
|
+
</Select>
|
|
1110
|
+
{!quantityName && (
|
|
1111
|
+
<Input
|
|
1112
|
+
value={customQuantityName}
|
|
1113
|
+
onChange={(e) => setCustomQuantityName(e.target.value)}
|
|
1114
|
+
placeholder="Or type custom quantity name..."
|
|
1115
|
+
className="font-mono text-sm"
|
|
1116
|
+
/>
|
|
1117
|
+
)}
|
|
1118
|
+
</div>
|
|
1119
|
+
) : (
|
|
1120
|
+
<Input
|
|
1121
|
+
value={customQuantityName}
|
|
1122
|
+
onChange={(e) => setCustomQuantityName(e.target.value)}
|
|
1123
|
+
placeholder="e.g., Length"
|
|
1124
|
+
className="font-mono text-sm"
|
|
1125
|
+
/>
|
|
1126
|
+
)}
|
|
1127
|
+
</div>
|
|
1128
|
+
|
|
1129
|
+
{/* Value input */}
|
|
1130
|
+
<div className="space-y-2">
|
|
1131
|
+
<Label className="text-sm font-medium">
|
|
1132
|
+
Value
|
|
1133
|
+
{quantityName && (
|
|
1134
|
+
<span className="ml-2 text-xs text-zinc-400 font-normal">
|
|
1135
|
+
({getQuantityUnit(quantityType)})
|
|
1136
|
+
</span>
|
|
1137
|
+
)}
|
|
1138
|
+
</Label>
|
|
1139
|
+
<Input
|
|
1140
|
+
value={value}
|
|
1141
|
+
onChange={(e) => setValue(e.target.value)}
|
|
1142
|
+
placeholder="Numeric value"
|
|
1143
|
+
type="number"
|
|
1144
|
+
step="any"
|
|
1145
|
+
className="font-mono text-sm"
|
|
1146
|
+
/>
|
|
1147
|
+
</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
<DialogFooter>
|
|
1150
|
+
<Button variant="outline" onClick={() => { setOpen(false); resetForm(); }}>
|
|
1151
|
+
Cancel
|
|
1152
|
+
</Button>
|
|
1153
|
+
<Button onClick={handleSubmit} disabled={!effectiveQtoName || !effectiveQuantityName}>
|
|
1154
|
+
Add Quantity
|
|
1155
|
+
</Button>
|
|
1156
|
+
</DialogFooter>
|
|
1157
|
+
</DialogContent>
|
|
1158
|
+
</Dialog>
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ============================================================================
|
|
1163
|
+
// Edit Toolbar (combines all add actions)
|
|
1164
|
+
// ============================================================================
|
|
1165
|
+
|
|
1166
|
+
interface EditToolbarProps {
|
|
1167
|
+
modelId: string;
|
|
1168
|
+
entityId: number;
|
|
1169
|
+
entityType: string;
|
|
1170
|
+
existingPsets: string[];
|
|
1171
|
+
existingQtos?: string[];
|
|
1172
|
+
schemaVersion?: string;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Edit mode toolbar with dropdown for adding properties, classifications, materials, and quantities.
|
|
1177
|
+
* Schema-aware: filters available property/quantity sets based on entity type.
|
|
1178
|
+
*/
|
|
1179
|
+
export function EditToolbar({ modelId, entityId, entityType, existingPsets, existingQtos, schemaVersion }: EditToolbarProps) {
|
|
1180
|
+
return (
|
|
1181
|
+
<div className="flex items-center justify-between gap-2 mb-3 pb-2 border-b border-purple-200 dark:border-purple-800 bg-purple-50/30 dark:bg-purple-950/20 -mx-3 -mt-3 px-3 pt-3">
|
|
1182
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
1183
|
+
<NewPropertyDialog
|
|
1184
|
+
modelId={modelId}
|
|
1185
|
+
entityId={entityId}
|
|
1186
|
+
entityType={entityType}
|
|
1187
|
+
existingPsets={existingPsets}
|
|
1188
|
+
schemaVersion={schemaVersion}
|
|
1189
|
+
/>
|
|
1190
|
+
<AddQuantityDialog
|
|
1191
|
+
modelId={modelId}
|
|
1192
|
+
entityId={entityId}
|
|
1193
|
+
entityType={entityType}
|
|
1194
|
+
existingQtos={existingQtos ?? []}
|
|
1195
|
+
/>
|
|
1196
|
+
<AddClassificationDialog
|
|
1197
|
+
modelId={modelId}
|
|
1198
|
+
entityId={entityId}
|
|
1199
|
+
entityType={entityType}
|
|
1200
|
+
/>
|
|
1201
|
+
<AddMaterialDialog
|
|
1202
|
+
modelId={modelId}
|
|
1203
|
+
entityId={entityId}
|
|
1204
|
+
entityType={entityType}
|
|
1205
|
+
/>
|
|
1206
|
+
</div>
|
|
1207
|
+
<UndoRedoButtons modelId={modelId} />
|
|
1208
|
+
</div>
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// ============================================================================
|
|
1213
|
+
// Undo/Redo
|
|
1214
|
+
// ============================================================================
|
|
1215
|
+
|
|
430
1216
|
interface UndoRedoButtonsProps {
|
|
431
1217
|
modelId: string;
|
|
432
1218
|
}
|
|
@@ -488,7 +1274,9 @@ export function UndoRedoButtons({ modelId }: UndoRedoButtonsProps) {
|
|
|
488
1274
|
);
|
|
489
1275
|
}
|
|
490
1276
|
|
|
491
|
-
//
|
|
1277
|
+
// ============================================================================
|
|
1278
|
+
// Helper Functions
|
|
1279
|
+
// ============================================================================
|
|
492
1280
|
|
|
493
1281
|
/**
|
|
494
1282
|
* Extract the raw value from typed IFC values.
|
|
@@ -524,7 +1312,7 @@ function formatValue(value: unknown): string {
|
|
|
524
1312
|
|
|
525
1313
|
function formatDisplayValue(value: unknown): string {
|
|
526
1314
|
const raw = extractRawValue(value);
|
|
527
|
-
if (raw === null || raw === undefined) return '
|
|
1315
|
+
if (raw === null || raw === undefined) return '\u2014';
|
|
528
1316
|
if (typeof raw === 'boolean') return raw ? 'True' : 'False';
|
|
529
1317
|
if (typeof raw === 'number') {
|
|
530
1318
|
return Number.isInteger(raw)
|
|
@@ -533,11 +1321,12 @@ function formatDisplayValue(value: unknown): string {
|
|
|
533
1321
|
}
|
|
534
1322
|
if (Array.isArray(raw)) return JSON.stringify(raw);
|
|
535
1323
|
|
|
536
|
-
// Handle boolean strings
|
|
1324
|
+
// Handle boolean strings (STEP enum format)
|
|
537
1325
|
const strVal = String(raw);
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
if (
|
|
1326
|
+
const upper = strVal.toUpperCase();
|
|
1327
|
+
if (upper === '.T.') return 'True';
|
|
1328
|
+
if (upper === '.F.') return 'False';
|
|
1329
|
+
if (upper === '.U.') return 'Unknown';
|
|
541
1330
|
return strVal;
|
|
542
1331
|
}
|
|
543
1332
|
|