chore: Project cleanup and Canvas UX improvements (Phase 7-9)

## Cleanup (v0.5.0)
- Delete 102+ orphaned MCP session temp files
- Remove build artifacts (htmlcov, dist, __pycache__)
- Archive superseded plan docs (RALPH_LOOP V2/V3, CANVAS V3, etc.)
- Move debug/analysis scripts from tests/ to tools/analysis/
- Archive redundant NX journals to archive/nx_journals/
- Archive monolithic PROTOCOL.md to docs/archive/
- Update .gitignore with missing patterns
- Clean old study files (optimization_log_old.txt, run_optimization_old.py)

## Canvas UX (Phases 7-9)
- Phase 7: Resizable panels with localStorage persistence
  - Left sidebar: 200-400px, Right panel: 280-600px
  - New useResizablePanel hook and ResizeHandle component
- Phase 8: Enable all palette items
  - All 8 node types now draggable
  - Singleton logic for model/solver/algorithm/surrogate
- Phase 9: Solver configuration
  - Add SolverEngine type (nxnastran, mscnastran, python, etc.)
  - Add NastranSolutionType (SOL101-SOL200)
  - Engine/solution dropdowns in config panel
  - Python script path support

## Documentation
- Update CHANGELOG.md with recent versions
- Update docs/00_INDEX.md
- Create examples/README.md
- Add docs/plans/CANVAS_UX_IMPROVEMENTS.md
This commit is contained in:
2026-01-24 15:17:34 -05:00
parent 2cb8dccc3a
commit a3f18dc377
38 changed files with 1172 additions and 2570 deletions

View File

@@ -0,0 +1,67 @@
/**
* ResizeHandle - Visual drag handle for resizable panels
*
* A thin vertical bar that can be dragged to resize panels.
* Shows visual feedback on hover and during drag.
*/
import { memo } from 'react';
interface ResizeHandleProps {
/** Mouse down handler to start dragging */
onMouseDown: (e: React.MouseEvent) => void;
/** Double click handler to reset size */
onDoubleClick?: () => void;
/** Whether panel is currently being dragged */
isDragging?: boolean;
/** Position of the handle ('left' or 'right' edge of the panel) */
position?: 'left' | 'right';
}
function ResizeHandleComponent({
onMouseDown,
onDoubleClick,
isDragging = false,
position = 'right',
}: ResizeHandleProps) {
return (
<div
className={`
absolute top-0 bottom-0 w-1 z-30
cursor-col-resize
transition-colors duration-150
${position === 'right' ? 'right-0' : 'left-0'}
${isDragging
? 'bg-primary-500'
: 'bg-transparent hover:bg-primary-500/50'
}
`}
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
title="Drag to resize, double-click to reset"
>
{/* Wider hit area for easier grabbing */}
<div
className={`
absolute top-0 bottom-0 w-3
${position === 'right' ? '-left-1' : '-right-1'}
`}
/>
{/* Visual indicator dots (shown on hover via CSS) */}
<div className={`
absolute top-1/2 -translate-y-1/2
${position === 'right' ? '-left-0.5' : '-right-0.5'}
flex flex-col gap-1 opacity-0 hover:opacity-100 transition-opacity
${isDragging ? 'opacity-100' : ''}
`}>
<div className="w-1 h-1 rounded-full bg-dark-400" />
<div className="w-1 h-1 rounded-full bg-dark-400" />
<div className="w-1 h-1 rounded-full bg-dark-400" />
</div>
</div>
);
}
export const ResizeHandle = memo(ResizeHandleComponent);
export default ResizeHandle;

View File

