@ifc-lite/viewer 1.13.0 → 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 +24 -0
- package/dist/assets/{Arrow.dom-VW5W1XFO.js → Arrow.dom-CNguvlQi.js} +1 -1
- package/dist/assets/{browser-C6mwD6n0.js → browser-D6lgLpkA.js} +1 -1
- package/dist/assets/{index-DQE23JyT.js → index-BMwpw264.js} +4 -4
- package/dist/assets/index-Qp8stcGO.css +1 -0
- package/dist/assets/{index-BzoX4cQC.js → index-UaDsJsCR.js} +24458 -22069
- package/dist/assets/{native-bridge-BibEEmFV.js → native-bridge-DqELq4X0.js} +1 -1
- package/dist/assets/{wasm-bridge-CYzUd3Io.js → wasm-bridge-CVWvHlfH.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/PropertiesPanel.tsx +209 -15
- package/src/components/viewer/properties/BsddCard.tsx +507 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +1 -0
- 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/mutationSlice.ts +155 -1
- package/vite.config.ts +7 -0
- package/dist/assets/index-Cx134arv.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,a as m}from"./index-
|
|
1
|
+
import{I as f,a as m}from"./index-UaDsJsCR.js";class u{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}async processGeometry(s){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),s),n=i.collectMeshes(),r=i.getBuildingRotation();performance.now();let e=0,o=0;for(const c of n)e+=c.positions.length/3,o+=c.indices.length/3;return{meshes:n,totalVertices:e,totalTriangles:o,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:r}}}async processGeometryStreaming(s,i){this.initialized||await this.init();const n=performance.now(),r=new m(this.bridge.getApi(),s);let e=0,o=0,a=0;try{for await(const t of r.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;e+=l.length;for(const d of l)o+=d.positions.length/3,a+=d.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:e,total:e,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const h=performance.now()-n,g={totalMeshes:e,totalVertices:o,totalTriangles:a,parseTimeMs:h*.3,geometryTimeMs:h*.7};return i.onComplete?.(g),g}getApi(){return this.bridge.getApi()}}export{u as WasmBridge};
|
package/dist/index.html
CHANGED
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
<meta name="theme-color" content="#7aa2f7">
|
|
45
45
|
<meta name="msapplication-TileColor" content="#1a1b26">
|
|
46
46
|
<meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
|
|
47
|
-
<script type="module" crossorigin src="/assets/index-
|
|
48
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-UaDsJsCR.js"></script>
|
|
48
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Qp8stcGO.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
|
51
51
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -41,24 +41,24 @@
|
|
|
41
41
|
"tailwind-merge": "^3.4.0",
|
|
42
42
|
"tailwindcss": "^4.1.18",
|
|
43
43
|
"zustand": "^4.4.0",
|
|
44
|
-
"@ifc-lite/bcf": "^1.
|
|
45
|
-
"@ifc-lite/cache": "^1.
|
|
46
|
-
"@ifc-lite/data": "^1.
|
|
47
|
-
"@ifc-lite/drawing-2d": "^1.
|
|
48
|
-
"@ifc-lite/encoding": "^1.
|
|
49
|
-
"@ifc-lite/export": "^1.
|
|
50
|
-
"@ifc-lite/geometry": "^1.
|
|
51
|
-
"@ifc-lite/ids": "^1.
|
|
52
|
-
"@ifc-lite/lens": "^1.
|
|
53
|
-
"@ifc-lite/lists": "^1.
|
|
54
|
-
"@ifc-lite/mutations": "^1.
|
|
55
|
-
"@ifc-lite/parser": "^1.
|
|
56
|
-
"@ifc-lite/query": "^1.
|
|
57
|
-
"@ifc-lite/renderer": "^1.
|
|
58
|
-
"@ifc-lite/sandbox": "^1.
|
|
59
|
-
"@ifc-lite/server-client": "^1.
|
|
60
|
-
"@ifc-lite/spatial": "^1.
|
|
61
|
-
"@ifc-lite/wasm": "^1.
|
|
44
|
+
"@ifc-lite/bcf": "^1.14.0",
|
|
45
|
+
"@ifc-lite/cache": "^1.14.0",
|
|
46
|
+
"@ifc-lite/data": "^1.14.0",
|
|
47
|
+
"@ifc-lite/drawing-2d": "^1.14.0",
|
|
48
|
+
"@ifc-lite/encoding": "^1.14.0",
|
|
49
|
+
"@ifc-lite/export": "^1.14.0",
|
|
50
|
+
"@ifc-lite/geometry": "^1.14.0",
|
|
51
|
+
"@ifc-lite/ids": "^1.14.0",
|
|
52
|
+
"@ifc-lite/lens": "^1.14.0",
|
|
53
|
+
"@ifc-lite/lists": "^1.14.0",
|
|
54
|
+
"@ifc-lite/mutations": "^1.14.0",
|
|
55
|
+
"@ifc-lite/parser": "^1.14.0",
|
|
56
|
+
"@ifc-lite/query": "^1.14.0",
|
|
57
|
+
"@ifc-lite/renderer": "^1.14.0",
|
|
58
|
+
"@ifc-lite/sandbox": "^1.14.0",
|
|
59
|
+
"@ifc-lite/server-client": "^1.14.0",
|
|
60
|
+
"@ifc-lite/spatial": "^1.14.0",
|
|
61
|
+
"@ifc-lite/wasm": "^1.14.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@tailwindcss/postcss": "^4.1.18",
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
type BulkQueryPreview,
|
|
62
62
|
type BulkQueryResult,
|
|
63
63
|
} from '@ifc-lite/mutations';
|
|
64
|
-
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
64
|
+
import { extractPropertiesOnDemand, extractQuantitiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
65
65
|
|
|
66
66
|
// Common IFC type enum IDs (from IFC schema)
|
|
67
67
|
// These correspond to the typeEnum values in EntityTable
|
|
@@ -245,6 +245,13 @@ export function BulkPropertyEditor({ trigger }: BulkPropertyEditorProps) {
|
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// Set up on-demand quantity extraction if the data store supports it
|
|
249
|
+
if (dataStore.onDemandQuantityMap && dataStore.source?.length > 0) {
|
|
250
|
+
mutationView.setQuantityExtractor((entityId: number) => {
|
|
251
|
+
return extractQuantitiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
248
255
|
// Register the mutation view
|
|
249
256
|
registerMutationView(selectedModelId, mutationView);
|
|
250
257
|
}, [selectedModel, selectedModelId, getMutationView, registerMutationView]);
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
type MatchResult,
|
|
72
72
|
type ImportStats,
|
|
73
73
|
} from '@ifc-lite/mutations';
|
|
74
|
-
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
74
|
+
import { extractPropertiesOnDemand, extractQuantitiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
75
75
|
|
|
76
76
|
type MatchType = 'globalId' | 'expressId' | 'name' | 'property';
|
|
77
77
|
|
|
@@ -187,6 +187,13 @@ export function DataConnector({ trigger }: DataConnectorProps) {
|
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// Set up on-demand quantity extraction if the data store supports it
|
|
191
|
+
if (dataStore.onDemandQuantityMap && dataStore.source?.length > 0) {
|
|
192
|
+
mutationView.setQuantityExtractor((entityId: number) => {
|
|
193
|
+
return extractQuantitiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
190
197
|
// Register the mutation view
|
|
191
198
|
registerMutationView(selectedModelId, mutationView);
|
|
192
199
|
}, [selectedModel, selectedModelId, getMutationView, registerMutationView]);
|
|
@@ -15,7 +15,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
|
|
15
15
|
import { useViewerStore } from '@/store';
|
|
16
16
|
import { StepExporter } from '@ifc-lite/export';
|
|
17
17
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
18
|
-
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
18
|
+
import { extractPropertiesOnDemand, extractQuantitiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
19
19
|
import { toast } from '@/components/ui/toast';
|
|
20
20
|
|
|
21
21
|
interface ExportChangesButtonProps {
|
|
@@ -87,6 +87,13 @@ export function ExportChangesButton({ className }: ExportChangesButtonProps) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Set up on-demand quantity extraction
|
|
91
|
+
if (dataStore.onDemandQuantityMap && dataStore.source?.length > 0) {
|
|
92
|
+
mutationView.setQuantityExtractor((entityId: number) => {
|
|
93
|
+
return extractQuantitiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
registerMutationView(modelInfo.id, mutationView);
|
|
91
98
|
}, [modelInfo, getMutationView, registerMutationView]);
|
|
92
99
|
|
|
@@ -52,7 +52,7 @@ import { useViewerStore } from '@/store';
|
|
|
52
52
|
import { toast } from '@/components/ui/toast';
|
|
53
53
|
import { StepExporter, MergedExporter, Ifc5Exporter, type MergeModelInput } from '@ifc-lite/export';
|
|
54
54
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
55
|
-
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
55
|
+
import { extractPropertiesOnDemand, extractQuantitiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
56
56
|
|
|
57
57
|
type ExportScope = 'single' | 'merged';
|
|
58
58
|
type SchemaVersion = 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5';
|
|
@@ -154,6 +154,13 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// Set up on-demand quantity extraction if the data store supports it
|
|
158
|
+
if (dataStore.onDemandQuantityMap && dataStore.source?.length > 0) {
|
|
159
|
+
mutationView.setQuantityExtractor((entityId: number) => {
|
|
160
|
+
return extractQuantitiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
// Register the mutation view
|
|
158
165
|
registerMutationView(selectedModelId, mutationView);
|
|
159
166
|
}, [selectedModel, selectedModelId, getMutationView, registerMutationView]);
|
|
@@ -30,7 +30,7 @@ import { useViewerStore } from '@/store';
|
|
|
30
30
|
import { useIfc } from '@/hooks/useIfc';
|
|
31
31
|
import { IfcQuery } from '@ifc-lite/query';
|
|
32
32
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
33
|
-
import { extractPropertiesOnDemand, extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractDocumentsOnDemand, extractRelationshipsOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
33
|
+
import { extractPropertiesOnDemand, extractQuantitiesOnDemand, extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractDocumentsOnDemand, extractRelationshipsOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
34
34
|
import type { EntityRef, FederatedModel } from '@/store/types';
|
|
35
35
|
|
|
36
36
|
import { CoordVal, CoordRow } from './properties/CoordinateDisplay';
|
|
@@ -42,6 +42,7 @@ import { MaterialCard } from './properties/MaterialCard';
|
|
|
42
42
|
import { DocumentCard } from './properties/DocumentCard';
|
|
43
43
|
import { RelationshipsCard } from './properties/RelationshipsCard';
|
|
44
44
|
import type { PropertySet, QuantitySet } from './properties/encodingUtils';
|
|
45
|
+
import { BsddCard } from './properties/BsddCard';
|
|
45
46
|
|
|
46
47
|
export function PropertiesPanel() {
|
|
47
48
|
const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
|
|
@@ -97,6 +98,13 @@ export function PropertiesPanel() {
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Set up on-demand quantity extractor if available
|
|
102
|
+
if (dataStore.onDemandQuantityMap && dataStore.source?.length > 0) {
|
|
103
|
+
mutationView.setQuantityExtractor((entityId: number) => {
|
|
104
|
+
return extractQuantitiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
registerMutationView(modelId, mutationView);
|
|
101
109
|
}, [model, selectedEntity, getMutationView, registerMutationView]);
|
|
102
110
|
|
|
@@ -367,17 +375,56 @@ export function PropertiesPanel() {
|
|
|
367
375
|
}, [entityNode, selectedEntity, mutationViews, mutationVersion]);
|
|
368
376
|
|
|
369
377
|
const quantities: QuantitySet[] = useMemo(() => {
|
|
378
|
+
let modelId = selectedEntity?.modelId;
|
|
379
|
+
const expressId = selectedEntity?.expressId;
|
|
380
|
+
|
|
381
|
+
if (modelId === 'legacy') modelId = '__legacy__';
|
|
382
|
+
|
|
383
|
+
// Try mutation view first to include added quantities from bSDD
|
|
384
|
+
const mutationView = modelId ? mutationViews.get(modelId) : null;
|
|
385
|
+
if (mutationView && expressId) {
|
|
386
|
+
const merged = mutationView.getQuantitiesForEntity(expressId);
|
|
387
|
+
if (merged.length > 0) return merged;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Fallback to entity node quantities
|
|
370
391
|
if (!entityNode) return [];
|
|
371
392
|
return entityNode.quantities();
|
|
372
|
-
}, [entityNode]);
|
|
393
|
+
}, [entityNode, selectedEntity, mutationViews, mutationVersion]);
|
|
373
394
|
|
|
374
395
|
// Build attributes array for display - must be before early return to maintain hook order
|
|
375
396
|
// Uses schema-aware extraction to show ALL string/enum attributes for the entity type.
|
|
397
|
+
// Merges mutated attributes (from bSDD) into the base attribute list.
|
|
376
398
|
// Note: GlobalId is intentionally excluded since it's shown in the dedicated GUID field above
|
|
377
399
|
const attributes = useMemo(() => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
400
|
+
const base = entityNode ? entityNode.allAttributes() : [];
|
|
401
|
+
|
|
402
|
+
// Merge mutated attributes from bSDD
|
|
403
|
+
let modelId = selectedEntity?.modelId;
|
|
404
|
+
const expressId = selectedEntity?.expressId;
|
|
405
|
+
if (modelId === 'legacy') modelId = '__legacy__';
|
|
406
|
+
const mutationView = modelId ? mutationViews.get(modelId) : null;
|
|
407
|
+
if (mutationView && expressId) {
|
|
408
|
+
const mutatedAttrs = mutationView.getAttributeMutationsForEntity(expressId);
|
|
409
|
+
if (mutatedAttrs.length > 0) {
|
|
410
|
+
const baseNames = new Set(base.map(a => a.name));
|
|
411
|
+
const merged = [...base];
|
|
412
|
+
for (const ma of mutatedAttrs) {
|
|
413
|
+
if (baseNames.has(ma.name)) {
|
|
414
|
+
// Update existing attribute value
|
|
415
|
+
const idx = merged.findIndex(a => a.name === ma.name);
|
|
416
|
+
if (idx >= 0) merged[idx] = { name: ma.name, value: ma.value };
|
|
417
|
+
} else {
|
|
418
|
+
// Add new attribute
|
|
419
|
+
merged.push({ name: ma.name, value: ma.value });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return merged;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return base;
|
|
427
|
+
}, [entityNode, selectedEntity, mutationViews, mutationVersion]);
|
|
381
428
|
|
|
382
429
|
// Extract classifications for the selected entity from the IFC data store
|
|
383
430
|
const classifications = useMemo(() => {
|
|
@@ -545,6 +592,37 @@ export function PropertiesPanel() {
|
|
|
545
592
|
return result;
|
|
546
593
|
}, [properties, typeProperties]);
|
|
547
594
|
|
|
595
|
+
// Build a set of existing property keys ("PsetName:PropName") for bSDD deduplication
|
|
596
|
+
const existingProps = useMemo(() => {
|
|
597
|
+
const keys = new Set<string>();
|
|
598
|
+
for (const pset of mergedProperties) {
|
|
599
|
+
for (const prop of pset.properties) {
|
|
600
|
+
keys.add(`${pset.name}:${prop.name}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return keys;
|
|
604
|
+
}, [mergedProperties]);
|
|
605
|
+
|
|
606
|
+
// Build a set of existing quantity keys ("QsetName:QuantName") for bSDD deduplication
|
|
607
|
+
const existingQuants = useMemo(() => {
|
|
608
|
+
const keys = new Set<string>();
|
|
609
|
+
for (const qset of quantities) {
|
|
610
|
+
for (const q of qset.quantities) {
|
|
611
|
+
keys.add(`${qset.name}:${q.name}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return keys;
|
|
615
|
+
}, [quantities]);
|
|
616
|
+
|
|
617
|
+
// Build a set of existing attribute names for bSDD deduplication
|
|
618
|
+
const existingAttributeNames = useMemo(() => {
|
|
619
|
+
const names = new Set<string>();
|
|
620
|
+
for (const attr of attributes) {
|
|
621
|
+
if (attr.value) names.add(attr.name);
|
|
622
|
+
}
|
|
623
|
+
return names;
|
|
624
|
+
}, [attributes]);
|
|
625
|
+
|
|
548
626
|
// Model metadata display (when clicking top-level model in hierarchy)
|
|
549
627
|
if (selectedModelId) {
|
|
550
628
|
const selectedModel = models.get(selectedModelId);
|
|
@@ -795,6 +873,7 @@ export function PropertiesPanel() {
|
|
|
795
873
|
<CollapsibleTrigger className="flex items-center gap-2 w-full p-3 hover:bg-muted/50 text-left">
|
|
796
874
|
<Tag className="h-4 w-4 text-muted-foreground" />
|
|
797
875
|
<span className="font-medium text-sm">Attributes</span>
|
|
876
|
+
{editMode && <PenLine className="h-3 w-3 text-purple-500 ml-1" />}
|
|
798
877
|
<span className="text-xs text-muted-foreground ml-auto">{attributes.length}</span>
|
|
799
878
|
</CollapsibleTrigger>
|
|
800
879
|
<CollapsibleContent>
|
|
@@ -802,11 +881,20 @@ export function PropertiesPanel() {
|
|
|
802
881
|
{attributes.map((attr) => (
|
|
803
882
|
<div key={attr.name} className="grid grid-cols-[minmax(80px,1fr)_minmax(0,2fr)] gap-2 px-3 py-1.5 text-sm">
|
|
804
883
|
<span className="text-muted-foreground truncate" title={attr.name}>{attr.name}</span>
|
|
805
|
-
|
|
806
|
-
<
|
|
807
|
-
{
|
|
808
|
-
|
|
809
|
-
|
|
884
|
+
{editMode && selectedEntity ? (
|
|
885
|
+
<AttributeEditorField
|
|
886
|
+
modelId={selectedEntity.modelId}
|
|
887
|
+
entityId={selectedEntity.expressId}
|
|
888
|
+
attrName={attr.name}
|
|
889
|
+
currentValue={attr.value}
|
|
890
|
+
/>
|
|
891
|
+
) : (
|
|
892
|
+
<div className="overflow-x-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 min-w-0">
|
|
893
|
+
<span className="font-medium whitespace-nowrap" title={attr.value}>
|
|
894
|
+
{attr.value}
|
|
895
|
+
</span>
|
|
896
|
+
</div>
|
|
897
|
+
)}
|
|
810
898
|
</div>
|
|
811
899
|
))}
|
|
812
900
|
</div>
|
|
@@ -837,21 +925,25 @@ export function PropertiesPanel() {
|
|
|
837
925
|
|
|
838
926
|
{/* Tabs */}
|
|
839
927
|
<Tabs defaultValue="properties" className="flex-1 flex flex-col overflow-hidden">
|
|
840
|
-
<TabsList className="tabs-list w-full justify-start rounded-none h-
|
|
928
|
+
<TabsList className="tabs-list w-full justify-start rounded-none h-9 p-0 shrink-0 flex" style={{ backgroundColor: 'var(--tabs-bg)', borderBottom: '1px solid var(--tabs-border)' }}>
|
|
841
929
|
<TabsTrigger
|
|
842
930
|
value="properties"
|
|
843
|
-
className="tab-trigger flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-primary uppercase text-
|
|
931
|
+
className="tab-trigger flex-1 min-w-0 rounded-none border-b-2 border-transparent data-[state=active]:border-primary uppercase text-[11px] tracking-wide h-full px-2"
|
|
844
932
|
>
|
|
845
|
-
<FileText className="h-3.5 w-3.5 mr-2" />
|
|
846
933
|
Properties
|
|
847
934
|
</TabsTrigger>
|
|
848
935
|
<TabsTrigger
|
|
849
936
|
value="quantities"
|
|
850
|
-
className="tab-trigger flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-primary uppercase text-
|
|
937
|
+
className="tab-trigger flex-1 min-w-0 rounded-none border-b-2 border-transparent data-[state=active]:border-primary uppercase text-[11px] tracking-wide h-full px-2"
|
|
851
938
|
>
|
|
852
|
-
<Calculator className="h-3.5 w-3.5 mr-2" />
|
|
853
939
|
Quantities
|
|
854
940
|
</TabsTrigger>
|
|
941
|
+
<TabsTrigger
|
|
942
|
+
value="bsdd"
|
|
943
|
+
className="tab-trigger flex-1 min-w-0 rounded-none border-b-2 border-transparent data-[state=active]:border-primary uppercase text-[11px] tracking-wide h-full px-2"
|
|
944
|
+
>
|
|
945
|
+
bSDD
|
|
946
|
+
</TabsTrigger>
|
|
855
947
|
</TabsList>
|
|
856
948
|
|
|
857
949
|
<ScrollArea className="flex-1 bg-white dark:bg-black">
|
|
@@ -944,12 +1036,114 @@ export function PropertiesPanel() {
|
|
|
944
1036
|
</div>
|
|
945
1037
|
)}
|
|
946
1038
|
</TabsContent>
|
|
1039
|
+
|
|
1040
|
+
<TabsContent value="bsdd" className="m-0 p-3 overflow-hidden">
|
|
1041
|
+
{selectedEntity && (
|
|
1042
|
+
<BsddCard
|
|
1043
|
+
entityType={entityType}
|
|
1044
|
+
modelId={selectedEntity.modelId}
|
|
1045
|
+
entityId={selectedEntity.expressId}
|
|
1046
|
+
existingPsets={mergedProperties.map(p => p.name)}
|
|
1047
|
+
existingProps={existingProps}
|
|
1048
|
+
existingQsets={quantities.map(q => q.name)}
|
|
1049
|
+
existingQuants={existingQuants}
|
|
1050
|
+
existingAttributes={existingAttributeNames}
|
|
1051
|
+
/>
|
|
1052
|
+
)}
|
|
1053
|
+
</TabsContent>
|
|
947
1054
|
</ScrollArea>
|
|
948
1055
|
</Tabs>
|
|
949
1056
|
</div>
|
|
950
1057
|
);
|
|
951
1058
|
}
|
|
952
1059
|
|
|
1060
|
+
/** Inline attribute editor — pen icon to enter edit mode, input + save/cancel */
|
|
1061
|
+
function AttributeEditorField({
|
|
1062
|
+
modelId,
|
|
1063
|
+
entityId,
|
|
1064
|
+
attrName,
|
|
1065
|
+
currentValue,
|
|
1066
|
+
}: {
|
|
1067
|
+
modelId: string;
|
|
1068
|
+
entityId: number;
|
|
1069
|
+
attrName: string;
|
|
1070
|
+
currentValue: string;
|
|
1071
|
+
}) {
|
|
1072
|
+
const setAttribute = useViewerStore((s) => s.setAttribute);
|
|
1073
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
1074
|
+
const [editing, setEditing] = useState(false);
|
|
1075
|
+
const [value, setValue] = useState(currentValue);
|
|
1076
|
+
const inputRef = useCallback((node: HTMLInputElement | null) => {
|
|
1077
|
+
if (node) { node.focus(); node.select(); }
|
|
1078
|
+
}, []);
|
|
1079
|
+
|
|
1080
|
+
const save = useCallback(() => {
|
|
1081
|
+
let normalizedModelId = modelId;
|
|
1082
|
+
if (modelId === 'legacy') normalizedModelId = '__legacy__';
|
|
1083
|
+
setAttribute(normalizedModelId, entityId, attrName, value, currentValue || undefined);
|
|
1084
|
+
bumpMutationVersion();
|
|
1085
|
+
setEditing(false);
|
|
1086
|
+
}, [modelId, entityId, attrName, value, currentValue, setAttribute, bumpMutationVersion]);
|
|
1087
|
+
|
|
1088
|
+
const cancel = useCallback(() => {
|
|
1089
|
+
setValue(currentValue);
|
|
1090
|
+
setEditing(false);
|
|
1091
|
+
}, [currentValue]);
|
|
1092
|
+
|
|
1093
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
1094
|
+
if (e.key === 'Enter') { e.preventDefault(); save(); }
|
|
1095
|
+
else if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
|
1096
|
+
}, [save, cancel]);
|
|
1097
|
+
|
|
1098
|
+
if (editing) {
|
|
1099
|
+
return (
|
|
1100
|
+
<div className="flex items-center gap-1 min-w-0">
|
|
1101
|
+
<input
|
|
1102
|
+
ref={inputRef}
|
|
1103
|
+
value={value}
|
|
1104
|
+
onChange={(e) => setValue(e.target.value)}
|
|
1105
|
+
onKeyDown={handleKeyDown}
|
|
1106
|
+
onBlur={save}
|
|
1107
|
+
className="flex-1 min-w-0 h-6 px-1.5 text-sm font-mono bg-white dark:bg-zinc-900 border border-purple-300 dark:border-purple-700 outline-none focus:ring-1 focus:ring-purple-400"
|
|
1108
|
+
/>
|
|
1109
|
+
<Button
|
|
1110
|
+
variant="ghost"
|
|
1111
|
+
size="icon"
|
|
1112
|
+
className="h-5 w-5 p-0 shrink-0 hover:bg-emerald-100 dark:hover:bg-emerald-900/30"
|
|
1113
|
+
onClick={save}
|
|
1114
|
+
>
|
|
1115
|
+
<Check className="h-3 w-3 text-emerald-500" />
|
|
1116
|
+
</Button>
|
|
1117
|
+
</div>
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return (
|
|
1122
|
+
<div className="flex items-center gap-1 min-w-0 group/attr">
|
|
1123
|
+
<span
|
|
1124
|
+
className="font-medium whitespace-nowrap truncate flex-1 min-w-0 cursor-text"
|
|
1125
|
+
title={currentValue}
|
|
1126
|
+
onClick={() => setEditing(true)}
|
|
1127
|
+
>
|
|
1128
|
+
{currentValue || <span className="text-zinc-400 italic">empty</span>}
|
|
1129
|
+
</span>
|
|
1130
|
+
<Tooltip>
|
|
1131
|
+
<TooltipTrigger asChild>
|
|
1132
|
+
<Button
|
|
1133
|
+
variant="ghost"
|
|
1134
|
+
size="icon"
|
|
1135
|
+
className="h-5 w-5 p-0 shrink-0 opacity-0 group-hover/attr:opacity-100 hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-opacity"
|
|
1136
|
+
onClick={() => setEditing(true)}
|
|
1137
|
+
>
|
|
1138
|
+
<PenLine className="h-3 w-3 text-purple-500" />
|
|
1139
|
+
</Button>
|
|
1140
|
+
</TooltipTrigger>
|
|
1141
|
+
<TooltipContent side="left">Edit attribute</TooltipContent>
|
|
1142
|
+
</Tooltip>
|
|
1143
|
+
</div>
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
953
1147
|
/** Multi-entity panel for unified storeys - shows data from multiple entities stacked */
|
|
954
1148
|
function MultiEntityPanel({
|
|
955
1149
|
entities,
|