feat(canvas): project edges from objective/constraint source
This commit is contained in:
@@ -422,6 +422,89 @@ function SpecRendererInner({
|
|||||||
}
|
}
|
||||||
}, [studyId, loadSpec, onStudyChange]);
|
}, [studyId, loadSpec, onStudyChange]);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Option A: Edge projection sync (source fields are truth)
|
||||||
|
// Keep canvas edges in sync when user edits objective/constraint source in panels.
|
||||||
|
// We only enforce Extractor -> Objective/Constraint wiring edges here.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const isEdgeSyncingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!spec || !studyId) return;
|
||||||
|
if (isEdgeSyncingRef.current) return;
|
||||||
|
|
||||||
|
const current = spec.canvas?.edges || [];
|
||||||
|
|
||||||
|
// Compute desired extractor->objective/constraint edges from source fields
|
||||||
|
const desiredPairs = new Set<string>();
|
||||||
|
|
||||||
|
for (const obj of spec.objectives || []) {
|
||||||
|
const extractorId = obj.source?.extractor_id;
|
||||||
|
const outputName = obj.source?.output_name;
|
||||||
|
if (extractorId && outputName && extractorId !== '__UNSET__' && outputName !== '__UNSET__') {
|
||||||
|
desiredPairs.add(`${extractorId}__${obj.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const con of spec.constraints || []) {
|
||||||
|
const extractorId = con.source?.extractor_id;
|
||||||
|
const outputName = con.source?.output_name;
|
||||||
|
if (extractorId && outputName && extractorId !== '__UNSET__' && outputName !== '__UNSET__') {
|
||||||
|
desiredPairs.add(`${extractorId}__${con.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify current wiring edges (ext_* -> obj_*/con_*)
|
||||||
|
const currentWiringPairs = new Set<string>();
|
||||||
|
for (const e of current) {
|
||||||
|
if (e.source?.startsWith('ext_') && (e.target?.startsWith('obj_') || e.target?.startsWith('con_'))) {
|
||||||
|
currentWiringPairs.add(`${e.source}__${e.target}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine adds/removes
|
||||||
|
const toAdd: Array<{ source: string; target: string }> = [];
|
||||||
|
for (const key of desiredPairs) {
|
||||||
|
if (!currentWiringPairs.has(key)) {
|
||||||
|
const [source, target] = key.split('__');
|
||||||
|
toAdd.push({ source, target });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toRemove: Array<{ source: string; target: string }> = [];
|
||||||
|
for (const key of currentWiringPairs) {
|
||||||
|
if (!desiredPairs.has(key)) {
|
||||||
|
const [source, target] = key.split('__');
|
||||||
|
toRemove.push({ source, target });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAdd.length === 0 && toRemove.length === 0) return;
|
||||||
|
|
||||||
|
isEdgeSyncingRef.current = true;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Remove stale edges first
|
||||||
|
for (const e of toRemove) {
|
||||||
|
await removeEdge(e.source, e.target);
|
||||||
|
}
|
||||||
|
// Add missing edges
|
||||||
|
for (const e of toAdd) {
|
||||||
|
await addEdge(e.source, e.target);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[SpecRenderer] Edge projection sync failed:', err);
|
||||||
|
} finally {
|
||||||
|
// Small delay avoids re-entrancy storms when backend broadcasts updates
|
||||||
|
setTimeout(() => {
|
||||||
|
isEdgeSyncingRef.current = false;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [spec, studyId, addEdge, removeEdge]);
|
||||||
|
|
||||||
// Convert spec to ReactFlow nodes
|
// Convert spec to ReactFlow nodes
|
||||||
const nodes = useMemo(() => {
|
const nodes = useMemo(() => {
|
||||||
const baseNodes = specToNodes(spec);
|
const baseNodes = specToNodes(spec);
|
||||||
|
|||||||
Reference in New Issue
Block a user