@@ -49,16 +49,23 @@ import { validateSpec, canRunOptimization } from '../../lib/validation/specValid
// Drag-Drop Helpers
// ============================================================================
/** Addable node types via drag-drop */
const ADDABLE_NODE_TYPES = ['designVar', 'extractor', 'objective', 'constraint'] as const;
import { SINGLETON_TYPES } from './palette/NodePalette';
/** All node types that can be added via drag-drop */
const ADDABLE_NODE_TYPES = ['model', 'solver', 'designVar', 'extractor', 'objective', 'constraint', 'algorithm', 'surrogate'] as const;
type AddableNodeType = typeof ADDABLE_NODE_TYPES[number];
function isAddableNodeType(type: string): type is AddableNodeType {
return ADDABLE_NODE_TYPES.includes(type as AddableNodeType);
}
/** Check if a node type is a singleton (only one allowed) */
function isSingletonType(type: string): boolean {
return SINGLETON_TYPES.includes(type as typeof SINGLETON_TYPES[number]);
}
/** Maps canvas NodeType to spec API type */
function mapNodeTypeToSpecType(type: AddableNodeType): 'designVar' | 'extractor' | 'objective' | 'constraint' {
function mapNodeTypeToSpecType(type: AddableNodeType): 'designVar' | 'extractor' | 'objective' | 'constraint' | 'model' | 'solver' | 'algorithm' | 'surrogate' {
return type;
}
@@ -67,6 +74,22 @@ function getDefaultNodeData(type: AddableNodeType, position: { x: number; y: num
const timestamp = Date.now();
switch (type) {
case 'model':
return {
name: 'Model',
sim: {
path: '',
solver: 'nastran',
},
canvas_position: position,
};
case 'solver':
return {
name: 'Solver',
engine: 'nxnastran',
solution_type: 'SOL101',
canvas_position: position,
};
case 'designVar':
return {
name: `variable_${timestamp}`,
@@ -130,6 +153,23 @@ function getDefaultNodeData(type: AddableNodeType, position: { x: number; y: num
enabled: true,
canvas_position: position,
};
case 'algorithm':
return {
name: 'Algorithm',
type: 'TPE',
budget: {
max_trials: 100,
},
canvas_position: position,
};
case 'surrogate':
return {
name: 'Surrogate',
enabled: false,
model_type: 'MLP',
min_trials: 20,
canvas_position: position,
};
}
}
@@ -602,6 +642,18 @@ function SpecRendererInner({
return;
}
// Check if this is a singleton type that already exists
if (isSingletonType(type)) {
const existingNode = localNodes.find(n => n.type === type);
if (existingNode) {
// Select the existing node instead of creating a duplicate
selectNode(existingNode.id);
// Show a toast notification would be nice here
console.log(`${type} already exists - selected existing node`);
return;
}
}
// Convert screen position to flow position
const position = reactFlowInstance.current.screenToFlowPosition({
x: event.clientX,
@@ -612,8 +664,19 @@ function SpecRendererInner({
const nodeData = getDefaultNodeData(type, position);
const specType = mapNodeTypeToSpecType(type);
// For structural types (model, solver, algorithm, surrogate), these are
// part of the spec structure rather than array items. Handle differently.
const structuralTypes = ['model', 'solver', 'algorithm', 'surrogate'];
if (structuralTypes.includes(type)) {
// These nodes are derived from spec structure - they shouldn't be "added"
// They already exist if the spec has that section configured
console.log(`${type} is a structural node - configure via spec directly`);
setError(`${type} nodes are configured via the spec. Use the config panel to edit.`);
return;
}
try {
const nodeId = await addNode(specType, nodeData);
const nodeId = await addNode(specType as 'designVar' | 'extractor' | 'objective' | 'constraint', nodeData);
// Select the newly created node
selectNode(nodeId);
} catch (err) {
@@ -621,7 +684,7 @@ function SpecRendererInner({
setError(err instanceof Error ? err.message : 'Failed to add node');
}
},
[editable, addNode, selectNode, setError]
[editable, addNode, selectNode, setError, localNodes]
);
// Loading state

View File

@@ -1,14 +1,44 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { Cpu } from 'lucide-react';
import { Cpu, Terminal } from 'lucide-react';
import { BaseNode } from './BaseNode';
import { SolverNodeData } from '../../../lib/canvas/schema';
import { SolverNodeData, SolverEngine } from '../../../lib/canvas/schema';
// Human-readable engine names
const ENGINE_LABELS: Record<SolverEngine, string> = {
nxnastran: 'NX Nastran',
mscnastran: 'MSC Nastran',
python: 'Python Script',
abaqus: 'Abaqus',
ansys: 'ANSYS',
};
function SolverNodeComponent(props: NodeProps<SolverNodeData>) {
const { data } = props;
// Build display string: "Engine - SolutionType" or just one
const engineLabel = data.engine ? ENGINE_LABELS[data.engine] : null;
const solverTypeLabel = data.solverType || null;
let displayText: string;
if (engineLabel && solverTypeLabel) {
displayText = `${engineLabel} (${solverTypeLabel})`;
} else if (engineLabel) {
displayText = engineLabel;
} else if (solverTypeLabel) {
displayText = solverTypeLabel;
} else {
displayText = 'Configure solver';
}
// Use Terminal icon for Python, Cpu for others
const icon = data.engine === 'python'
? <Terminal size={16} />
: <Cpu size={16} />;
return (
<BaseNode {...props} icon={<Cpu size={16} />} iconColor="text-violet-400">
{data.solverType || 'Select solution'}
<BaseNode {...props} icon={icon} iconColor="text-violet-400">
{displayText}
</BaseNode>
);
}

View File

@@ -54,6 +54,9 @@ export interface NodePaletteProps {
// Constants
// ============================================================================
/** Singleton node types - only one of each allowed on canvas */
export const SINGLETON_TYPES: NodeType[] = ['model', 'solver', 'algorithm', 'surrogate'];
export const PALETTE_ITEMS: PaletteItem[] = [
{
type: 'model',
@@ -61,15 +64,15 @@ export const PALETTE_ITEMS: PaletteItem[] = [
icon: Box,
description: 'NX model file (.prt, .sim)',
color: 'text-blue-400',
canAdd: false, // Synthetic - derived from spec
canAdd: true, // Singleton - only one allowed
},
{
type: 'solver',
label: 'Solver',
icon: Cpu,
description: 'Nastran solution type',
description: 'Analysis solver config',
color: 'text-violet-400',
canAdd: false, // Synthetic - derived from model
canAdd: true, // Singleton - only one allowed
},
{
type: 'designVar',
@@ -109,7 +112,7 @@ export const PALETTE_ITEMS: PaletteItem[] = [
icon: BrainCircuit,
description: 'Optimization method',
color: 'text-indigo-400',
canAdd: false, // Synthetic - derived from spec.optimization
canAdd: true, // Singleton - only one allowed
},
{
type: 'surrogate',
@@ -117,7 +120,7 @@ export const PALETTE_ITEMS: PaletteItem[] = [
icon: Rocket,
description: 'Neural acceleration',
color: 'text-pink-400',
canAdd: false, // Synthetic - derived from spec.optimization.surrogate
canAdd: true, // Singleton - only one allowed
},
];

View File

@@ -315,18 +315,106 @@ function ModelNodeConfig({ spec }: SpecConfigProps) {
}
function SolverNodeConfig({ spec }: SpecConfigProps) {
const { patchSpec } = useSpecStore();
const [isUpdating, setIsUpdating] = useState(false);
const engine = spec.model.sim?.engine || 'nxnastran';
const solutionType = spec.model.sim?.solution_type || 'SOL101';
const scriptPath = spec.model.sim?.script_path || '';
const isPython = engine === 'python';
const handleEngineChange = async (newEngine: string) => {
setIsUpdating(true);
try {
await patchSpec('model.sim.engine', newEngine);
} catch (err) {
console.error('Failed to update engine:', err);
} finally {
setIsUpdating(false);
}
};
const handleSolutionTypeChange = async (newType: string) => {
setIsUpdating(true);
try {
await patchSpec('model.sim.solution_type', newType);
} catch (err) {
console.error('Failed to update solution type:', err);
} finally {
setIsUpdating(false);
}
};
const handleScriptPathChange = async (newPath: string) => {
setIsUpdating(true);
try {
await patchSpec('model.sim.script_path', newPath);
} catch (err) {
console.error('Failed to update script path:', err);
} finally {
setIsUpdating(false);
}
};
return (
<div>
<label className={labelClass}>Solution Type</label>
<input
type="text"
value={spec.model.sim?.solution_type || 'Not configured'}
readOnly
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
title="Solver type is determined by the model file."
/>
<p className="text-xs text-dark-500 mt-1">Detected from model file.</p>
</div>
<>
{isUpdating && (
<div className="text-xs text-primary-400 animate-pulse">Updating...</div>
)}
<div>
<label className={labelClass}>Solver Engine</label>
<select
value={engine}
onChange={(e) => handleEngineChange(e.target.value)}
className={selectClass}
>
<option value="nxnastran">NX Nastran (built-in)</option>
<option value="mscnastran">MSC Nastran (external)</option>
<option value="python">Python Script</option>
<option value="abaqus" disabled>Abaqus (coming soon)</option>
<option value="ansys" disabled>ANSYS (coming soon)</option>
</select>
<p className="text-xs text-dark-500 mt-1">
{isPython ? 'Run custom Python analysis script' : 'Select FEA solver software'}
</p>
</div>
{!isPython && (
<div>
<label className={labelClass}>Solution Type</label>
<select
value={solutionType}
onChange={(e) => handleSolutionTypeChange(e.target.value)}
className={selectClass}
>
<option value="SOL101">SOL101 - Linear Statics</option>
<option value="SOL103">SOL103 - Normal Modes</option>
<option value="SOL105">SOL105 - Buckling</option>
<option value="SOL106">SOL106 - Nonlinear Statics</option>
<option value="SOL111">SOL111 - Modal Frequency Response</option>
<option value="SOL112">SOL112 - Modal Transient Response</option>
<option value="SOL200">SOL200 - Design Optimization</option>
</select>
</div>
)}
{isPython && (
<div>
<label className={labelClass}>Script Path</label>
<input
type="text"
value={scriptPath}
onChange={(e) => handleScriptPathChange(e.target.value)}
placeholder="path/to/solver_script.py"
className={`${inputClass} font-mono text-sm`}
/>
<p className="text-xs text-dark-500 mt-1">
Python script must define solve(params) function
</p>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,156 @@
/**
* useResizablePanel - Hook for creating resizable panels with persistence
*
* Features:
* - Drag to resize
* - Min/max constraints
* - localStorage persistence
* - Double-click to reset to default
*/
import { useState, useCallback, useEffect, useRef } from 'react';
export interface ResizablePanelConfig {
/** Unique key for localStorage persistence */
storageKey: string;
/** Default width in pixels */
defaultWidth: number;
/** Minimum width in pixels */
minWidth: number;
/** Maximum width in pixels */
maxWidth: number;
/** Side of the panel ('left' or 'right') - affects resize direction */
side: 'left' | 'right';
}
export interface ResizablePanelState {
/** Current width in pixels */
width: number;
/** Whether user is currently dragging */
isDragging: boolean;
/** Start drag handler - attach to resize handle mousedown */
startDrag: (e: React.MouseEvent) => void;
/** Reset to default width */
resetWidth: () => void;
/** Set width programmatically */
setWidth: (width: number) => void;
}
const STORAGE_PREFIX = 'atomizer-panel-';
function getStoredWidth(key: string, defaultWidth: number): number {
if (typeof window === 'undefined') return defaultWidth;
try {
const stored = localStorage.getItem(STORAGE_PREFIX + key);
if (stored) {
const parsed = parseInt(stored, 10);
if (!isNaN(parsed)) return parsed;
}
} catch {
// localStorage not available
}
return defaultWidth;
}
function storeWidth(key: string, width: number): void {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(STORAGE_PREFIX + key, String(width));
} catch {
// localStorage not available
}
}
export function useResizablePanel(config: ResizablePanelConfig): ResizablePanelState {
const { storageKey, defaultWidth, minWidth, maxWidth, side } = config;
// Initialize from localStorage
const [width, setWidthState] = useState(() => {
const stored = getStoredWidth(storageKey, defaultWidth);
return Math.max(minWidth, Math.min(maxWidth, stored));
});
const [isDragging, setIsDragging] = useState(false);
// Track initial position for drag calculation
const dragStartRef = useRef<{ x: number; width: number } | null>(null);
// Clamp width within bounds
const clampWidth = useCallback((w: number) => {
return Math.max(minWidth, Math.min(maxWidth, w));
}, [minWidth, maxWidth]);
// Set width with clamping and persistence
const setWidth = useCallback((newWidth: number) => {
const clamped = clampWidth(newWidth);
setWidthState(clamped);
storeWidth(storageKey, clamped);
}, [clampWidth, storageKey]);
// Reset to default
const resetWidth = useCallback(() => {
setWidth(defaultWidth);
}, [defaultWidth, setWidth]);
// Start drag handler
const startDrag = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
dragStartRef.current = { x: e.clientX, width };
}, [width]);
// Handle mouse move during drag
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
if (!dragStartRef.current) return;
const delta = e.clientX - dragStartRef.current.x;
// For left panels, positive delta increases width
// For right panels, negative delta increases width
const newWidth = side === 'left'
? dragStartRef.current.width + delta
: dragStartRef.current.width - delta;
setWidthState(clampWidth(newWidth));
};
const handleMouseUp = () => {
if (dragStartRef.current) {
// Persist the final width
storeWidth(storageKey, width);
}
setIsDragging(false);
dragStartRef.current = null;
};
// Add listeners to document for smooth dragging
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Change cursor globally during drag
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isDragging, side, clampWidth, storageKey, width]);
return {
width,
isDragging,
startDrag,
resetWidth,
setWidth,
};
}
export default useResizablePanel;

View File

@@ -25,9 +25,17 @@ export interface ModelNodeData extends BaseNodeData {
fileType?: 'prt' | 'fem' | 'sim';
}
export type SolverEngine = 'nxnastran' | 'mscnastran' | 'python' | 'abaqus' | 'ansys';
export type NastranSolutionType = 'SOL101' | 'SOL103' | 'SOL105' | 'SOL106' | 'SOL111' | 'SOL112' | 'SOL200';
export interface SolverNodeData extends BaseNodeData {
type: 'solver';
solverType?: 'SOL101' | 'SOL103' | 'SOL105' | 'SOL106' | 'SOL111' | 'SOL112';
/** Solver engine (nxnastran, mscnastran, python, etc.) */
engine?: SolverEngine;
/** Solution type for Nastran solvers */
solverType?: NastranSolutionType;
/** Python script path (for python engine) */
scriptPath?: string;
}
export interface DesignVarNodeData extends BaseNodeData {

View File

@@ -11,8 +11,10 @@ import { NodeConfigPanel } from '../components/canvas/panels/NodeConfigPanel';
import { NodeConfigPanelV2 } from '../components/canvas/panels/NodeConfigPanelV2';
import { ChatPanel } from '../components/canvas/panels/ChatPanel';
import { PanelContainer } from '../components/canvas/panels/PanelContainer';
import { ResizeHandle } from '../components/canvas/ResizeHandle';
import { useCanvasStore } from '../hooks/useCanvasStore';
import { useSpecStore, useSpec, useSpecLoading, useSpecIsDirty, useSelectedNodeId } from '../hooks/useSpecStore';
import { useResizablePanel } from '../hooks/useResizablePanel';
// usePanelStore is now used by child components - PanelContainer handles panels
import { useSpecUndoRedo, useUndoRedoKeyboard } from '../hooks/useSpecUndoRedo';
import { useStudy } from '../context/StudyContext';
@@ -31,6 +33,23 @@ export function CanvasView() {
const [paletteCollapsed, setPaletteCollapsed] = useState(false);
const [leftSidebarTab, setLeftSidebarTab] = useState<'components' | 'files'>('components');
const navigate = useNavigate();
// Resizable panels
const leftPanel = useResizablePanel({
storageKey: 'left-sidebar',
defaultWidth: 240,
minWidth: 200,
maxWidth: 400,
side: 'left',
});
const rightPanel = useResizablePanel({
storageKey: 'right-panel',
defaultWidth: 384,
minWidth: 280,
maxWidth: 600,
side: 'right',
});
const [searchParams] = useSearchParams();
// Spec mode is the default (AtomizerSpec v2.0)
@@ -423,7 +442,10 @@ export function CanvasView() {
<main className="flex-1 overflow-hidden flex">
{/* Left Sidebar with tabs (spec mode only - AtomizerCanvas has its own) */}
{useSpecMode && (
<div className={`${paletteCollapsed ? 'w-14' : 'w-60'} bg-dark-850 border-r border-dark-700 flex flex-col transition-all duration-200`}>
<div
className="relative bg-dark-850 border-r border-dark-700 flex flex-col"
style={{ width: paletteCollapsed ? 56 : leftPanel.width }}
>
{/* Tab buttons (only show when expanded) */}
{!paletteCollapsed && (
<div className="flex border-b border-dark-700">
@@ -469,6 +491,16 @@ export function CanvasView() {
/>
)}
</div>
{/* Resize handle (only when not collapsed) */}
{!paletteCollapsed && (
<ResizeHandle
onMouseDown={leftPanel.startDrag}
onDoubleClick={leftPanel.resetWidth}
isDragging={leftPanel.isDragging}
position="right"
/>
)}
</div>
)}
@@ -494,14 +526,35 @@ export function CanvasView() {
{/* Shows INSTEAD of chat when a node is selected */}
{selectedNodeId ? (
useSpecMode ? (
<NodeConfigPanelV2 onClose={() => useSpecStore.getState().clearSelection()} />
<div
className="relative border-l border-dark-700 bg-dark-850 flex flex-col"
style={{ width: rightPanel.width }}
>
<ResizeHandle
onMouseDown={rightPanel.startDrag}
onDoubleClick={rightPanel.resetWidth}
isDragging={rightPanel.isDragging}
position="left"
/>
<NodeConfigPanelV2 onClose={() => useSpecStore.getState().clearSelection()} />
</div>
) : (
<div className="w-80 border-l border-dark-700 bg-dark-850 overflow-y-auto">
<NodeConfigPanel nodeId={selectedNodeId} />
</div>
)
) : showChat ? (
<div className="w-96 border-l border-dark-700 bg-dark-850 flex flex-col">
<div
className="relative border-l border-dark-700 bg-dark-850 flex flex-col"
style={{ width: rightPanel.width }}
>
{/* Resize handle */}
<ResizeHandle
onMouseDown={rightPanel.startDrag}
onDoubleClick={rightPanel.resetWidth}
isDragging={rightPanel.isDragging}
position="left"
/>
{/* Chat Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700">
<div className="flex items-center gap-2">

View File

@@ -39,6 +39,10 @@ export interface SpecMeta {
tags?: string[];
/** Real-world engineering context */
engineering_context?: string;
/** Current workflow status */
status?: 'draft' | 'introspected' | 'configured' | 'validated' | 'ready' | 'running' | 'completed' | 'failed';
/** Topic/folder for organization */
topic?: string;
}
// ============================================================================
@@ -64,6 +68,29 @@ export interface FemConfig {
}
export type SolverType = 'nastran' | 'NX_Nastran' | 'abaqus';
/**
* SolverEngine - The actual solver software used for analysis
* - nxnastran: NX Nastran (built into Siemens NX)
* - mscnastran: MSC Nastran (external)
* - python: Custom Python script
* - abaqus: Abaqus (future)
* - ansys: ANSYS (future)
*/
export type SolverEngine = 'nxnastran' | 'mscnastran' | 'python' | 'abaqus' | 'ansys';
/**
* NastranSolutionType - Common Nastran solution types
*/
export type NastranSolutionType =
| 'SOL101' // Linear Statics
| 'SOL103' // Normal Modes
| 'SOL105' // Buckling
| 'SOL106' // Nonlinear Statics
| 'SOL111' // Modal Frequency Response
| 'SOL112' // Modal Transient Response
| 'SOL200'; // Design Optimization
export type SubcaseType = 'static' | 'modal' | 'thermal' | 'buckling';
export interface Subcase {
@@ -75,10 +102,14 @@ export interface Subcase {
export interface SimConfig {
/** Path to .sim file */
path: string;
/** Solver type */
/** Solver type (legacy, use engine instead) */
solver: SolverType;
/** Solver engine software */
engine?: SolverEngine;
/** Solution type (e.g., SOL101) */
solution_type?: string;
solution_type?: NastranSolutionType | string;
/** Python script path (for python engine) */
script_path?: string;
/** Defined subcases */
subcases?: Subcase[];
}
@@ -89,11 +120,40 @@ export interface NxSettings {
auto_start_nx?: boolean;
}
export interface IntrospectionExpression {
name: string;
value: number | null;
units: string | null;
formula: string | null;
is_candidate: boolean;
confidence: number;
}
export interface IntrospectionData {
timestamp: string;
solver_type: string | null;
mass_kg: number | null;
volume_mm3: number | null;
expressions: IntrospectionExpression[];
warnings: string[];
baseline: {
timestamp: string;
solve_time_seconds: number;
mass_kg: number | null;
max_displacement_mm: number | null;
max_stress_mpa: number | null;
success: boolean;
error: string | null;
} | null;
}
export interface ModelConfig {
nx_part?: NxPartConfig;
prt?: NxPartConfig;
fem?: FemConfig;
sim: SimConfig;
sim?: SimConfig;
nx_settings?: NxSettings;
introspection?: IntrospectionData;
}
// ============================================================================