feat(canvas): Add Phase 3+4 - Bidirectional sync, templates, and visual polish
Phase 3 - Bidirectional Sync: - Add loadFromIntent and loadFromConfig to canvas store - Create useIntentParser hook for parsing Claude messages - Create ConfigImporter component (file upload, paste JSON, load study) - Add import/clear buttons to CanvasView header Phase 4 - Templates & Polish: - Create template library with 5 presets: - Mass Minimization (single-objective) - Multi-Objective Pareto (NSGA-II) - Turbo Mode (with MLP surrogate) - Mirror Zernike (optical optimization) - Frequency Optimization (modal) - Create TemplateSelector component with category filters - Enhanced BaseNode with animations, glow effects, status indicators - Add colorBg to all node types for visual distinction - Add notification toast system - Update all exports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,45 @@ interface CanvasState {
|
||||
toIntent: () => OptimizationIntent;
|
||||
clear: () => void;
|
||||
loadFromIntent: (intent: OptimizationIntent) => void;
|
||||
loadFromConfig: (config: OptimizationConfig) => void;
|
||||
}
|
||||
|
||||
// Optimization config structure (from optimization_config.json)
|
||||
export interface OptimizationConfig {
|
||||
study_name?: string;
|
||||
model?: {
|
||||
path?: string;
|
||||
type?: string;
|
||||
};
|
||||
solver?: {
|
||||
type?: string;
|
||||
solution?: number;
|
||||
};
|
||||
design_variables?: Array<{
|
||||
name: string;
|
||||
expression_name?: string;
|
||||
lower: number;
|
||||
upper: number;
|
||||
type?: string;
|
||||
}>;
|
||||
objectives?: Array<{
|
||||
name: string;
|
||||
direction?: string;
|
||||
weight?: number;
|
||||
extractor?: string;
|
||||
}>;
|
||||
constraints?: Array<{
|
||||
name: string;
|
||||
type?: string;
|
||||
value?: number;
|
||||
extractor?: string;
|
||||
}>;
|
||||
method?: string;
|
||||
max_trials?: number;
|
||||
surrogate?: {
|
||||
type?: string;
|
||||
min_trials?: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
let nodeIdCounter = 0;
|
||||
@@ -43,6 +82,14 @@ const getDefaultData = (type: NodeType): CanvasNodeData => {
|
||||
}
|
||||
};
|
||||
|
||||
// Layout constants for auto-arrangement
|
||||
const LAYOUT = {
|
||||
startX: 100,
|
||||
startY: 100,
|
||||
colWidth: 250,
|
||||
rowHeight: 120,
|
||||
};
|
||||
|
||||
export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
@@ -119,7 +166,216 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
},
|
||||
|
||||
loadFromIntent: (intent) => {
|
||||
// TODO: Implement reverse serialization
|
||||
console.log('Loading intent:', intent);
|
||||
// Clear existing
|
||||
nodeIdCounter = 0;
|
||||
const nodes: Node<CanvasNodeData>[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
let col = 0;
|
||||
let row = 0;
|
||||
|
||||
// Helper to create positioned node
|
||||
const createNode = (type: NodeType, data: Partial<CanvasNodeData>, colOffset = 0): string => {
|
||||
const id = getNodeId();
|
||||
nodes.push({
|
||||
id,
|
||||
type,
|
||||
position: {
|
||||
x: LAYOUT.startX + (col + colOffset) * LAYOUT.colWidth,
|
||||
y: LAYOUT.startY + row * LAYOUT.rowHeight,
|
||||
},
|
||||
data: { ...getDefaultData(type), ...data, configured: true } as CanvasNodeData,
|
||||
});
|
||||
return id;
|
||||
};
|
||||
|
||||
// Model node (column 0)
|
||||
col = 0;
|
||||
const modelId = createNode('model', {
|
||||
label: 'Model',
|
||||
filePath: intent.model?.path,
|
||||
fileType: intent.model?.type as 'prt' | 'fem' | 'sim' | undefined,
|
||||
});
|
||||
|
||||
// Solver node (column 1)
|
||||
col = 1;
|
||||
const solverId = createNode('solver', {
|
||||
label: 'Solver',
|
||||
solverType: intent.solver?.type as any,
|
||||
});
|
||||
edges.push({ id: `e_${modelId}_${solverId}`, source: modelId, target: solverId });
|
||||
|
||||
// Design variables (column 0, multiple rows)
|
||||
col = 0;
|
||||
row = 1;
|
||||
const dvIds: string[] = [];
|
||||
for (const dv of intent.design_variables || []) {
|
||||
const dvId = createNode('designVar', {
|
||||
label: dv.name,
|
||||
expressionName: dv.name,
|
||||
minValue: dv.min,
|
||||
maxValue: dv.max,
|
||||
unit: dv.unit,
|
||||
});
|
||||
dvIds.push(dvId);
|
||||
edges.push({ id: `e_${dvId}_${modelId}`, source: dvId, target: modelId });
|
||||
row++;
|
||||
}
|
||||
|
||||
// Extractors (column 2)
|
||||
col = 2;
|
||||
row = 0;
|
||||
const extractorMap: Record<string, string> = {};
|
||||
for (const ext of intent.extractors || []) {
|
||||
const extId = createNode('extractor', {
|
||||
label: ext.name,
|
||||
extractorId: ext.id,
|
||||
extractorName: ext.name,
|
||||
config: ext.config,
|
||||
});
|
||||
extractorMap[ext.id] = extId;
|
||||
edges.push({ id: `e_${solverId}_${extId}`, source: solverId, target: extId });
|
||||
row++;
|
||||
}
|
||||
|
||||
// Objectives (column 3)
|
||||
col = 3;
|
||||
row = 0;
|
||||
const objIds: string[] = [];
|
||||
for (const obj of intent.objectives || []) {
|
||||
const objId = createNode('objective', {
|
||||
label: obj.name,
|
||||
name: obj.name,
|
||||
direction: obj.direction,
|
||||
weight: obj.weight,
|
||||
});
|
||||
objIds.push(objId);
|
||||
// Connect to extractor if specified
|
||||
if (obj.extractor && extractorMap[obj.extractor]) {
|
||||
edges.push({ id: `e_${extractorMap[obj.extractor]}_${objId}`, source: extractorMap[obj.extractor], target: objId });
|
||||
}
|
||||
row++;
|
||||
}
|
||||
|
||||
// Constraints (column 3, after objectives)
|
||||
const conIds: string[] = [];
|
||||
for (const con of intent.constraints || []) {
|
||||
const conId = createNode('constraint', {
|
||||
label: con.name,
|
||||
name: con.name,
|
||||
operator: con.operator as any,
|
||||
value: con.value,
|
||||
});
|
||||
conIds.push(conId);
|
||||
if (con.extractor && extractorMap[con.extractor]) {
|
||||
edges.push({ id: `e_${extractorMap[con.extractor]}_${conId}`, source: extractorMap[con.extractor], target: conId });
|
||||
}
|
||||
row++;
|
||||
}
|
||||
|
||||
// Algorithm (column 4)
|
||||
col = 4;
|
||||
row = 0;
|
||||
const algoId = createNode('algorithm', {
|
||||
label: 'Algorithm',
|
||||
method: intent.optimization?.method as any,
|
||||
maxTrials: intent.optimization?.max_trials,
|
||||
});
|
||||
// Connect all objectives and constraints to algorithm
|
||||
for (const objId of objIds) {
|
||||
edges.push({ id: `e_${objId}_${algoId}`, source: objId, target: algoId });
|
||||
}
|
||||
for (const conId of conIds) {
|
||||
edges.push({ id: `e_${conId}_${algoId}`, source: conId, target: algoId });
|
||||
}
|
||||
|
||||
// Surrogate (column 5, optional)
|
||||
if (intent.surrogate?.enabled) {
|
||||
col = 5;
|
||||
const surId = createNode('surrogate', {
|
||||
label: 'Surrogate',
|
||||
enabled: true,
|
||||
modelType: intent.surrogate.type as any,
|
||||
minTrials: intent.surrogate.min_trials,
|
||||
});
|
||||
edges.push({ id: `e_${algoId}_${surId}`, source: algoId, target: surId });
|
||||
}
|
||||
|
||||
set({
|
||||
nodes,
|
||||
edges,
|
||||
selectedNode: 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,
|
||||
};
|
||||
|
||||
// Infer extractors from objectives and constraints
|
||||
const extractorIds = new Set<string>();
|
||||
for (const obj of intent.objectives) {
|
||||
if (obj.extractor) extractorIds.add(obj.extractor);
|
||||
}
|
||||
for (const con of intent.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)',
|
||||
};
|
||||
|
||||
intent.extractors = Array.from(extractorIds).map(id => ({
|
||||
id,
|
||||
name: extractorNames[id] || id,
|
||||
}));
|
||||
|
||||
get().loadFromIntent(intent);
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user