@ifc-lite/viewer 1.1.7 → 1.6.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/LICENSE +373 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Arrow.dom-BjDQoB2M.js +20 -0
- package/dist/assets/arrow2-bb-jcVEo.js +2 -0
- package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
- package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
- package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
- package/dist/assets/event-DIOks52T.js +1 -0
- package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-YBtrHPu3.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-CULtTDX3.js +111 -0
- package/dist/assets/wasm-bridge-CjL-lSak.js +1 -0
- package/dist/favicon-16x16-cropped.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-192x192-cropped.png +0 -0
- package/dist/favicon-192x192.png +0 -0
- package/dist/favicon-32x32-cropped.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48-cropped.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon-512x512-cropped.png +0 -0
- package/dist/favicon-512x512.png +0 -0
- package/dist/favicon-64x64-cropped.png +0 -0
- package/dist/favicon-64x64.png +0 -0
- package/dist/favicon-96x96-cropped.png +0 -0
- package/dist/favicon-96x96.png +0 -0
- package/dist/favicon-square-512.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +3 -0
- package/dist/index.html +44 -0
- package/dist/logo.png +0 -0
- package/dist/manifest.json +48 -0
- package/index.html +33 -2
- package/package.json +34 -17
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16-cropped.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-192x192-cropped.png +0 -0
- package/public/favicon-192x192.png +0 -0
- package/public/favicon-32x32-cropped.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48-cropped.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon-512x512-cropped.png +0 -0
- package/public/favicon-512x512.png +0 -0
- package/public/favicon-64x64-cropped.png +0 -0
- package/public/favicon-64x64.png +0 -0
- package/public/favicon-96x96-cropped.png +0 -0
- package/public/favicon-96x96.png +0 -0
- package/public/favicon-square-512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +3 -0
- package/public/logo.png +0 -0
- package/public/manifest.json +48 -0
- package/src/App.tsx +2 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/BCFPanel.tsx +1164 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
- package/src/components/viewer/DataConnector.tsx +840 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
- package/src/components/viewer/EntityContextMenu.tsx +45 -17
- package/src/components/viewer/ExportChangesButton.tsx +195 -0
- package/src/components/viewer/ExportDialog.tsx +402 -0
- package/src/components/viewer/HierarchyPanel.tsx +1132 -218
- package/src/components/viewer/IDSPanel.tsx +661 -0
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
- package/src/components/viewer/MainToolbar.tsx +418 -94
- package/src/components/viewer/PropertiesPanel.tsx +1355 -91
- package/src/components/viewer/PropertyEditor.tsx +611 -0
- package/src/components/viewer/Section2DPanel.tsx +3313 -0
- package/src/components/viewer/SheetSetupPanel.tsx +502 -0
- package/src/components/viewer/StatusBar.tsx +27 -16
- package/src/components/viewer/TitleBlockEditor.tsx +437 -0
- package/src/components/viewer/ToolOverlays.tsx +935 -127
- package/src/components/viewer/ViewerLayout.tsx +40 -11
- package/src/components/viewer/Viewport.tsx +1276 -336
- package/src/components/viewer/ViewportContainer.tsx +554 -18
- package/src/components/viewer/ViewportOverlays.tsx +24 -7
- package/src/hooks/useBCF.ts +504 -0
- package/src/hooks/useIDS.ts +1065 -0
- package/src/hooks/useIfc.ts +1534 -205
- package/src/hooks/useIfcCache.ts +279 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -8
- package/src/hooks/useModelSelection.ts +61 -0
- package/src/hooks/useViewerSelectors.ts +218 -0
- package/src/hooks/useWebGPU.ts +80 -0
- package/src/index.css +265 -27
- package/src/lib/platform.ts +23 -0
- package/src/services/cacheService.ts +142 -0
- package/src/services/desktop-cache.ts +143 -0
- package/src/services/fs-cache.ts +212 -0
- package/src/services/ifc-cache.ts +14 -6
- package/src/store/constants.ts +85 -0
- package/src/store/index.ts +214 -0
- package/src/store/slices/bcfSlice.ts +372 -0
- package/src/store/slices/cameraSlice.ts +63 -0
- package/src/store/slices/dataSlice.test.ts +226 -0
- package/src/store/slices/dataSlice.ts +112 -0
- package/src/store/slices/drawing2DSlice.ts +340 -0
- package/src/store/slices/hoverSlice.ts +40 -0
- package/src/store/slices/idsSlice.ts +310 -0
- package/src/store/slices/loadingSlice.ts +33 -0
- package/src/store/slices/measurementSlice.test.ts +217 -0
- package/src/store/slices/measurementSlice.ts +293 -0
- package/src/store/slices/modelSlice.test.ts +271 -0
- package/src/store/slices/modelSlice.ts +211 -0
- package/src/store/slices/mutationSlice.ts +502 -0
- package/src/store/slices/sectionSlice.test.ts +125 -0
- package/src/store/slices/sectionSlice.ts +58 -0
- package/src/store/slices/selectionSlice.test.ts +286 -0
- package/src/store/slices/selectionSlice.ts +263 -0
- package/src/store/slices/sheetSlice.ts +565 -0
- package/src/store/slices/uiSlice.ts +58 -0
- package/src/store/slices/visibilitySlice.test.ts +304 -0
- package/src/store/slices/visibilitySlice.ts +277 -0
- package/src/store/types.test.ts +135 -0
- package/src/store/types.ts +248 -0
- package/src/store.ts +40 -515
- package/src/utils/ifcConfig.ts +82 -0
- package/src/utils/localParsingUtils.ts +287 -0
- package/src/utils/serverDataModel.ts +783 -0
- package/src/utils/spatialHierarchy.ts +283 -0
- package/src/utils/viewportUtils.ts +334 -0
- package/src/vite-env.d.ts +23 -0
- package/src/webgpu-types.d.ts +128 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +7 -0
- package/src-tauri/capabilities/default.json +18 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +21 -0
- package/src-tauri/src/main.rs +10 -0
- package/src-tauri/tauri.conf.json +39 -0
- package/vite.config.ts +174 -26
- package/public/ifc-lite_bg.wasm +0 -0
- package/public/web-ifc.wasm +0 -0
- package/src/components/Viewport.tsx +0 -723
- package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
|
@@ -0,0 +1,611 @@
|
|
|
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
|
+
* Property Editor component for editing IFC property values inline.
|
|
7
|
+
* Production-ready with keyboard support and proper UX.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
11
|
+
import {
|
|
12
|
+
Save,
|
|
13
|
+
X,
|
|
14
|
+
Plus,
|
|
15
|
+
Trash2,
|
|
16
|
+
PenLine,
|
|
17
|
+
Undo,
|
|
18
|
+
Redo,
|
|
19
|
+
Check,
|
|
20
|
+
} from 'lucide-react';
|
|
21
|
+
import { Button } from '@/components/ui/button';
|
|
22
|
+
import { Input } from '@/components/ui/input';
|
|
23
|
+
import { Label } from '@/components/ui/label';
|
|
24
|
+
import {
|
|
25
|
+
Select,
|
|
26
|
+
SelectContent,
|
|
27
|
+
SelectItem,
|
|
28
|
+
SelectTrigger,
|
|
29
|
+
SelectValue,
|
|
30
|
+
} from '@/components/ui/select';
|
|
31
|
+
import {
|
|
32
|
+
Dialog,
|
|
33
|
+
DialogContent,
|
|
34
|
+
DialogDescription,
|
|
35
|
+
DialogFooter,
|
|
36
|
+
DialogHeader,
|
|
37
|
+
DialogTitle,
|
|
38
|
+
DialogTrigger,
|
|
39
|
+
} from '@/components/ui/dialog';
|
|
40
|
+
import { Switch } from '@/components/ui/switch';
|
|
41
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
42
|
+
import { useViewerStore } from '@/store';
|
|
43
|
+
import { PropertyValueType } from '@ifc-lite/data';
|
|
44
|
+
import type { PropertyValue } from '@ifc-lite/mutations';
|
|
45
|
+
|
|
46
|
+
interface PropertyEditorProps {
|
|
47
|
+
modelId: string;
|
|
48
|
+
entityId: number;
|
|
49
|
+
psetName: string;
|
|
50
|
+
propName: string;
|
|
51
|
+
currentValue: unknown;
|
|
52
|
+
currentType?: PropertyValueType;
|
|
53
|
+
onClose?: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inline property value editor with pen icon on the right.
|
|
58
|
+
* Supports keyboard: Enter to save, Escape to cancel.
|
|
59
|
+
*/
|
|
60
|
+
export function PropertyEditor({
|
|
61
|
+
modelId,
|
|
62
|
+
entityId,
|
|
63
|
+
psetName,
|
|
64
|
+
propName,
|
|
65
|
+
currentValue,
|
|
66
|
+
currentType = PropertyValueType.String,
|
|
67
|
+
onClose,
|
|
68
|
+
}: PropertyEditorProps) {
|
|
69
|
+
const setProperty = useViewerStore((s) => s.setProperty);
|
|
70
|
+
const deleteProperty = useViewerStore((s) => s.deleteProperty);
|
|
71
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
72
|
+
|
|
73
|
+
const [value, setValue] = useState<string>(formatValue(currentValue));
|
|
74
|
+
const [valueType, setValueType] = useState<PropertyValueType>(detectValueType(currentValue, currentType));
|
|
75
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
76
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
77
|
+
|
|
78
|
+
// Focus input when entering edit mode
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (isEditing && inputRef.current) {
|
|
81
|
+
inputRef.current.focus();
|
|
82
|
+
inputRef.current.select();
|
|
83
|
+
}
|
|
84
|
+
}, [isEditing]);
|
|
85
|
+
|
|
86
|
+
const handleSave = useCallback(() => {
|
|
87
|
+
const parsedValue = parseValue(value, valueType);
|
|
88
|
+
|
|
89
|
+
// Normalize model ID for legacy models
|
|
90
|
+
let normalizedModelId = modelId;
|
|
91
|
+
if (modelId === 'legacy') {
|
|
92
|
+
normalizedModelId = '__legacy__';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setProperty(normalizedModelId, entityId, psetName, propName, parsedValue, valueType);
|
|
96
|
+
bumpMutationVersion();
|
|
97
|
+
setIsEditing(false);
|
|
98
|
+
onClose?.();
|
|
99
|
+
}, [modelId, entityId, psetName, propName, value, valueType, setProperty, bumpMutationVersion, onClose]);
|
|
100
|
+
|
|
101
|
+
const handleDelete = useCallback(() => {
|
|
102
|
+
// Normalize model ID for legacy models
|
|
103
|
+
let normalizedModelId = modelId;
|
|
104
|
+
if (modelId === 'legacy') {
|
|
105
|
+
normalizedModelId = '__legacy__';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
deleteProperty(normalizedModelId, entityId, psetName, propName);
|
|
109
|
+
bumpMutationVersion();
|
|
110
|
+
setIsEditing(false);
|
|
111
|
+
onClose?.();
|
|
112
|
+
}, [modelId, entityId, psetName, propName, deleteProperty, bumpMutationVersion, onClose]);
|
|
113
|
+
|
|
114
|
+
const handleCancel = useCallback(() => {
|
|
115
|
+
setValue(formatValue(currentValue));
|
|
116
|
+
setValueType(detectValueType(currentValue, currentType));
|
|
117
|
+
setIsEditing(false);
|
|
118
|
+
onClose?.();
|
|
119
|
+
}, [currentValue, currentType, onClose]);
|
|
120
|
+
|
|
121
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
122
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
handleSave();
|
|
125
|
+
} else if (e.key === 'Escape') {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
handleCancel();
|
|
128
|
+
}
|
|
129
|
+
}, [handleSave, handleCancel]);
|
|
130
|
+
|
|
131
|
+
const displayValue = formatDisplayValue(currentValue);
|
|
132
|
+
|
|
133
|
+
// Non-editing view: value with pen icon on right (always visible)
|
|
134
|
+
if (!isEditing) {
|
|
135
|
+
return (
|
|
136
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
137
|
+
<span
|
|
138
|
+
className="font-mono text-zinc-900 dark:text-zinc-100 select-all break-words flex-1 min-w-0 cursor-text"
|
|
139
|
+
onClick={() => setIsEditing(true)}
|
|
140
|
+
title="Click to edit"
|
|
141
|
+
>
|
|
142
|
+
{displayValue}
|
|
143
|
+
</span>
|
|
144
|
+
<Tooltip>
|
|
145
|
+
<TooltipTrigger asChild>
|
|
146
|
+
<Button
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="icon"
|
|
149
|
+
className="h-5 w-5 shrink-0 hover:bg-purple-100 dark:hover:bg-purple-900/30"
|
|
150
|
+
onClick={() => setIsEditing(true)}
|
|
151
|
+
>
|
|
152
|
+
<PenLine className="h-3 w-3 text-purple-500" />
|
|
153
|
+
</Button>
|
|
154
|
+
</TooltipTrigger>
|
|
155
|
+
<TooltipContent side="left">Edit property</TooltipContent>
|
|
156
|
+
</Tooltip>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Editing view: inline input with type selector and action buttons
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex flex-col gap-2 p-2 -mx-2 bg-purple-50/50 dark:bg-purple-950/30 border border-purple-200 dark:border-purple-800 rounded">
|
|
164
|
+
{/* Value input */}
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
{valueType === PropertyValueType.Boolean || valueType === PropertyValueType.Logical ? (
|
|
167
|
+
<div className="flex items-center gap-2 flex-1">
|
|
168
|
+
<Switch
|
|
169
|
+
checked={value === 'true'}
|
|
170
|
+
onCheckedChange={(checked) => setValue(checked ? 'true' : 'false')}
|
|
171
|
+
/>
|
|
172
|
+
<span className="text-xs text-zinc-500">{value === 'true' ? 'True' : 'False'}</span>
|
|
173
|
+
</div>
|
|
174
|
+
) : (
|
|
175
|
+
<Input
|
|
176
|
+
ref={inputRef}
|
|
177
|
+
value={value}
|
|
178
|
+
onChange={(e) => setValue(e.target.value)}
|
|
179
|
+
onKeyDown={handleKeyDown}
|
|
180
|
+
className="h-7 text-xs font-mono flex-1 bg-white dark:bg-zinc-900"
|
|
181
|
+
placeholder="Enter value"
|
|
182
|
+
type={valueType === PropertyValueType.Real || valueType === PropertyValueType.Integer ? 'number' : 'text'}
|
|
183
|
+
step={valueType === PropertyValueType.Real ? 'any' : undefined}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Action buttons */}
|
|
188
|
+
<Tooltip>
|
|
189
|
+
<TooltipTrigger asChild>
|
|
190
|
+
<Button
|
|
191
|
+
variant="ghost"
|
|
192
|
+
size="icon"
|
|
193
|
+
className="h-6 w-6 hover:bg-green-100 dark:hover:bg-green-900/30"
|
|
194
|
+
onClick={handleSave}
|
|
195
|
+
>
|
|
196
|
+
<Check className="h-3.5 w-3.5 text-green-600" />
|
|
197
|
+
</Button>
|
|
198
|
+
</TooltipTrigger>
|
|
199
|
+
<TooltipContent>Save (Enter)</TooltipContent>
|
|
200
|
+
</Tooltip>
|
|
201
|
+
<Tooltip>
|
|
202
|
+
<TooltipTrigger asChild>
|
|
203
|
+
<Button
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="icon"
|
|
206
|
+
className="h-6 w-6 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
|
207
|
+
onClick={handleCancel}
|
|
208
|
+
>
|
|
209
|
+
<X className="h-3.5 w-3.5 text-zinc-500" />
|
|
210
|
+
</Button>
|
|
211
|
+
</TooltipTrigger>
|
|
212
|
+
<TooltipContent>Cancel (Esc)</TooltipContent>
|
|
213
|
+
</Tooltip>
|
|
214
|
+
<Tooltip>
|
|
215
|
+
<TooltipTrigger asChild>
|
|
216
|
+
<Button
|
|
217
|
+
variant="ghost"
|
|
218
|
+
size="icon"
|
|
219
|
+
className="h-6 w-6 hover:bg-red-100 dark:hover:bg-red-900/30"
|
|
220
|
+
onClick={handleDelete}
|
|
221
|
+
>
|
|
222
|
+
<Trash2 className="h-3.5 w-3.5 text-red-500" />
|
|
223
|
+
</Button>
|
|
224
|
+
</TooltipTrigger>
|
|
225
|
+
<TooltipContent>Delete property</TooltipContent>
|
|
226
|
+
</Tooltip>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Type selector - always visible */}
|
|
230
|
+
<div className="flex flex-wrap gap-1">
|
|
231
|
+
{[
|
|
232
|
+
{ type: PropertyValueType.String, label: 'String' },
|
|
233
|
+
{ type: PropertyValueType.Label, label: 'Label' },
|
|
234
|
+
{ type: PropertyValueType.Identifier, label: 'ID' },
|
|
235
|
+
{ type: PropertyValueType.Real, label: 'Real' },
|
|
236
|
+
{ type: PropertyValueType.Integer, label: 'Int' },
|
|
237
|
+
{ type: PropertyValueType.Boolean, label: 'Bool' },
|
|
238
|
+
].map(({ type, label }) => (
|
|
239
|
+
<Button
|
|
240
|
+
key={type}
|
|
241
|
+
variant={valueType === type ? 'default' : 'outline'}
|
|
242
|
+
size="sm"
|
|
243
|
+
className="h-5 px-2 text-[10px]"
|
|
244
|
+
onClick={() => {
|
|
245
|
+
setValueType(type);
|
|
246
|
+
// Convert value if switching to/from boolean
|
|
247
|
+
if (type === PropertyValueType.Boolean || type === PropertyValueType.Logical) {
|
|
248
|
+
const boolVal = value.toLowerCase() === 'true' || value === '1' || value === 'yes';
|
|
249
|
+
setValue(boolVal ? 'true' : 'false');
|
|
250
|
+
}
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
{label}
|
|
254
|
+
</Button>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface NewPropertyDialogProps {
|
|
262
|
+
modelId: string;
|
|
263
|
+
entityId: number;
|
|
264
|
+
existingPsets: string[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Dialog for adding a new property
|
|
269
|
+
*/
|
|
270
|
+
export function NewPropertyDialog({ modelId, entityId, existingPsets }: NewPropertyDialogProps) {
|
|
271
|
+
const setProperty = useViewerStore((s) => s.setProperty);
|
|
272
|
+
const createPropertySet = useViewerStore((s) => s.createPropertySet);
|
|
273
|
+
const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
|
|
274
|
+
|
|
275
|
+
const [open, setOpen] = useState(false);
|
|
276
|
+
const [psetName, setPsetName] = useState('');
|
|
277
|
+
const [isNewPset, setIsNewPset] = useState(false);
|
|
278
|
+
const [propName, setPropName] = useState('');
|
|
279
|
+
const [value, setValue] = useState('');
|
|
280
|
+
const [valueType, setValueType] = useState<PropertyValueType>(PropertyValueType.String);
|
|
281
|
+
|
|
282
|
+
const commonPsets = useMemo(() => [
|
|
283
|
+
'Pset_WallCommon',
|
|
284
|
+
'Pset_DoorCommon',
|
|
285
|
+
'Pset_WindowCommon',
|
|
286
|
+
'Pset_SlabCommon',
|
|
287
|
+
'Pset_BeamCommon',
|
|
288
|
+
'Pset_ColumnCommon',
|
|
289
|
+
'Pset_BuildingElementProxyCommon',
|
|
290
|
+
'Pset_SpaceCommon',
|
|
291
|
+
], []);
|
|
292
|
+
|
|
293
|
+
const handleSubmit = useCallback(() => {
|
|
294
|
+
if (!psetName || !propName) return;
|
|
295
|
+
|
|
296
|
+
// Normalize model ID for legacy models
|
|
297
|
+
let normalizedModelId = modelId;
|
|
298
|
+
if (modelId === 'legacy') {
|
|
299
|
+
normalizedModelId = '__legacy__';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const parsedValue = parseValue(value, valueType);
|
|
303
|
+
|
|
304
|
+
if (isNewPset) {
|
|
305
|
+
createPropertySet(normalizedModelId, entityId, psetName, [
|
|
306
|
+
{ name: propName, value: parsedValue, type: valueType },
|
|
307
|
+
]);
|
|
308
|
+
} else {
|
|
309
|
+
setProperty(normalizedModelId, entityId, psetName, propName, parsedValue, valueType);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
bumpMutationVersion();
|
|
313
|
+
|
|
314
|
+
// Reset form
|
|
315
|
+
setPsetName('');
|
|
316
|
+
setPropName('');
|
|
317
|
+
setValue('');
|
|
318
|
+
setValueType(PropertyValueType.String);
|
|
319
|
+
setIsNewPset(false);
|
|
320
|
+
setOpen(false);
|
|
321
|
+
}, [modelId, entityId, psetName, propName, value, valueType, isNewPset, setProperty, createPropertySet, bumpMutationVersion]);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
325
|
+
<DialogTrigger asChild>
|
|
326
|
+
<Button variant="outline" size="sm" className="h-7">
|
|
327
|
+
<Plus className="h-3 w-3 mr-1" />
|
|
328
|
+
Add Property
|
|
329
|
+
</Button>
|
|
330
|
+
</DialogTrigger>
|
|
331
|
+
<DialogContent className="sm:max-w-md">
|
|
332
|
+
<DialogHeader>
|
|
333
|
+
<DialogTitle>Add Property</DialogTitle>
|
|
334
|
+
<DialogDescription>
|
|
335
|
+
Add a new property to this entity
|
|
336
|
+
</DialogDescription>
|
|
337
|
+
</DialogHeader>
|
|
338
|
+
<div className="grid gap-4 py-4">
|
|
339
|
+
<div className="flex items-center gap-4">
|
|
340
|
+
<Label className="w-24">New Pset</Label>
|
|
341
|
+
<Switch checked={isNewPset} onCheckedChange={setIsNewPset} />
|
|
342
|
+
</div>
|
|
343
|
+
<div className="flex items-center gap-4">
|
|
344
|
+
<Label className="w-24">Property Set</Label>
|
|
345
|
+
{isNewPset ? (
|
|
346
|
+
<Input
|
|
347
|
+
value={psetName}
|
|
348
|
+
onChange={(e) => setPsetName(e.target.value)}
|
|
349
|
+
placeholder="e.g., Pset_CustomProperties"
|
|
350
|
+
/>
|
|
351
|
+
) : (
|
|
352
|
+
<Select value={psetName} onValueChange={setPsetName}>
|
|
353
|
+
<SelectTrigger>
|
|
354
|
+
<SelectValue placeholder="Select property set" />
|
|
355
|
+
</SelectTrigger>
|
|
356
|
+
<SelectContent>
|
|
357
|
+
{existingPsets.map((name) => (
|
|
358
|
+
<SelectItem key={name} value={name}>
|
|
359
|
+
{name}
|
|
360
|
+
</SelectItem>
|
|
361
|
+
))}
|
|
362
|
+
{commonPsets
|
|
363
|
+
.filter((p) => !existingPsets.includes(p))
|
|
364
|
+
.map((name) => (
|
|
365
|
+
<SelectItem key={name} value={name}>
|
|
366
|
+
{name} (new)
|
|
367
|
+
</SelectItem>
|
|
368
|
+
))}
|
|
369
|
+
</SelectContent>
|
|
370
|
+
</Select>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
<div className="flex items-center gap-4">
|
|
374
|
+
<Label className="w-24">Property</Label>
|
|
375
|
+
<Input
|
|
376
|
+
value={propName}
|
|
377
|
+
onChange={(e) => setPropName(e.target.value)}
|
|
378
|
+
placeholder="e.g., FireRating"
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
<div className="flex items-center gap-4">
|
|
382
|
+
<Label className="w-24">Type</Label>
|
|
383
|
+
<Select
|
|
384
|
+
value={valueType.toString()}
|
|
385
|
+
onValueChange={(v) => setValueType(parseInt(v) as PropertyValueType)}
|
|
386
|
+
>
|
|
387
|
+
<SelectTrigger>
|
|
388
|
+
<SelectValue />
|
|
389
|
+
</SelectTrigger>
|
|
390
|
+
<SelectContent>
|
|
391
|
+
<SelectItem value={PropertyValueType.String.toString()}>String</SelectItem>
|
|
392
|
+
<SelectItem value={PropertyValueType.Real.toString()}>Real</SelectItem>
|
|
393
|
+
<SelectItem value={PropertyValueType.Integer.toString()}>Integer</SelectItem>
|
|
394
|
+
<SelectItem value={PropertyValueType.Boolean.toString()}>Boolean</SelectItem>
|
|
395
|
+
<SelectItem value={PropertyValueType.Label.toString()}>Label</SelectItem>
|
|
396
|
+
<SelectItem value={PropertyValueType.Identifier.toString()}>Identifier</SelectItem>
|
|
397
|
+
</SelectContent>
|
|
398
|
+
</Select>
|
|
399
|
+
</div>
|
|
400
|
+
<div className="flex items-center gap-4">
|
|
401
|
+
<Label className="w-24">Value</Label>
|
|
402
|
+
{valueType === PropertyValueType.Boolean ? (
|
|
403
|
+
<Switch
|
|
404
|
+
checked={value === 'true'}
|
|
405
|
+
onCheckedChange={(checked) => setValue(checked ? 'true' : 'false')}
|
|
406
|
+
/>
|
|
407
|
+
) : (
|
|
408
|
+
<Input
|
|
409
|
+
value={value}
|
|
410
|
+
onChange={(e) => setValue(e.target.value)}
|
|
411
|
+
placeholder="Property value"
|
|
412
|
+
type={valueType === PropertyValueType.Real || valueType === PropertyValueType.Integer ? 'number' : 'text'}
|
|
413
|
+
/>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
<DialogFooter>
|
|
418
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
419
|
+
Cancel
|
|
420
|
+
</Button>
|
|
421
|
+
<Button onClick={handleSubmit} disabled={!psetName || !propName}>
|
|
422
|
+
Add Property
|
|
423
|
+
</Button>
|
|
424
|
+
</DialogFooter>
|
|
425
|
+
</DialogContent>
|
|
426
|
+
</Dialog>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
interface UndoRedoButtonsProps {
|
|
431
|
+
modelId: string;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Undo/Redo buttons for property mutations
|
|
436
|
+
*/
|
|
437
|
+
export function UndoRedoButtons({ modelId }: UndoRedoButtonsProps) {
|
|
438
|
+
const canUndo = useViewerStore((s) => s.canUndo);
|
|
439
|
+
const canRedo = useViewerStore((s) => s.canRedo);
|
|
440
|
+
const undo = useViewerStore((s) => s.undo);
|
|
441
|
+
const redo = useViewerStore((s) => s.redo);
|
|
442
|
+
|
|
443
|
+
// Normalize model ID for legacy models
|
|
444
|
+
let normalizedModelId = modelId;
|
|
445
|
+
if (modelId === 'legacy') {
|
|
446
|
+
normalizedModelId = '__legacy__';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const handleUndo = useCallback(() => {
|
|
450
|
+
undo(normalizedModelId);
|
|
451
|
+
}, [normalizedModelId, undo]);
|
|
452
|
+
|
|
453
|
+
const handleRedo = useCallback(() => {
|
|
454
|
+
redo(normalizedModelId);
|
|
455
|
+
}, [normalizedModelId, redo]);
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<div className="flex items-center gap-1">
|
|
459
|
+
<Tooltip>
|
|
460
|
+
<TooltipTrigger asChild>
|
|
461
|
+
<Button
|
|
462
|
+
variant="ghost"
|
|
463
|
+
size="icon"
|
|
464
|
+
className="h-7 w-7"
|
|
465
|
+
onClick={handleUndo}
|
|
466
|
+
disabled={!canUndo(normalizedModelId)}
|
|
467
|
+
>
|
|
468
|
+
<Undo className="h-4 w-4" />
|
|
469
|
+
</Button>
|
|
470
|
+
</TooltipTrigger>
|
|
471
|
+
<TooltipContent>Undo</TooltipContent>
|
|
472
|
+
</Tooltip>
|
|
473
|
+
<Tooltip>
|
|
474
|
+
<TooltipTrigger asChild>
|
|
475
|
+
<Button
|
|
476
|
+
variant="ghost"
|
|
477
|
+
size="icon"
|
|
478
|
+
className="h-7 w-7"
|
|
479
|
+
onClick={handleRedo}
|
|
480
|
+
disabled={!canRedo(normalizedModelId)}
|
|
481
|
+
>
|
|
482
|
+
<Redo className="h-4 w-4" />
|
|
483
|
+
</Button>
|
|
484
|
+
</TooltipTrigger>
|
|
485
|
+
<TooltipContent>Redo</TooltipContent>
|
|
486
|
+
</Tooltip>
|
|
487
|
+
</div>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Helper functions
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Extract the raw value from typed IFC values.
|
|
495
|
+
* Handles: arrays like [IFCLABEL, value], strings like "IFCLABEL,value"
|
|
496
|
+
*/
|
|
497
|
+
function extractRawValue(value: unknown): unknown {
|
|
498
|
+
if (value === null || value === undefined) return value;
|
|
499
|
+
|
|
500
|
+
// Handle typed value arrays [IFCTYPENAME, actualValue]
|
|
501
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'string' && value[0].toUpperCase().startsWith('IFC')) {
|
|
502
|
+
return value[1];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Handle string format "IFCTYPENAME,actualValue"
|
|
506
|
+
if (typeof value === 'string') {
|
|
507
|
+
const match = value.match(/^(IFC[A-Z0-9_]+),(.*)$/i);
|
|
508
|
+
if (match) {
|
|
509
|
+
return match[2]; // Return just the value part
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function formatValue(value: unknown): string {
|
|
517
|
+
const raw = extractRawValue(value);
|
|
518
|
+
if (raw === null || raw === undefined) return '';
|
|
519
|
+
if (typeof raw === 'boolean') return raw ? 'true' : 'false';
|
|
520
|
+
if (typeof raw === 'number') return raw.toString();
|
|
521
|
+
if (Array.isArray(raw)) return JSON.stringify(raw);
|
|
522
|
+
return String(raw);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function formatDisplayValue(value: unknown): string {
|
|
526
|
+
const raw = extractRawValue(value);
|
|
527
|
+
if (raw === null || raw === undefined) return '—';
|
|
528
|
+
if (typeof raw === 'boolean') return raw ? 'True' : 'False';
|
|
529
|
+
if (typeof raw === 'number') {
|
|
530
|
+
return Number.isInteger(raw)
|
|
531
|
+
? raw.toLocaleString()
|
|
532
|
+
: raw.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
533
|
+
}
|
|
534
|
+
if (Array.isArray(raw)) return JSON.stringify(raw);
|
|
535
|
+
|
|
536
|
+
// Handle boolean strings
|
|
537
|
+
const strVal = String(raw);
|
|
538
|
+
if (strVal === '.T.' || strVal.toUpperCase() === '.T.') return 'True';
|
|
539
|
+
if (strVal === '.F.' || strVal.toUpperCase() === '.F.') return 'False';
|
|
540
|
+
if (strVal === '.U.' || strVal.toUpperCase() === '.U.') return 'Unknown';
|
|
541
|
+
return strVal;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function detectValueType(value: unknown, fallback: PropertyValueType): PropertyValueType {
|
|
545
|
+
// First check if it's a typed value and extract the type
|
|
546
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'string') {
|
|
547
|
+
const typeName = value[0].toUpperCase();
|
|
548
|
+
if (typeName === 'IFCBOOLEAN' || typeName === 'IFCLOGICAL') return PropertyValueType.Boolean;
|
|
549
|
+
if (typeName === 'IFCREAL') return PropertyValueType.Real;
|
|
550
|
+
if (typeName === 'IFCINTEGER') return PropertyValueType.Integer;
|
|
551
|
+
if (typeName === 'IFCIDENTIFIER') return PropertyValueType.Identifier;
|
|
552
|
+
if (typeName === 'IFCLABEL') return PropertyValueType.Label;
|
|
553
|
+
if (typeName === 'IFCTEXT') return PropertyValueType.String;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Check string format "IFCTYPE,value"
|
|
557
|
+
if (typeof value === 'string') {
|
|
558
|
+
const match = value.match(/^(IFC[A-Z0-9_]+),/i);
|
|
559
|
+
if (match) {
|
|
560
|
+
const typeName = match[1].toUpperCase();
|
|
561
|
+
if (typeName === 'IFCBOOLEAN' || typeName === 'IFCLOGICAL') return PropertyValueType.Boolean;
|
|
562
|
+
if (typeName === 'IFCREAL') return PropertyValueType.Real;
|
|
563
|
+
if (typeName === 'IFCINTEGER') return PropertyValueType.Integer;
|
|
564
|
+
if (typeName === 'IFCIDENTIFIER') return PropertyValueType.Identifier;
|
|
565
|
+
if (typeName === 'IFCLABEL') return PropertyValueType.Label;
|
|
566
|
+
if (typeName === 'IFCTEXT') return PropertyValueType.String;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check for boolean enum values
|
|
570
|
+
const upper = value.toUpperCase();
|
|
571
|
+
if (upper === '.T.' || upper === '.F.' || upper === '.U.') {
|
|
572
|
+
return PropertyValueType.Boolean;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Check raw value type
|
|
577
|
+
const raw = extractRawValue(value);
|
|
578
|
+
if (typeof raw === 'boolean') return PropertyValueType.Boolean;
|
|
579
|
+
if (typeof raw === 'number') {
|
|
580
|
+
return Number.isInteger(raw) ? PropertyValueType.Integer : PropertyValueType.Real;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return fallback;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function getTypeName(type: PropertyValueType): string {
|
|
587
|
+
switch (type) {
|
|
588
|
+
case PropertyValueType.String: return 'String';
|
|
589
|
+
case PropertyValueType.Label: return 'Label';
|
|
590
|
+
case PropertyValueType.Identifier: return 'Identifier';
|
|
591
|
+
case PropertyValueType.Real: return 'Real';
|
|
592
|
+
case PropertyValueType.Integer: return 'Integer';
|
|
593
|
+
case PropertyValueType.Boolean: return 'Boolean';
|
|
594
|
+
case PropertyValueType.Logical: return 'Logical';
|
|
595
|
+
default: return 'String';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function parseValue(value: string, type: PropertyValueType): PropertyValue {
|
|
600
|
+
switch (type) {
|
|
601
|
+
case PropertyValueType.Real:
|
|
602
|
+
return parseFloat(value) || 0;
|
|
603
|
+
case PropertyValueType.Integer:
|
|
604
|
+
return parseInt(value, 10) || 0;
|
|
605
|
+
case PropertyValueType.Boolean:
|
|
606
|
+
case PropertyValueType.Logical:
|
|
607
|
+
return value.toLowerCase() === 'true';
|
|
608
|
+
default:
|
|
609
|
+
return value;
|
|
610
|
+
}
|
|
611
|
+
}
|