fix(canvas): Add Save/Reload buttons and expand IntrospectionPanel to show all model data
CanvasView: - Fix Save button visibility - now shows when spec is loaded (grayed if no changes) - Separate logic for spec mode vs legacy mode save buttons - Fix Reload button visibility IntrospectionPanel: - Add Mass Properties section (mass, volume, surface area, CoG, body count) - Add Linked Parts section showing file dependencies - Add Bodies section (solid/sheet body counts) - Add Units section showing unit system - Type-safe access to all nested properties
This commit is contained in:
@@ -15,6 +15,10 @@ import {
|
||||
FlaskConical,
|
||||
SlidersHorizontal,
|
||||
AlertTriangle,
|
||||
Scale,
|
||||
Link,
|
||||
Box,
|
||||
Settings2,
|
||||
} from 'lucide-react';
|
||||
import { useCanvasStore } from '../../../hooks/useCanvasStore';
|
||||
|
||||
@@ -316,6 +320,180 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mass Properties Section */}
|
||||
{result.mass_properties && (
|
||||
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('mass')}
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Scale size={14} className="text-blue-400" />
|
||||
<span className="text-sm font-medium text-white">Mass Properties</span>
|
||||
</div>
|
||||
{expandedSections.has('mass') ? (
|
||||
<ChevronDown size={14} className="text-dark-400" />
|
||||
) : (
|
||||
<ChevronRight size={14} className="text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('mass') && (
|
||||
<div className="p-2 space-y-1">
|
||||
{result.mass_properties.mass_kg !== undefined && (
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">Mass</span>
|
||||
<span className="text-white font-mono">
|
||||
{(result.mass_properties.mass_kg as number).toFixed(4)} kg
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{result.mass_properties.volume_mm3 !== undefined && (result.mass_properties.volume_mm3 as number) > 0 && (
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">Volume</span>
|
||||
<span className="text-white font-mono">
|
||||
{((result.mass_properties.volume_mm3 as number) / 1e9).toFixed(6)} m³
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{result.mass_properties.surface_area_mm2 !== undefined && (result.mass_properties.surface_area_mm2 as number) > 0 && (
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">Surface Area</span>
|
||||
<span className="text-white font-mono">
|
||||
{((result.mass_properties.surface_area_mm2 as number) / 1e6).toFixed(4)} m²
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{Array.isArray(result.mass_properties.center_of_gravity_mm) && (
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">CoG (mm)</span>
|
||||
<span className="text-white font-mono text-right">
|
||||
[{(result.mass_properties.center_of_gravity_mm as number[]).map((v: number) => v.toFixed(1)).join(', ')}]
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{typeof result.mass_properties.num_bodies === 'number' && (
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">Bodies</span>
|
||||
<span className="text-white font-mono">{result.mass_properties.num_bodies}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Linked Parts / File Dependencies Section */}
|
||||
{result.linked_parts && (
|
||||
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('linked')}
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link size={14} className="text-purple-400" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Linked Parts ({(result.linked_parts.loaded_parts as Array<{name: string}>)?.length || 0})
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('linked') ? (
|
||||
<ChevronDown size={14} className="text-dark-400" />
|
||||
) : (
|
||||
<ChevronRight size={14} className="text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('linked') && (
|
||||
<div className="p-2 space-y-1 max-h-48 overflow-y-auto">
|
||||
{((result.linked_parts.loaded_parts as Array<{name: string, path: string, leaf_name: string}>) || []).map((part) => (
|
||||
<div
|
||||
key={part.path}
|
||||
className="p-2 bg-dark-850 rounded"
|
||||
>
|
||||
<p className="text-sm text-white">{part.name}</p>
|
||||
<p className="text-xs text-dark-500 truncate" title={part.path}>
|
||||
{part.leaf_name || part.path.split(/[/\\]/).pop()}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{((result.linked_parts.loaded_parts as Array<unknown>) || []).length === 0 && (
|
||||
<p className="text-xs text-dark-500 text-center py-2">No linked parts found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bodies Section */}
|
||||
{result.bodies && (result.bodies.counts as {total: number})?.total > 0 && (
|
||||
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('bodies')}
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Box size={14} className="text-orange-400" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Bodies ({(result.bodies.counts as {total: number})?.total || 0})
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('bodies') ? (
|
||||
<ChevronDown size={14} className="text-dark-400" />
|
||||
) : (
|
||||
<ChevronRight size={14} className="text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('bodies') && (
|
||||
<div className="p-2 space-y-1">
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">Solid Bodies</span>
|
||||
<span className="text-white font-mono">{(result.bodies.counts as {solid: number})?.solid || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">Sheet Bodies</span>
|
||||
<span className="text-white font-mono">{(result.bodies.counts as {sheet: number})?.sheet || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Units Section */}
|
||||
{result.units && (
|
||||
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('units')}
|
||||
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 size={14} className="text-gray-400" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Units ({(result.units as {system?: string})?.system || 'Unknown'})
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('units') ? (
|
||||
<ChevronDown size={14} className="text-dark-400" />
|
||||
) : (
|
||||
<ChevronRight size={14} className="text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('units') && (
|
||||
<div className="p-2 space-y-1">
|
||||
{(result.units as {base_units?: Record<string, string>})?.base_units &&
|
||||
Object.entries((result.units as {base_units: Record<string, string>}).base_units).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between p-2 bg-dark-850 rounded text-xs">
|
||||
<span className="text-dark-400">{key}</span>
|
||||
<span className="text-white font-mono">{value}</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extractors Section - only show if available */}
|
||||
{(result.extractors_available?.length ?? 0) > 0 && (
|
||||
<div className="border border-dark-700 rounded-lg overflow-hidden">
|
||||
|
||||
@@ -296,17 +296,34 @@ export function CanvasView() {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Save Button - only show when there's a study and changes */}
|
||||
{activeStudyId && (
|
||||
{/* Save Button - always show in spec mode with study, grayed when no changes */}
|
||||
{useSpecMode && spec && (
|
||||
<button
|
||||
onClick={saveToConfig}
|
||||
disabled={isSaving || (useSpecMode ? !specIsDirty : !hasUnsavedChanges)}
|
||||
disabled={isSaving || !specIsDirty}
|
||||
className={`px-3 py-1.5 text-sm rounded-lg transition-colors flex items-center gap-1.5 ${
|
||||
(useSpecMode ? specIsDirty : hasUnsavedChanges)
|
||||
specIsDirty
|
||||
? 'bg-green-600 hover:bg-green-500 text-white'
|
||||
: 'bg-dark-700 text-dark-400 cursor-not-allowed border border-dark-600'
|
||||
}`}
|
||||
title={(useSpecMode ? specIsDirty : hasUnsavedChanges) ? `Save changes to ${useSpecMode ? 'atomizer_spec.json' : 'optimization_config.json'}` : 'No changes to save'}
|
||||
title={specIsDirty ? 'Save changes to atomizer_spec.json' : 'No changes to save'}
|
||||
>
|
||||
<Save size={14} />
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Legacy Save Button */}
|
||||
{!useSpecMode && activeStudyId && (
|
||||
<button
|
||||
onClick={saveToConfig}
|
||||
disabled={isSaving || !hasUnsavedChanges}
|
||||
className={`px-3 py-1.5 text-sm rounded-lg transition-colors flex items-center gap-1.5 ${
|
||||
hasUnsavedChanges
|
||||
? 'bg-green-600 hover:bg-green-500 text-white'
|
||||
: 'bg-dark-700 text-dark-400 cursor-not-allowed border border-dark-600'
|
||||
}`}
|
||||
title={hasUnsavedChanges ? 'Save changes to optimization_config.json' : 'No changes to save'}
|
||||
>
|
||||
<Save size={14} />
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
@@ -314,7 +331,7 @@ export function CanvasView() {
|
||||
)}
|
||||
|
||||
{/* Reload Button */}
|
||||
{activeStudyId && (
|
||||
{(useSpecMode ? spec : activeStudyId) && (
|
||||
<button
|
||||
onClick={handleReload}
|
||||
disabled={isLoading || specLoading}
|
||||
|
||||
Reference in New Issue
Block a user