diff --git a/frontend/app.js b/frontend/app.js index 94ee5d6..5ad4b67 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -364,11 +364,12 @@ function defaultProject() { }; } -function compileOptions() { +function compileOptions(extra = {}) { return { render_mode: state.renderMode, show_labels: state.showLabels, - generic_symbols: true + generic_symbols: true, + ...extra }; } @@ -1807,12 +1808,21 @@ async function compileModel(model, opts = {}) { const source = opts.source ?? "manual"; const fit = opts.fit ?? false; const keepView = opts.keepView ?? false; + const preservePlacement = opts.preservePlacement ?? false; setStatus(source === "drag" ? "Compiling after drag..." : "Compiling..."); try { const result = await apiPost("/compile", { payload: model, - options: compileOptions() + options: compileOptions( + preservePlacement + ? { + preserve_placement: true, + auto_rotate: false, + respect_locks: true + } + : {} + ) }); state.model = applyCompileLayoutToModel(model, result); @@ -3316,7 +3326,7 @@ function setupEvents() { inst.placement.locked = true; } } - await compileModel(state.model, { source: "drag", keepView: true }); + await compileModel(state.model, { source: "drag", keepView: true, preservePlacement: true }); } }); diff --git a/src/layout.js b/src/layout.js index 8f4e29d..8d2f181 100644 --- a/src/layout.js +++ b/src/layout.js @@ -118,6 +118,41 @@ function rotateSide(side, rotation) { return order[(idx + steps) % 4]; } +function hasValidPlacement(inst) { + return Number.isFinite(inst?.placement?.x) && Number.isFinite(inst?.placement?.y); +} + +function preservePlacedInstances(model, options = {}) { + const respectLocks = options.respectLocks ?? true; + const autoRotate = options.autoRotate ?? false; + const fallback = placeInstances(model, { respectLocks, autoRotate }); + const fallbackByRef = new Map((fallback?.placed ?? []).map((inst) => [inst.ref, inst])); + const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref)); + + return instances.map((inst) => { + if (hasValidPlacement(inst)) { + return { + ...inst, + placement: { + x: toGrid(Number(inst.placement.x)), + y: toGrid(Number(inst.placement.y)), + rotation: normalizeRotation(inst.placement.rotation ?? 0), + locked: respectLocks ? Boolean(inst.placement.locked) : false + } + }; + } + return fallbackByRef.get(inst.ref) ?? { + ...inst, + placement: { + x: toGrid(MARGIN_X), + y: toGrid(MARGIN_Y), + rotation: normalizeRotation(inst.placement?.rotation ?? 0), + locked: respectLocks ? Boolean(inst.placement?.locked) : false + } + }; + }); +} + function getNodePin(model, placedMap, node) { const inst = placedMap.get(node.ref); if (!inst) { @@ -2358,8 +2393,11 @@ export function layoutAndRoute(model, options = {}) { const renderMode = options.render_mode === "explicit" ? "explicit" : DEFAULT_RENDER_MODE; const respectLocks = options.respect_locks ?? true; const autoRotate = options.auto_rotate ?? true; - - const { placed, placedMap } = placeInstances(model, { respectLocks, autoRotate }); + const preservePlacement = options.preserve_placement === true; + const placed = preservePlacement + ? preservePlacedInstances(model, { respectLocks, autoRotate: false }) + : placeInstances(model, { respectLocks, autoRotate }).placed; + const placedMap = new Map(placed.map((inst) => [inst.ref, inst])); const bounds = buildBounds(model, placed); const { routed, busGroups } = routeAllNets(model, placed, placedMap, bounds, { renderMode