@ifc-lite/viewer 1.1.7 → 1.5.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-B0e15b_b.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-Dgd6vzw_.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
- package/dist/assets/wasm-bridge-Dc82YpdZ.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,661 @@
|
|
|
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
|
+
* IDSPanel - IDS (Information Delivery Specification) validation panel
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* - Load IDS files
|
|
10
|
+
* - Run validation against loaded models
|
|
11
|
+
* - View validation results with pass/fail status
|
|
12
|
+
* - Filter by specification, status
|
|
13
|
+
* - Click to select entities in 3D view
|
|
14
|
+
* - Isolate failed/passed entities
|
|
15
|
+
* - Multi-language support (EN/DE/FR)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React, { useCallback, useState, useMemo, useRef } from 'react';
|
|
19
|
+
import {
|
|
20
|
+
X,
|
|
21
|
+
Upload,
|
|
22
|
+
Play,
|
|
23
|
+
CheckCircle,
|
|
24
|
+
XCircle,
|
|
25
|
+
AlertCircle,
|
|
26
|
+
ChevronDown,
|
|
27
|
+
ChevronRight,
|
|
28
|
+
Filter,
|
|
29
|
+
Focus,
|
|
30
|
+
EyeOff,
|
|
31
|
+
Eye,
|
|
32
|
+
FileText,
|
|
33
|
+
Loader2,
|
|
34
|
+
Building2,
|
|
35
|
+
RefreshCw,
|
|
36
|
+
Trash2,
|
|
37
|
+
FileJson,
|
|
38
|
+
FileCode,
|
|
39
|
+
} from 'lucide-react';
|
|
40
|
+
import { Button } from '@/components/ui/button';
|
|
41
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
42
|
+
import { Separator } from '@/components/ui/separator';
|
|
43
|
+
import { Badge } from '@/components/ui/badge';
|
|
44
|
+
import { Progress } from '@/components/ui/progress';
|
|
45
|
+
import {
|
|
46
|
+
Select,
|
|
47
|
+
SelectContent,
|
|
48
|
+
SelectItem,
|
|
49
|
+
SelectTrigger,
|
|
50
|
+
SelectValue,
|
|
51
|
+
} from '@/components/ui/select';
|
|
52
|
+
import {
|
|
53
|
+
Collapsible,
|
|
54
|
+
CollapsibleContent,
|
|
55
|
+
CollapsibleTrigger,
|
|
56
|
+
} from '@/components/ui/collapsible';
|
|
57
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
58
|
+
import { useIDS } from '@/hooks/useIDS';
|
|
59
|
+
import type {
|
|
60
|
+
IDSSpecificationResult,
|
|
61
|
+
IDSEntityResult,
|
|
62
|
+
IDSRequirementResult,
|
|
63
|
+
} from '@ifc-lite/ids';
|
|
64
|
+
import { cn } from '@/lib/utils';
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Types
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
interface IDSPanelProps {
|
|
71
|
+
onClose?: () => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Helper Components
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
function StatusIcon({ status, showLabel = false }: { status: 'pass' | 'fail' | 'not_applicable'; showLabel?: boolean }) {
|
|
79
|
+
const labels = {
|
|
80
|
+
pass: 'Passed',
|
|
81
|
+
fail: 'Failed',
|
|
82
|
+
not_applicable: 'Not Applicable',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const icons = {
|
|
86
|
+
pass: <CheckCircle className="h-4 w-4 text-green-500" aria-hidden="true" />,
|
|
87
|
+
fail: <XCircle className="h-4 w-4 text-red-500" aria-hidden="true" />,
|
|
88
|
+
not_applicable: <AlertCircle className="h-4 w-4 text-yellow-500" aria-hidden="true" />,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<span className="inline-flex items-center gap-1" role="status" aria-label={labels[status]}>
|
|
93
|
+
{icons[status]}
|
|
94
|
+
{showLabel && <span className="sr-only">{labels[status]}</span>}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function StatusBadge({ status }: { status: 'pass' | 'fail' | 'not_applicable' }) {
|
|
100
|
+
const variant = status === 'pass' ? 'default' : status === 'fail' ? 'destructive' : 'secondary';
|
|
101
|
+
const label = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'N/A';
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Badge variant={variant} className="text-xs">
|
|
105
|
+
{label}
|
|
106
|
+
</Badge>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function PassRateBar({ passRate }: { passRate: number }) {
|
|
111
|
+
const color = passRate >= 80 ? 'bg-green-500' : passRate >= 50 ? 'bg-yellow-500' : 'bg-red-500';
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
|
116
|
+
<div
|
|
117
|
+
className={cn('h-full rounded-full transition-all', color)}
|
|
118
|
+
style={{ width: `${passRate}%` }}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<span className="text-xs text-muted-foreground w-10 text-right">{passRate}%</span>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Specification Card Component
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
interface SpecificationCardProps {
|
|
131
|
+
result: IDSSpecificationResult;
|
|
132
|
+
isActive: boolean;
|
|
133
|
+
onSelect: () => void;
|
|
134
|
+
onEntityClick: (modelId: string, expressId: number) => void;
|
|
135
|
+
filterMode: 'all' | 'failed' | 'passed';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function SpecificationCard({
|
|
139
|
+
result,
|
|
140
|
+
isActive,
|
|
141
|
+
onSelect,
|
|
142
|
+
onEntityClick,
|
|
143
|
+
filterMode,
|
|
144
|
+
}: SpecificationCardProps) {
|
|
145
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
146
|
+
|
|
147
|
+
// Filter entity results based on mode
|
|
148
|
+
const filteredEntities = useMemo(() => {
|
|
149
|
+
if (filterMode === 'all') return result.entityResults;
|
|
150
|
+
return result.entityResults.filter((e) =>
|
|
151
|
+
filterMode === 'failed' ? !e.passed : e.passed
|
|
152
|
+
);
|
|
153
|
+
}, [result.entityResults, filterMode]);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
|
|
157
|
+
<div
|
|
158
|
+
className={cn(
|
|
159
|
+
'rounded-lg border transition-colors',
|
|
160
|
+
isActive ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{/* Specification Header */}
|
|
164
|
+
<CollapsibleTrigger asChild>
|
|
165
|
+
<button
|
|
166
|
+
className="w-full p-3 text-left"
|
|
167
|
+
onClick={onSelect}
|
|
168
|
+
>
|
|
169
|
+
<div className="flex items-start gap-2">
|
|
170
|
+
{isExpanded ? (
|
|
171
|
+
<ChevronDown className="h-4 w-4 mt-0.5 shrink-0" />
|
|
172
|
+
) : (
|
|
173
|
+
<ChevronRight className="h-4 w-4 mt-0.5 shrink-0" />
|
|
174
|
+
)}
|
|
175
|
+
<div className="flex-1 min-w-0">
|
|
176
|
+
<div className="flex items-center gap-2 mb-1">
|
|
177
|
+
<StatusIcon status={result.status} />
|
|
178
|
+
<span className="font-medium text-sm truncate">
|
|
179
|
+
{result.specification.name}
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
{result.specification.description && (
|
|
183
|
+
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
|
|
184
|
+
{result.specification.description}
|
|
185
|
+
</p>
|
|
186
|
+
)}
|
|
187
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
188
|
+
<span className="flex items-center gap-1">
|
|
189
|
+
<Building2 className="h-3 w-3" />
|
|
190
|
+
{result.applicableCount} entities
|
|
191
|
+
</span>
|
|
192
|
+
<span className="text-green-600">{result.passedCount} passed</span>
|
|
193
|
+
<span className="text-red-600">{result.failedCount} failed</span>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="mt-2">
|
|
196
|
+
<PassRateBar passRate={result.passRate} />
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</button>
|
|
201
|
+
</CollapsibleTrigger>
|
|
202
|
+
|
|
203
|
+
{/* Entity Results */}
|
|
204
|
+
<CollapsibleContent>
|
|
205
|
+
<Separator />
|
|
206
|
+
<div className="max-h-64 overflow-auto">
|
|
207
|
+
{filteredEntities.length === 0 ? (
|
|
208
|
+
<div className="p-3 text-sm text-muted-foreground text-center">
|
|
209
|
+
No {filterMode === 'failed' ? 'failed' : filterMode === 'passed' ? 'passed' : ''} entities
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
<div className="divide-y">
|
|
213
|
+
{filteredEntities.slice(0, 100).map((entity) => (
|
|
214
|
+
<EntityResultRow
|
|
215
|
+
key={`${entity.modelId}:${entity.expressId}`}
|
|
216
|
+
entity={entity}
|
|
217
|
+
onClick={() => onEntityClick(entity.modelId, entity.expressId)}
|
|
218
|
+
/>
|
|
219
|
+
))}
|
|
220
|
+
{filteredEntities.length > 100 && (
|
|
221
|
+
<div className="p-2 text-xs text-muted-foreground text-center">
|
|
222
|
+
Showing 100 of {filteredEntities.length} entities
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</CollapsibleContent>
|
|
229
|
+
</div>
|
|
230
|
+
</Collapsible>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Entity Result Row Component
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
interface EntityResultRowProps {
|
|
239
|
+
entity: IDSEntityResult;
|
|
240
|
+
onClick: () => void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function EntityResultRow({ entity, onClick }: EntityResultRowProps) {
|
|
244
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
245
|
+
|
|
246
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
247
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
onClick();
|
|
250
|
+
} else if (e.key === 'ArrowRight') {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
setShowDetails(true);
|
|
253
|
+
} else if (e.key === 'ArrowLeft') {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
setShowDetails(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className="hover:bg-muted/50 focus-within:bg-muted/50 focus-within:ring-2 focus-within:ring-primary focus-within:ring-inset rounded-md">
|
|
261
|
+
<button
|
|
262
|
+
className="w-full p-2 text-left flex items-center gap-2 focus:outline-none"
|
|
263
|
+
onClick={onClick}
|
|
264
|
+
onKeyDown={handleKeyDown}
|
|
265
|
+
tabIndex={0}
|
|
266
|
+
aria-expanded={showDetails}
|
|
267
|
+
aria-label={`${entity.entityName || '#' + entity.expressId} - ${entity.entityType} - ${entity.passed ? 'Passed' : 'Failed'}`}
|
|
268
|
+
>
|
|
269
|
+
<StatusIcon status={entity.passed ? 'pass' : 'fail'} />
|
|
270
|
+
<div className="flex-1 min-w-0">
|
|
271
|
+
<div className="text-sm truncate">
|
|
272
|
+
{entity.entityName || `#${entity.expressId}`}
|
|
273
|
+
</div>
|
|
274
|
+
<div className="text-xs text-muted-foreground truncate">
|
|
275
|
+
{entity.entityType}
|
|
276
|
+
{entity.globalId && ` · ${entity.globalId}`}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{/* Chevron - shrink-0 keeps it visible */}
|
|
280
|
+
<span
|
|
281
|
+
role="button"
|
|
282
|
+
tabIndex={-1}
|
|
283
|
+
className="shrink-0 p-1 rounded hover:bg-accent"
|
|
284
|
+
onClick={(e) => {
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
setShowDetails(!showDetails);
|
|
287
|
+
}}
|
|
288
|
+
aria-label={showDetails ? 'Hide details' : 'Show details'}
|
|
289
|
+
>
|
|
290
|
+
{showDetails ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
291
|
+
</span>
|
|
292
|
+
</button>
|
|
293
|
+
{showDetails && (
|
|
294
|
+
<div className="pl-8 pr-2 pb-2 space-y-1">
|
|
295
|
+
{entity.requirementResults.map((req, idx) => (
|
|
296
|
+
<RequirementResultRow key={req.requirement.id || idx} result={req} />
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// Requirement Result Row Component
|
|
306
|
+
// ============================================================================
|
|
307
|
+
|
|
308
|
+
interface RequirementResultRowProps {
|
|
309
|
+
result: IDSRequirementResult;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function RequirementResultRow({ result }: RequirementResultRowProps) {
|
|
313
|
+
return (
|
|
314
|
+
<div className="text-xs flex items-start gap-2 py-1">
|
|
315
|
+
<StatusIcon status={result.status} />
|
|
316
|
+
<div className="flex-1 min-w-0">
|
|
317
|
+
<div className="text-muted-foreground">{result.checkedDescription}</div>
|
|
318
|
+
{result.failureReason && (
|
|
319
|
+
<div className="text-red-600 mt-0.5">{result.failureReason}</div>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Main Panel Component
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
331
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
332
|
+
|
|
333
|
+
const {
|
|
334
|
+
// State
|
|
335
|
+
document,
|
|
336
|
+
report,
|
|
337
|
+
loading,
|
|
338
|
+
progress,
|
|
339
|
+
error,
|
|
340
|
+
activeSpecificationId,
|
|
341
|
+
filterMode,
|
|
342
|
+
|
|
343
|
+
// Actions
|
|
344
|
+
loadIDSFile,
|
|
345
|
+
clearIDS,
|
|
346
|
+
runValidation,
|
|
347
|
+
clearValidation,
|
|
348
|
+
setActiveSpecification,
|
|
349
|
+
selectEntity,
|
|
350
|
+
setFilterMode,
|
|
351
|
+
applyColors,
|
|
352
|
+
isolateFailed,
|
|
353
|
+
isolatePassed,
|
|
354
|
+
clearIsolation,
|
|
355
|
+
exportReportJSON,
|
|
356
|
+
exportReportHTML,
|
|
357
|
+
} = useIDS();
|
|
358
|
+
|
|
359
|
+
// Handle file selection
|
|
360
|
+
const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
361
|
+
const file = e.target.files?.[0];
|
|
362
|
+
if (file) {
|
|
363
|
+
await loadIDSFile(file);
|
|
364
|
+
}
|
|
365
|
+
// Reset input for re-selection of same file
|
|
366
|
+
e.target.value = '';
|
|
367
|
+
}, [loadIDSFile]);
|
|
368
|
+
|
|
369
|
+
// Handle entity click
|
|
370
|
+
const handleEntityClick = useCallback((modelId: string, expressId: number) => {
|
|
371
|
+
selectEntity(modelId, expressId);
|
|
372
|
+
}, [selectEntity]);
|
|
373
|
+
|
|
374
|
+
// Render validation progress
|
|
375
|
+
const renderProgress = () => {
|
|
376
|
+
if (!progress) return null;
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div className="p-3 border-b">
|
|
380
|
+
<div className="flex items-center gap-2 mb-2">
|
|
381
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
382
|
+
<span className="text-sm">
|
|
383
|
+
{progress.phase === 'filtering' && 'Finding applicable entities...'}
|
|
384
|
+
{progress.phase === 'validating' && `Validating... (${progress.entitiesProcessed}/${progress.totalEntities})`}
|
|
385
|
+
{progress.phase === 'complete' && 'Complete'}
|
|
386
|
+
</span>
|
|
387
|
+
</div>
|
|
388
|
+
<Progress value={progress.percentage} className="h-2" />
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Render empty state
|
|
394
|
+
const renderEmptyState = () => {
|
|
395
|
+
if (document) return null;
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
|
399
|
+
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
|
|
400
|
+
<h3 className="font-medium text-sm mb-2">No IDS Loaded</h3>
|
|
401
|
+
<p className="text-xs text-muted-foreground mb-4">
|
|
402
|
+
Load an IDS (Information Delivery Specification) file to validate your model
|
|
403
|
+
</p>
|
|
404
|
+
<input
|
|
405
|
+
ref={fileInputRef}
|
|
406
|
+
type="file"
|
|
407
|
+
accept=".ids,.xml"
|
|
408
|
+
className="hidden"
|
|
409
|
+
onChange={handleFileSelect}
|
|
410
|
+
/>
|
|
411
|
+
<Button onClick={() => fileInputRef.current?.click()}>
|
|
412
|
+
<Upload className="h-4 w-4 mr-2" />
|
|
413
|
+
Load IDS File
|
|
414
|
+
</Button>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Render document loaded but no validation
|
|
420
|
+
const renderDocumentLoaded = () => {
|
|
421
|
+
if (!document || report) return null;
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div className="p-4">
|
|
425
|
+
<div className="rounded-lg border p-4 mb-4">
|
|
426
|
+
<h3 className="font-medium text-sm mb-1">{document.info.title}</h3>
|
|
427
|
+
{document.info.description && (
|
|
428
|
+
<p className="text-xs text-muted-foreground mb-2">{document.info.description}</p>
|
|
429
|
+
)}
|
|
430
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
431
|
+
<span>{document.specifications.length} specifications</span>
|
|
432
|
+
{document.info.version && <span>v{document.info.version}</span>}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<Button className="w-full" onClick={runValidation} disabled={loading}>
|
|
437
|
+
{loading ? (
|
|
438
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
439
|
+
) : (
|
|
440
|
+
<Play className="h-4 w-4 mr-2" />
|
|
441
|
+
)}
|
|
442
|
+
Run Validation
|
|
443
|
+
</Button>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Render validation results
|
|
449
|
+
const renderResults = () => {
|
|
450
|
+
if (!report) return null;
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<>
|
|
454
|
+
{/* Summary Header */}
|
|
455
|
+
<div className="p-3 border-b bg-muted/30">
|
|
456
|
+
<div className="flex items-center gap-2 mb-2">
|
|
457
|
+
<StatusIcon status={report.summary.failedSpecifications > 0 ? 'fail' : 'pass'} />
|
|
458
|
+
<span className="font-medium text-sm">
|
|
459
|
+
{report.summary.passedSpecifications}/{report.summary.totalSpecifications} Specifications Passed
|
|
460
|
+
</span>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="grid grid-cols-3 gap-2 text-xs text-center">
|
|
463
|
+
<div className="bg-background rounded p-2">
|
|
464
|
+
<div className="font-medium">{report.summary.totalEntitiesChecked}</div>
|
|
465
|
+
<div className="text-muted-foreground">Checked</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="bg-background rounded p-2">
|
|
468
|
+
<div className="font-medium text-green-600">{report.summary.totalEntitiesPassed}</div>
|
|
469
|
+
<div className="text-muted-foreground">Passed</div>
|
|
470
|
+
</div>
|
|
471
|
+
<div className="bg-background rounded p-2">
|
|
472
|
+
<div className="font-medium text-red-600">{report.summary.totalEntitiesFailed}</div>
|
|
473
|
+
<div className="text-muted-foreground">Failed</div>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
<div className="mt-2">
|
|
477
|
+
<PassRateBar passRate={report.summary.overallPassRate} />
|
|
478
|
+
</div>
|
|
479
|
+
<p className="text-xs text-muted-foreground mt-2 text-center">
|
|
480
|
+
💡 Click any entity to select and zoom to it in the 3D view
|
|
481
|
+
</p>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{/* Filter & Actions Bar */}
|
|
485
|
+
<div className="p-2 border-b flex items-center gap-1 flex-wrap">
|
|
486
|
+
<Select value={filterMode} onValueChange={(v) => setFilterMode(v as 'all' | 'failed' | 'passed')}>
|
|
487
|
+
<SelectTrigger className="h-8 w-24">
|
|
488
|
+
<Filter className="h-3 w-3 mr-1" />
|
|
489
|
+
<SelectValue />
|
|
490
|
+
</SelectTrigger>
|
|
491
|
+
<SelectContent>
|
|
492
|
+
<SelectItem value="all">All</SelectItem>
|
|
493
|
+
<SelectItem value="failed">Failed</SelectItem>
|
|
494
|
+
<SelectItem value="passed">Passed</SelectItem>
|
|
495
|
+
</SelectContent>
|
|
496
|
+
</Select>
|
|
497
|
+
|
|
498
|
+
<div className="flex-1 min-w-2" />
|
|
499
|
+
|
|
500
|
+
<Tooltip>
|
|
501
|
+
<TooltipTrigger asChild>
|
|
502
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={isolateFailed}>
|
|
503
|
+
<EyeOff className="h-4 w-4" />
|
|
504
|
+
</Button>
|
|
505
|
+
</TooltipTrigger>
|
|
506
|
+
<TooltipContent>Isolate Failed</TooltipContent>
|
|
507
|
+
</Tooltip>
|
|
508
|
+
|
|
509
|
+
<Tooltip>
|
|
510
|
+
<TooltipTrigger asChild>
|
|
511
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={isolatePassed}>
|
|
512
|
+
<Eye className="h-4 w-4" />
|
|
513
|
+
</Button>
|
|
514
|
+
</TooltipTrigger>
|
|
515
|
+
<TooltipContent>Isolate Passed</TooltipContent>
|
|
516
|
+
</Tooltip>
|
|
517
|
+
|
|
518
|
+
<Tooltip>
|
|
519
|
+
<TooltipTrigger asChild>
|
|
520
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={clearIsolation}>
|
|
521
|
+
<Focus className="h-4 w-4" />
|
|
522
|
+
</Button>
|
|
523
|
+
</TooltipTrigger>
|
|
524
|
+
<TooltipContent>Clear Isolation</TooltipContent>
|
|
525
|
+
</Tooltip>
|
|
526
|
+
|
|
527
|
+
<Tooltip>
|
|
528
|
+
<TooltipTrigger asChild>
|
|
529
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={applyColors}>
|
|
530
|
+
<RefreshCw className="h-4 w-4" />
|
|
531
|
+
</Button>
|
|
532
|
+
</TooltipTrigger>
|
|
533
|
+
<TooltipContent>Reapply Colors</TooltipContent>
|
|
534
|
+
</Tooltip>
|
|
535
|
+
|
|
536
|
+
<Separator orientation="vertical" className="h-4 mx-1" />
|
|
537
|
+
|
|
538
|
+
<Tooltip>
|
|
539
|
+
<TooltipTrigger asChild>
|
|
540
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={exportReportJSON}>
|
|
541
|
+
<FileJson className="h-4 w-4 text-blue-500" />
|
|
542
|
+
</Button>
|
|
543
|
+
</TooltipTrigger>
|
|
544
|
+
<TooltipContent>Export JSON Report</TooltipContent>
|
|
545
|
+
</Tooltip>
|
|
546
|
+
|
|
547
|
+
<Tooltip>
|
|
548
|
+
<TooltipTrigger asChild>
|
|
549
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={exportReportHTML}>
|
|
550
|
+
<FileCode className="h-4 w-4 text-orange-500" />
|
|
551
|
+
</Button>
|
|
552
|
+
</TooltipTrigger>
|
|
553
|
+
<TooltipContent>Export HTML Report</TooltipContent>
|
|
554
|
+
</Tooltip>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{/* Specifications List */}
|
|
558
|
+
<ScrollArea className="flex-1">
|
|
559
|
+
<div className="p-2 space-y-2">
|
|
560
|
+
{report.specificationResults.map((specResult) => (
|
|
561
|
+
<SpecificationCard
|
|
562
|
+
key={specResult.specification.id}
|
|
563
|
+
result={specResult}
|
|
564
|
+
isActive={activeSpecificationId === specResult.specification.id}
|
|
565
|
+
onSelect={() => setActiveSpecification(specResult.specification.id)}
|
|
566
|
+
onEntityClick={handleEntityClick}
|
|
567
|
+
filterMode={filterMode}
|
|
568
|
+
/>
|
|
569
|
+
))}
|
|
570
|
+
</div>
|
|
571
|
+
</ScrollArea>
|
|
572
|
+
</>
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
return (
|
|
577
|
+
<div className="h-full flex flex-col bg-background">
|
|
578
|
+
{/* Header */}
|
|
579
|
+
<div className="flex items-center justify-between p-3 border-b">
|
|
580
|
+
<div className="flex items-center gap-2">
|
|
581
|
+
<FileText className="h-4 w-4" />
|
|
582
|
+
<span className="font-medium text-sm">IDS Validation</span>
|
|
583
|
+
</div>
|
|
584
|
+
<div className="flex items-center gap-1">
|
|
585
|
+
{/* Load New IDS */}
|
|
586
|
+
{document && (
|
|
587
|
+
<>
|
|
588
|
+
<input
|
|
589
|
+
ref={fileInputRef}
|
|
590
|
+
type="file"
|
|
591
|
+
accept=".ids,.xml"
|
|
592
|
+
className="hidden"
|
|
593
|
+
onChange={handleFileSelect}
|
|
594
|
+
/>
|
|
595
|
+
<Tooltip>
|
|
596
|
+
<TooltipTrigger asChild>
|
|
597
|
+
<Button
|
|
598
|
+
variant="ghost"
|
|
599
|
+
size="sm"
|
|
600
|
+
className="h-7 w-7 p-0"
|
|
601
|
+
onClick={() => fileInputRef.current?.click()}
|
|
602
|
+
>
|
|
603
|
+
<Upload className="h-3 w-3" />
|
|
604
|
+
</Button>
|
|
605
|
+
</TooltipTrigger>
|
|
606
|
+
<TooltipContent>Load New IDS</TooltipContent>
|
|
607
|
+
</Tooltip>
|
|
608
|
+
</>
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{/* Clear */}
|
|
612
|
+
{document && (
|
|
613
|
+
<Tooltip>
|
|
614
|
+
<TooltipTrigger asChild>
|
|
615
|
+
<Button
|
|
616
|
+
variant="ghost"
|
|
617
|
+
size="sm"
|
|
618
|
+
className="h-7 w-7 p-0"
|
|
619
|
+
onClick={() => {
|
|
620
|
+
clearIDS();
|
|
621
|
+
clearValidation();
|
|
622
|
+
}}
|
|
623
|
+
>
|
|
624
|
+
<Trash2 className="h-3 w-3" />
|
|
625
|
+
</Button>
|
|
626
|
+
</TooltipTrigger>
|
|
627
|
+
<TooltipContent>Clear IDS</TooltipContent>
|
|
628
|
+
</Tooltip>
|
|
629
|
+
)}
|
|
630
|
+
|
|
631
|
+
{/* Close */}
|
|
632
|
+
{onClose && (
|
|
633
|
+
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={onClose}>
|
|
634
|
+
<X className="h-4 w-4" />
|
|
635
|
+
</Button>
|
|
636
|
+
)}
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
|
|
640
|
+
{/* Error Display */}
|
|
641
|
+
{error && (
|
|
642
|
+
<div className="p-3 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800">
|
|
643
|
+
<div className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
|
|
644
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
645
|
+
<span>{error}</span>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
|
|
650
|
+
{/* Progress */}
|
|
651
|
+
{loading && renderProgress()}
|
|
652
|
+
|
|
653
|
+
{/* Content */}
|
|
654
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
655
|
+
{renderEmptyState()}
|
|
656
|
+
{renderDocumentLoaded()}
|
|
657
|
+
{renderResults()}
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
);
|
|
661
|
+
}
|