# Canvas UX Improvements - Master Plan **Created:** January 2026 **Status:** Planning **Branch:** `feature/studio-enhancement` ## Overview This plan addresses three major UX issues in the Canvas Builder: 1. **Resizable Panels** - Right pane (chat/config) is fixed at 384px, cannot be adjusted 2. **Disabled Palette Items** - Model, Solver, Algorithm, Surrogate are grayed out and not draggable 3. **Solver Type Selection** - Solver node should allow selection of solver type (NX Nastran, Python, etc.) --- ## Phase 7: Resizable Panels ### Current State - Left sidebar: Fixed 240px (expanded) or 56px (collapsed) - Right panel (Chat/Config): Fixed 384px - Canvas: Takes remaining space ### Requirements - Users should be able to drag panel edges to resize - Minimum/maximum constraints for usability - Persist panel sizes in localStorage - Smooth resize with proper cursor feedback ### Implementation #### 7.1 Create Resizable Panel Hook ```typescript // hooks/useResizablePanel.ts interface ResizablePanelState { width: number; isDragging: boolean; startDrag: (e: React.MouseEvent) => void; } function useResizablePanel( key: string, defaultWidth: number, minWidth: number, maxWidth: number ): ResizablePanelState ``` #### 7.2 Update CanvasView Layout - Wrap left sidebar with resizer - Wrap right panel with resizer - Add visual drag handles (thin border that highlights on hover) - Add cursor: col-resize on hover #### 7.3 Files to Modify | File | Changes | |------|---------| | `hooks/useResizablePanel.ts` | NEW - Resize hook with localStorage persistence | | `pages/CanvasView.tsx` | Add resizers to left/right panels | | `components/canvas/ResizeHandle.tsx` | NEW - Visual resize handle component | #### 7.4 Constraints | Panel | Min | Default | Max | |-------|-----|---------|-----| | Left (Palette/Files) | 200px | 240px | 400px | | Right (Chat/Config) | 280px | 384px | 600px | --- ## Phase 8: Enable All Palette Items ### Current State - Model, Solver, Algorithm, Surrogate are marked `canAdd: false` - They appear grayed out with "Auto-created" text - Users cannot drag them to canvas ### Problem Analysis These nodes were marked as "synthetic" because they're derived from: - **Model**: From `spec.model.sim.path` - **Solver**: From model's solution type - **Algorithm**: From `spec.optimization.algorithm` - **Surrogate**: From `spec.optimization.surrogate` However, users need to: 1. Add a Model node when creating a new study from scratch 2. Configure the Solver type 3. Choose an Algorithm 4. Enable/configure Surrogate ### Solution: Make All Items Draggable #### 8.1 Update NodePalette ```typescript // All items should be draggable export const PALETTE_ITEMS: PaletteItem[] = [ { type: 'model', label: 'Model', canAdd: true, // Changed from false description: 'NX/FEM model file', }, { type: 'solver', label: 'Solver', canAdd: true, // Changed from false description: 'Analysis solver', }, // ... etc ]; ``` #### 8.2 Handle "Singleton" Nodes Some nodes should only exist once on the canvas: - Model (only one model per study) - Solver (one solver) - Algorithm (one algorithm config) - Surrogate (optional, one) When user drags a singleton that already exists: - Option A: Show warning toast "Model already exists" - Option B: Select the existing node instead of creating new - **Recommended**: Option B (select existing) #### 8.3 Update SpecRenderer Drop Handler ```typescript const onDrop = useCallback(async (event: DragEvent) => { const type = event.dataTransfer.getData('application/reactflow'); // Check if singleton already exists const SINGLETON_TYPES = ['model', 'solver', 'algorithm', 'surrogate']; if (SINGLETON_TYPES.includes(type)) { const existingNode = nodes.find(n => n.type === type); if (existingNode) { selectNode(existingNode.id); showNotification(`${type} already exists - selected it`); return; } } // Create new node... }, [...]); ``` #### 8.4 Default Data for New Node Types ```typescript function getDefaultNodeData(type: NodeType, position) { switch (type) { case 'model': return { name: 'Model', sim: { path: '', solver: 'nastran' }, canvas_position: position, }; case 'solver': return { name: 'Solver', type: 'nxnastran', // Default solver solution_type: 'SOL101', 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, }; // ... existing cases } } ``` #### 8.5 Files to Modify | File | Changes | |------|---------| | `components/canvas/palette/NodePalette.tsx` | Set `canAdd: true` for all items | | `components/canvas/SpecRenderer.tsx` | Handle singleton logic in onDrop | | `lib/spec/converter.ts` | Ensure synthetic nodes have proper IDs | | `hooks/useSpecStore.ts` | Add model/solver/algorithm to addNode support | --- ## Phase 9: Solver Type Selection ### Current State - Solver node shows auto-detected solution type (SOL101, etc.) - No ability to change solver engine or configure it ### Requirements 1. Allow selection of solver engine type 2. Configure solution type 3. Support future solver types ### Solver Types to Support | Solver | Description | Status | |--------|-------------|--------| | `nxnastran` | NX Nastran (built-in) | Current | | `mscnastran` | MSC Nastran (external) | Future | | `python` | Python-based solver | Future | | `abaqus` | Abaqus (via Python API) | Future | | `ansys` | ANSYS (via Python API) | Future | ### Solution Types per Solver **NX Nastran / MSC Nastran:** - SOL101 - Linear Static - SOL103 - Normal Modes - SOL105 - Buckling - SOL106 - Nonlinear Static - SOL111 - Frequency Response - SOL112 - Transient Response - SOL200 - Design Optimization **Python Solver:** - Custom (user-defined) ### Schema Updates #### 9.1 Update AtomizerSpec Types ```typescript // types/atomizer-spec.ts export type SolverEngine = | 'nxnastran' | 'mscnastran' | 'python' | 'abaqus' | 'ansys'; export type NastranSolutionType = | 'SOL101' | 'SOL103' | 'SOL105' | 'SOL106' | 'SOL111' | 'SOL112' | 'SOL200'; export interface SolverConfig { /** Solver engine type */ engine: SolverEngine; /** Solution type (for Nastran) */ solution_type?: NastranSolutionType; /** Custom solver script path (for Python solver) */ script_path?: string; /** Additional solver options */ options?: Record; } export interface Model { sim?: { path: string; solver: SolverConfig; // Changed from just 'nastran' string }; // ... } ``` #### 9.2 Update SolverNode Component ```typescript // components/canvas/nodes/SolverNode.tsx function SolverNodeComponent(props: NodeProps) { const { data } = props; return ( } iconColor="text-violet-400">
{data.engine || 'nxnastran'} {data.solution_type || 'Auto-detect'}
); } ``` #### 9.3 Solver Configuration Panel Add to `NodeConfigPanelV2.tsx`: ```typescript function SolverNodeConfig({ spec }: SpecConfigProps) { const { patchSpec } = useSpecStore(); const solver = spec.model?.sim?.solver || { engine: 'nxnastran' }; const handleEngineChange = (engine: SolverEngine) => { patchSpec('model.sim.solver.engine', engine); }; const handleSolutionTypeChange = (type: NastranSolutionType) => { patchSpec('model.sim.solver.solution_type', type); }; return ( <>
{(solver.engine === 'nxnastran' || solver.engine === 'mscnastran') && (
)} {solver.engine === 'python' && (
patchSpec('model.sim.solver.script_path', e.target.value)} placeholder="/path/to/solver.py" className={inputClass} />

Python script that runs the analysis

)} ); } ``` #### 9.4 Files to Modify | File | Changes | |------|---------| | `types/atomizer-spec.ts` | Add SolverEngine, SolverConfig types | | `components/canvas/nodes/SolverNode.tsx` | Show engine and solution type | | `components/canvas/panels/NodeConfigPanelV2.tsx` | Add SolverNodeConfig | | `lib/canvas/schema.ts` | Update SolverNodeData | | Backend: `config/spec_models.py` | Add SolverConfig Pydantic model | --- ## Implementation Order | Phase | Effort | Priority | Dependencies | |-------|--------|----------|--------------| | **7.1** Resizable Panel Hook | 2h | High | None | | **7.2** CanvasView Resizers | 2h | High | 7.1 | | **8.1** Enable Palette Items | 1h | High | None | | **8.2** Singleton Logic | 2h | High | 8.1 | | **8.3** Default Node Data | 1h | High | 8.2 | | **9.1** Schema Updates | 2h | Medium | None | | **9.2** SolverNode UI | 1h | Medium | 9.1 | | **9.3** Solver Config Panel | 2h | Medium | 9.1, 9.2 | **Total Estimated Effort:** ~13 hours --- ## Success Criteria ### Phase 7 (Resizable Panels) - [ ] Left panel can be resized between 200-400px - [ ] Right panel can be resized between 280-600px - [ ] Resize handles show cursor feedback - [ ] Panel sizes persist across page reload - [ ] Double-click on handle resets to default ### Phase 8 (Enable Palette Items) - [ ] All 8 node types are draggable from palette - [ ] Dragging singleton to canvas with existing node selects existing - [ ] Toast notification explains the behavior - [ ] New studies can start with empty canvas and add Model first ### Phase 9 (Solver Selection) - [ ] Solver node shows engine type (nxnastran, python, etc.) - [ ] Clicking solver node opens config panel - [ ] Can select solver engine from dropdown - [ ] Nastran solvers show solution type dropdown - [ ] Python solver shows script path input - [ ] Changes persist to atomizer_spec.json --- ## Future Considerations ### Additional Solver Support - ANSYS integration via pyANSYS - Abaqus integration via abaqus-python - OpenFOAM for CFD - Custom Python solvers with standardized interface ### Multi-Solver Workflows - Support for chained solvers (thermal → structural) - Co-simulation workflows - Parallel solver execution ### Algorithm Node Enhancement - Similar to Solver, allow algorithm selection - Show algorithm-specific parameters - Support custom algorithms --- ## Commit Strategy ```bash # Phase 7 git commit -m "feat: Add resizable panels to canvas view" # Phase 8 git commit -m "feat: Enable all palette items with singleton handling" # Phase 9 git commit -m "feat: Add solver type selection and configuration" ```