feat(canvas): Canvas V3 - Bug fixes and study workflow improvements
Bug Fixes: - Fix Atomizer Assistant error with reconnect button and error state handling - Enable connection/edge deletion with keyboard Delete/Backspace keys - Fix drag & drop positioning using screenToFlowPosition correctly - Fix loadFromConfig to create all node types and edges properly UI/UX Improvements: - Minimal responsive header with context breadcrumb - Better contrast with white text on dark backgrounds - Larger font sizes in NodePalette for readability - Study-aware header showing selected study name New Features: - Enhanced ExecuteDialog with Create/Update mode toggle - Select existing study to update or create new study - Home page Canvas Builder button for quick access - Home navigation button in CanvasView header Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ interface CanvasState {
|
||||
nodes: Node<CanvasNodeData>[];
|
||||
edges: Edge[];
|
||||
selectedNode: string | null;
|
||||
selectedEdge: string | null;
|
||||
validation: ValidationResult;
|
||||
|
||||
// Actions
|
||||
@@ -17,7 +18,9 @@ interface CanvasState {
|
||||
addNode: (type: NodeType, position: { x: number; y: number }) => void;
|
||||
updateNodeData: (nodeId: string, data: Partial<CanvasNodeData>) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
selectEdge: (edgeId: string | null) => void;
|
||||
deleteSelected: () => void;
|
||||
deleteEdge: (edgeId: string) => void;
|
||||
validate: () => ValidationResult;
|
||||
toIntent: () => OptimizationIntent;
|
||||
clear: () => void;
|
||||
@@ -94,6 +97,7 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
selectedNode: null,
|
||||
selectedEdge: null,
|
||||
validation: { valid: false, errors: [], warnings: [] },
|
||||
|
||||
onNodesChange: (changes) => {
|
||||
@@ -129,11 +133,26 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
},
|
||||
|
||||
selectNode: (nodeId) => {
|
||||
set({ selectedNode: nodeId });
|
||||
set({ selectedNode: nodeId, selectedEdge: null });
|
||||
},
|
||||
|
||||
selectEdge: (edgeId) => {
|
||||
set({ selectedEdge: edgeId, selectedNode: null });
|
||||
},
|
||||
|
||||
deleteSelected: () => {
|
||||
const { selectedNode, nodes, edges } = get();
|
||||
const { selectedNode, selectedEdge, nodes, edges } = get();
|
||||
|
||||
// Delete selected edge
|
||||
if (selectedEdge) {
|
||||
set({
|
||||
edges: edges.filter((e) => e.id !== selectedEdge),
|
||||
selectedEdge: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete selected node
|
||||
if (!selectedNode) return;
|
||||
|
||||
set({
|
||||
@@ -143,6 +162,13 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
deleteEdge: (edgeId) => {
|
||||
set({
|
||||
edges: get().edges.filter((e) => e.id !== edgeId),
|
||||
selectedEdge: null,
|
||||
});
|
||||
},
|
||||
|
||||
validate: () => {
|
||||
const { nodes, edges } = get();
|
||||
const result = validateGraph(nodes, edges);
|
||||
@@ -160,6 +186,7 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
selectedNode: null,
|
||||
selectedEdge: null,
|
||||
validation: { valid: false, errors: [], warnings: [] },
|
||||
});
|
||||
nodeIdCounter = 0;
|
||||
@@ -305,77 +332,177 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
nodes,
|
||||
edges,
|
||||
selectedNode: null,
|
||||
selectedEdge: null,
|
||||
validation: { valid: false, errors: [], warnings: [] },
|
||||
});
|
||||
},
|
||||
|
||||
loadFromConfig: (config) => {
|
||||
// Convert optimization_config.json format to intent format, then load
|
||||
const intent: OptimizationIntent = {
|
||||
version: '1.0',
|
||||
source: 'canvas',
|
||||
timestamp: new Date().toISOString(),
|
||||
model: {
|
||||
path: config.model?.path,
|
||||
type: config.model?.type,
|
||||
},
|
||||
solver: {
|
||||
type: config.solver?.solution ? `SOL${config.solver.solution}` : undefined,
|
||||
},
|
||||
design_variables: (config.design_variables || []).map(dv => ({
|
||||
name: dv.expression_name || dv.name,
|
||||
min: dv.lower,
|
||||
max: dv.upper,
|
||||
})),
|
||||
extractors: [], // Will be inferred from objectives
|
||||
objectives: (config.objectives || []).map(obj => ({
|
||||
name: obj.name,
|
||||
direction: (obj.direction as 'minimize' | 'maximize') || 'minimize',
|
||||
weight: obj.weight || 1,
|
||||
extractor: obj.extractor || '',
|
||||
})),
|
||||
constraints: (config.constraints || []).map(con => ({
|
||||
name: con.name,
|
||||
operator: con.type === 'upper' ? '<=' : '>=',
|
||||
value: con.value || 0,
|
||||
extractor: con.extractor || '',
|
||||
})),
|
||||
optimization: {
|
||||
method: config.method,
|
||||
max_trials: config.max_trials,
|
||||
},
|
||||
surrogate: config.surrogate ? {
|
||||
enabled: true,
|
||||
type: config.surrogate.type,
|
||||
min_trials: config.surrogate.min_trials,
|
||||
} : undefined,
|
||||
// Complete rewrite: Create all nodes and edges directly from config
|
||||
nodeIdCounter = 0;
|
||||
const nodes: Node<CanvasNodeData>[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
// Column positions for proper layout
|
||||
const COLS = {
|
||||
modelDvar: 50,
|
||||
solver: 280,
|
||||
extractor: 510,
|
||||
objCon: 740,
|
||||
algo: 970,
|
||||
surrogate: 1200,
|
||||
};
|
||||
const ROW_HEIGHT = 100;
|
||||
const START_Y = 50;
|
||||
|
||||
// Helper to create node
|
||||
const createNode = (type: NodeType, x: number, y: number, data: Partial<CanvasNodeData>): string => {
|
||||
const id = getNodeId();
|
||||
nodes.push({
|
||||
id,
|
||||
type,
|
||||
position: { x, y },
|
||||
data: { ...getDefaultData(type), ...data, configured: true } as CanvasNodeData,
|
||||
});
|
||||
return id;
|
||||
};
|
||||
|
||||
// 1. Model node
|
||||
const modelId = createNode('model', COLS.modelDvar, START_Y, {
|
||||
label: config.study_name || 'Model',
|
||||
filePath: config.model?.path,
|
||||
fileType: config.model?.type as 'prt' | 'fem' | 'sim' | undefined,
|
||||
});
|
||||
|
||||
// 2. Solver node
|
||||
const solverType = config.solver?.solution ? `SOL${config.solver.solution}` : undefined;
|
||||
const solverId = createNode('solver', COLS.solver, START_Y, {
|
||||
label: 'Solver',
|
||||
solverType: solverType as any,
|
||||
});
|
||||
edges.push({ id: `e_model_solver`, source: modelId, target: solverId });
|
||||
|
||||
// 3. Design variables (column 0, below model)
|
||||
let dvRow = 1;
|
||||
for (const dv of config.design_variables || []) {
|
||||
const dvId = createNode('designVar', COLS.modelDvar, START_Y + dvRow * ROW_HEIGHT, {
|
||||
label: dv.expression_name || dv.name,
|
||||
expressionName: dv.expression_name || dv.name,
|
||||
minValue: dv.lower,
|
||||
maxValue: dv.upper,
|
||||
});
|
||||
edges.push({ id: `e_dv_${dvRow}_model`, source: dvId, target: modelId });
|
||||
dvRow++;
|
||||
}
|
||||
|
||||
// 4. Extractors - infer from objectives and constraints
|
||||
const extractorNames: Record<string, string> = {
|
||||
'E1': 'Displacement', 'E2': 'Frequency', 'E3': 'Solid Stress',
|
||||
'E4': 'BDF Mass', 'E5': 'CAD Mass', 'E8': 'Zernike (OP2)',
|
||||
'E9': 'Zernike (CSV)', 'E10': 'Zernike (RMS)',
|
||||
};
|
||||
|
||||
// Infer extractors from objectives and constraints
|
||||
const extractorIds = new Set<string>();
|
||||
for (const obj of intent.objectives) {
|
||||
for (const obj of config.objectives || []) {
|
||||
if (obj.extractor) extractorIds.add(obj.extractor);
|
||||
}
|
||||
for (const con of intent.constraints) {
|
||||
for (const con of config.constraints || []) {
|
||||
if (con.extractor) extractorIds.add(con.extractor);
|
||||
}
|
||||
|
||||
const extractorNames: Record<string, string> = {
|
||||
'E1': 'Displacement',
|
||||
'E2': 'Frequency',
|
||||
'E3': 'Solid Stress',
|
||||
'E4': 'BDF Mass',
|
||||
'E5': 'CAD Mass',
|
||||
'E8': 'Zernike (OP2)',
|
||||
'E9': 'Zernike (CSV)',
|
||||
'E10': 'Zernike (RMS)',
|
||||
};
|
||||
// If no extractors found, add a default based on objectives
|
||||
if (extractorIds.size === 0 && (config.objectives?.length || 0) > 0) {
|
||||
extractorIds.add('E5'); // Default to CAD Mass
|
||||
}
|
||||
|
||||
intent.extractors = Array.from(extractorIds).map(id => ({
|
||||
id,
|
||||
name: extractorNames[id] || id,
|
||||
}));
|
||||
let extRow = 0;
|
||||
const extractorMap: Record<string, string> = {};
|
||||
for (const extId of extractorIds) {
|
||||
const nodeId = createNode('extractor', COLS.extractor, START_Y + extRow * ROW_HEIGHT, {
|
||||
label: extractorNames[extId] || extId,
|
||||
extractorId: extId,
|
||||
extractorName: extractorNames[extId] || extId,
|
||||
});
|
||||
extractorMap[extId] = nodeId;
|
||||
edges.push({ id: `e_solver_ext_${extRow}`, source: solverId, target: nodeId });
|
||||
extRow++;
|
||||
}
|
||||
|
||||
get().loadFromIntent(intent);
|
||||
// 5. Objectives
|
||||
let objRow = 0;
|
||||
const objIds: string[] = [];
|
||||
for (const obj of config.objectives || []) {
|
||||
const objId = createNode('objective', COLS.objCon, START_Y + objRow * ROW_HEIGHT, {
|
||||
label: obj.name,
|
||||
name: obj.name,
|
||||
direction: (obj.direction as 'minimize' | 'maximize') || 'minimize',
|
||||
weight: obj.weight || 1,
|
||||
});
|
||||
objIds.push(objId);
|
||||
|
||||
// Connect to extractor
|
||||
const extNodeId = obj.extractor ? extractorMap[obj.extractor] : Object.values(extractorMap)[0];
|
||||
if (extNodeId) {
|
||||
edges.push({ id: `e_ext_obj_${objRow}`, source: extNodeId, target: objId });
|
||||
}
|
||||
objRow++;
|
||||
}
|
||||
|
||||
// 6. Constraints
|
||||
let conRow = objRow;
|
||||
const conIds: string[] = [];
|
||||
for (const con of config.constraints || []) {
|
||||
const conId = createNode('constraint', COLS.objCon, START_Y + conRow * ROW_HEIGHT, {
|
||||
label: con.name,
|
||||
name: con.name,
|
||||
operator: (con.type === 'upper' ? '<=' : '>=') as any,
|
||||
value: con.value || 0,
|
||||
});
|
||||
conIds.push(conId);
|
||||
|
||||
// Connect to extractor
|
||||
const extNodeId = con.extractor ? extractorMap[con.extractor] : Object.values(extractorMap)[0];
|
||||
if (extNodeId) {
|
||||
edges.push({ id: `e_ext_con_${conRow}`, source: extNodeId, target: conId });
|
||||
}
|
||||
conRow++;
|
||||
}
|
||||
|
||||
// 7. Algorithm node
|
||||
const method = config.method || (config as any).optimization?.sampler || 'TPE';
|
||||
const maxTrials = config.max_trials || (config as any).optimization?.n_trials || 100;
|
||||
const algoId = createNode('algorithm', COLS.algo, START_Y, {
|
||||
label: 'Algorithm',
|
||||
method: method as any,
|
||||
maxTrials: maxTrials,
|
||||
});
|
||||
|
||||
// Connect objectives to algorithm
|
||||
for (let i = 0; i < objIds.length; i++) {
|
||||
edges.push({ id: `e_obj_${i}_algo`, source: objIds[i], target: algoId });
|
||||
}
|
||||
// Connect constraints to algorithm
|
||||
for (let i = 0; i < conIds.length; i++) {
|
||||
edges.push({ id: `e_con_${i}_algo`, source: conIds[i], target: algoId });
|
||||
}
|
||||
|
||||
// 8. Surrogate node (if enabled)
|
||||
if (config.surrogate) {
|
||||
const surId = createNode('surrogate', COLS.surrogate, START_Y, {
|
||||
label: 'Surrogate',
|
||||
enabled: true,
|
||||
modelType: config.surrogate.type as any,
|
||||
minTrials: config.surrogate.min_trials,
|
||||
});
|
||||
edges.push({ id: `e_algo_sur`, source: algoId, target: surId });
|
||||
}
|
||||
|
||||
set({
|
||||
nodes,
|
||||
edges,
|
||||
selectedNode: null,
|
||||
selectedEdge: null,
|
||||
validation: { valid: false, errors: [], warnings: [] },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user