446 lines
12 KiB
Markdown
446 lines
12 KiB
Markdown
|
|
# 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<string, unknown>;
|
||
|
|
}
|
||
|
|
|
||
|
|
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<SolverNodeData>) {
|
||
|
|
const { data } = props;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<BaseNode {...props} icon={<Cpu size={16} />} iconColor="text-violet-400">
|
||
|
|
<div className="flex flex-col gap-1">
|
||
|
|
<span className="text-sm font-medium">{data.engine || 'nxnastran'}</span>
|
||
|
|
<span className="text-xs text-dark-400">
|
||
|
|
{data.solution_type || 'Auto-detect'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</BaseNode>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 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 (
|
||
|
|
<>
|
||
|
|
<div>
|
||
|
|
<label className={labelClass}>Solver Engine</label>
|
||
|
|
<select
|
||
|
|
value={solver.engine}
|
||
|
|
onChange={(e) => handleEngineChange(e.target.value as SolverEngine)}
|
||
|
|
className={selectClass}
|
||
|
|
>
|
||
|
|
<option value="nxnastran">NX Nastran</option>
|
||
|
|
<option value="mscnastran">MSC Nastran</option>
|
||
|
|
<option value="python">Python Script</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{(solver.engine === 'nxnastran' || solver.engine === 'mscnastran') && (
|
||
|
|
<div>
|
||
|
|
<label className={labelClass}>Solution Type</label>
|
||
|
|
<select
|
||
|
|
value={solver.solution_type || ''}
|
||
|
|
onChange={(e) => handleSolutionTypeChange(e.target.value as NastranSolutionType)}
|
||
|
|
className={selectClass}
|
||
|
|
>
|
||
|
|
<option value="">Auto-detect from model</option>
|
||
|
|
<option value="SOL101">SOL101 - Linear Static</option>
|
||
|
|
<option value="SOL103">SOL103 - Normal Modes</option>
|
||
|
|
<option value="SOL105">SOL105 - Buckling</option>
|
||
|
|
<option value="SOL106">SOL106 - Nonlinear Static</option>
|
||
|
|
<option value="SOL111">SOL111 - Frequency Response</option>
|
||
|
|
<option value="SOL112">SOL112 - Transient Response</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{solver.engine === 'python' && (
|
||
|
|
<div>
|
||
|
|
<label className={labelClass}>Solver Script</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={solver.script_path || ''}
|
||
|
|
onChange={(e) => patchSpec('model.sim.solver.script_path', e.target.value)}
|
||
|
|
placeholder="/path/to/solver.py"
|
||
|
|
className={inputClass}
|
||
|
|
/>
|
||
|
|
<p className="text-xs text-dark-500 mt-1">
|
||
|
|
Python script that runs the analysis
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 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"
|
||
|
|
```
|