Activate ELK backend placement path with deterministic fallback
Some checks are pending
CI / test (push) Waiting to run

This commit is contained in:
Rbanh 2026-02-20 01:31:39 -05:00
parent 128c5c6f4e
commit 46175efe1b
3 changed files with 253 additions and 19 deletions

View File

@ -2578,6 +2578,228 @@ function layoutAndRouteNative(model, options = {}) {
};
}
function buildElkEdges(model) {
const directed = buildDirectedEdges(model);
if (directed.length) {
return directed;
}
const undirected = new Map();
for (const net of model.nets ?? []) {
const refs = [...new Set((net.nodes ?? []).map((n) => n.ref))].sort();
if (refs.length < 2) {
continue;
}
const source = refs[0];
for (let i = 1; i < refs.length; i += 1) {
const target = refs[i];
if (source === target) continue;
undirected.set(`${source}->${target}`, [source, target]);
}
}
return [...undirected.values()];
}
function buildElkGraph(model) {
const instances = [...(model.instances ?? [])].sort((a, b) => a.ref.localeCompare(b.ref));
const edges = buildElkEdges(model);
return {
id: "schemeta-root",
layoutOptions: {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.spacing.nodeNode": "90",
"elk.layered.spacing.nodeNodeBetweenLayers": "170",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
children: instances.map((inst) => {
const sym = model.symbols[inst.symbol];
return {
id: inst.ref,
width: sym?.body?.width ?? 120,
height: sym?.body?.height ?? 80
};
}),
edges: edges.map(([source, target], idx) => ({
id: `e${idx}_${source}_${target}`,
sources: [source],
targets: [target]
}))
};
}
function resolveElkLayoutResult(elkRuntime, graph) {
const ElkCtor = elkRuntime.ElkCtor;
if (typeof ElkCtor !== "function") {
return {
ok: false,
reason: "invalid_runtime",
message: "ELK runtime did not provide a constructor."
};
}
let instance;
try {
instance = new ElkCtor();
} catch (err) {
return {
ok: false,
reason: "constructor_failed",
message: `ELK constructor failed: ${err instanceof Error ? err.message : String(err)}`
};
}
let laidOut;
if (typeof instance.layoutSync === "function") {
laidOut = instance.layoutSync(graph);
} else if (typeof instance.layout === "function") {
laidOut = instance.layout(graph);
if (laidOut && typeof laidOut.then === "function") {
return {
ok: false,
reason: "async_runtime",
message:
"ELK runtime returned an asynchronous layout promise; synchronous backend placement is required. Using default engine."
};
}
} else {
return {
ok: false,
reason: "invalid_runtime",
message: "ELK runtime did not expose layout/layoutSync."
};
}
if (!laidOut || !Array.isArray(laidOut.children)) {
return {
ok: false,
reason: "invalid_result",
message: "ELK runtime returned an invalid graph result."
};
}
return { ok: true, graph: laidOut };
}
function layoutAndRouteElk(model, options = {}, nativeLayout) {
const elkRuntime = resolveElkRuntime(options.elk_runtime_module);
if (!elkRuntime.ok) {
return {
ok: false,
warning: {
code: "elk_layout_unavailable_fallback",
message: elkRuntime.message
}
};
}
const graph = buildElkGraph(model);
const elkResult = resolveElkLayoutResult(elkRuntime, graph);
if (!elkResult.ok) {
return {
ok: false,
warning: {
code:
elkResult.reason === "async_runtime"
? "elk_layout_async_runtime_fallback"
: "elk_layout_invalid_result_fallback",
message: elkResult.message
}
};
}
const renderMode = options.render_mode === "explicit" ? "explicit" : DEFAULT_RENDER_MODE;
const respectLocks = options.respect_locks ?? true;
const autoRotate = options.auto_rotate ?? true;
const preservePlacement = options.preserve_placement !== false;
const rank = computeRanks(model).rank;
const laneProfiles = refLaneProfiles(model);
const minRank = Math.min(...[...rank.values(), 1]);
const nativeByRef = new Map((nativeLayout?.placed ?? []).map((inst) => [inst.ref, inst]));
const elkByRef = new Map(
elkResult.graph.children
.filter((child) => child && typeof child.id === "string")
.map((child) => [child.id, child])
);
const placed = [...model.instances]
.sort((a, b) => a.ref.localeCompare(b.ref))
.map((inst) => {
const sym = model.symbols[inst.symbol];
const locked = respectLocks ? Boolean(inst.placement?.locked) : false;
const keepExisting = preservePlacement && hasValidPlacement(inst);
const keepLocked = locked && hasValidPlacement(inst);
const native = nativeByRef.get(inst.ref);
const elkNode = elkByRef.get(inst.ref);
let x = hasValidPlacement(inst) ? Number(inst.placement.x) : Number.NaN;
let y = hasValidPlacement(inst) ? Number(inst.placement.y) : Number.NaN;
if (!keepExisting && !keepLocked) {
if (elkNode && Number.isFinite(elkNode.x) && Number.isFinite(elkNode.y)) {
const localRank = Math.max(0, (rank.get(inst.ref) ?? 1) - minRank);
const lane = laneProfiles.get(inst.ref)?.laneIndex ?? 2;
const rankTargetX = MARGIN_X + localRank * (COLUMN_GAP * 0.84);
const laneTargetY = MARGIN_Y + lane * (ROW_GAP * 0.66);
const rawX = toGrid(MARGIN_X + Number(elkNode.x));
const rawY = toGrid(MARGIN_Y + Number(elkNode.y));
x = toGrid(rawX * 0.7 + rankTargetX * 0.3);
y = toGrid(rawY * 0.62 + laneTargetY * 0.38);
} else if (native?.placement && Number.isFinite(native.placement.x) && Number.isFinite(native.placement.y)) {
x = native.placement.x;
y = native.placement.y;
}
}
if (!Number.isFinite(x) || !Number.isFinite(y)) {
if (native?.placement && Number.isFinite(native.placement.x) && Number.isFinite(native.placement.y)) {
x = native.placement.x;
y = native.placement.y;
} else {
x = toGrid(MARGIN_X);
y = toGrid(MARGIN_Y);
}
}
return {
...inst,
placement: {
x: toGrid(Math.max(MARGIN_X, x)),
y: toGrid(Math.max(MARGIN_Y, y)),
rotation: normalizeRotation(inst.placement?.rotation ?? 0),
locked
}
};
});
const placedMap = new Map(placed.map((inst) => [inst.ref, inst]));
applyAutoRotation(model, placedMap, { autoRotate });
applyAlignmentConstraints(placedMap, model.constraints);
applyNearConstraints(model, placedMap, model.constraints);
resolvePlacementOverlaps(model, placedMap, { respectLocks });
enforceFinalComponentSeparation(model, placedMap, { respectLocks });
const normalizedPlaced = [...model.instances]
.sort((a, b) => a.ref.localeCompare(b.ref))
.map((inst) => placedMap.get(inst.ref) ?? inst);
const bounds = buildBounds(model, normalizedPlaced);
const normalizedMap = new Map(normalizedPlaced.map((inst) => [inst.ref, inst]));
const { routed, busGroups } = routeAllNets(model, normalizedPlaced, normalizedMap, bounds, {
renderMode
});
return {
ok: true,
layout: {
placed: normalizedPlaced,
routed,
width: bounds.maxX,
height: bounds.maxY,
bus_groups: busGroups,
metrics: computeLayoutMetrics(routed, busGroups),
render_mode_used: renderMode
}
};
}
export function layoutAndRoute(model, options = {}) {
const requestedEngine = requestedLayoutEngine(options);
const nativeLayout = layoutAndRouteNative(model, options);
@ -2591,32 +2813,21 @@ export function layoutAndRoute(model, options = {}) {
};
}
const elkRuntime = resolveElkRuntime(options.elk_runtime_module);
if (!elkRuntime.ok) {
const elkLayout = layoutAndRouteElk(model, options, nativeLayout);
if (!elkLayout.ok) {
return {
...nativeLayout,
layout_engine_requested: "elk",
layout_engine_used: DEFAULT_LAYOUT_ENGINE,
layout_warnings: [
{
code: "elk_layout_unavailable_fallback",
message: elkRuntime.message
}
]
layout_warnings: [elkLayout.warning]
};
}
return {
...nativeLayout,
...elkLayout.layout,
layout_engine_requested: "elk",
layout_engine_used: DEFAULT_LAYOUT_ENGINE,
layout_warnings: [
{
code: "elk_layout_boundary_fallback",
message:
"ELK runtime resolved, but backend ELK placement is not yet enabled. Using default layout engine."
}
]
layout_engine_used: "elk",
layout_warnings: []
};
}

View File

@ -1,5 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { compile } from "../src/compile.js";
import fixture from "../examples/esp32-audio.json" with { type: "json" };
@ -51,12 +52,13 @@ test("compile default layout engine path remains stable", () => {
test("compile accepts ELK layout flag option", () => {
const result = compile(fixture, {
use_elk_layout: true,
elk_runtime_module: "__missing_elk_runtime_for_test__"
elk_runtime_module: path.resolve(process.cwd(), "tests/fixtures/mock-elk-runtime.cjs")
});
assert.equal(result.ok, true);
assert.equal(result.layout_engine_requested, "elk");
assert.equal(result.layout_engine_used, "schemeta-v2");
assert.equal(result.layout_engine_used, "elk");
assert.deepEqual(result.layout_warnings, []);
});
test("compile ELK fallback is deterministic when runtime is unavailable", () => {

21
tests/fixtures/mock-elk-runtime.cjs vendored Normal file
View File

@ -0,0 +1,21 @@
class MockElkRuntime {
layoutSync(graph) {
const children = Array.isArray(graph?.children) ? [...graph.children] : [];
const positioned = children.map((node, idx) => {
const col = idx % 4;
const row = Math.floor(idx / 4);
return {
...node,
x: 120 + col * 260,
y: 100 + row * 200
};
});
return {
id: graph?.id ?? "root",
children: positioned,
edges: Array.isArray(graph?.edges) ? graph.edges : []
};
}
}
module.exports = MockElkRuntime;