imrishabh18/pedometer
This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.
- Version
- 1.1.3
- License
- unset
- Stars
- 0
vendor/fanout-solver.js
// Generated from @tscircuit/fanout-solver@0.0.57 (MIT).
// Reproduce with bun run bundle:fanout. No source patches.
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) {
return this[key];
}
var __toESMCache_node;
var __toESMCache_esm;
var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
var cached = cache.get(mod);
if (cached)
return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: __accessProp.bind(mod, key),
enumerable: true
});
if (canCache)
cache.set(mod, to);
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
// node_modules/svgson/dist/svgson.umd.js
var require_svgson_umd = __commonJS((exports, module) => {
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.svgson = factory());
})(exports, function() {
/*!
* Determine if an object is a Buffer
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
var isBuffer_1 = function(obj) {
return obj != null && (isBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer);
};
function isBuffer(obj) {
return !!obj.constructor && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj);
}
function isSlowBuffer(obj) {
return typeof obj.readFloatLE === "function" && typeof obj.slice === "function" && isBuffer(obj.slice(0, 0));
}
var toString2 = Object.prototype.toString;
var kindOf = function kindOf2(val) {
if (typeof val === "undefined") {
return "undefined";
}
if (val === null) {
return "null";
}
if (val === true || val === false || val instanceof Boolean) {
return "boolean";
}
if (typeof val === "string" || val instanceof String) {
return "string";
}
if (typeof val === "number" || val instanceof Number) {
return "number";
}
if (typeof val === "function" || val instanceof Function) {
return "function";
}
if (typeof Array.isArray !== "undefined" && Array.isArray(val)) {
return "array";
}
if (val instanceof RegExp) {
return "regexp";
}
if (val instanceof Date) {
return "date";
}
var type = toString2.call(val);
if (type === "[object RegExp]") {
return "regexp";
}
if (type === "[object Date]") {
return "date";
}
if (type === "[object Arguments]") {
return "arguments";
}
if (type === "[object Error]") {
return "error";
}
if (isBuffer_1(val)) {
return "buffer";
}
if (type === "[object Set]") {
return "set";
}
if (type === "[object WeakSet]") {
return "weakset";
}
if (type === "[object Map]") {
return "map";
}
if (type === "[object WeakMap]") {
return "weakmap";
}
if (type === "[object Symbol]") {
return "symbol";
}
if (type === "[object Int8Array]") {
return "int8array";
}
if (type === "[object Uint8Array]") {
return "uint8array";
}
if (type === "[object Uint8ClampedArray]") {
return "uint8clampedarray";
}
if (type === "[object Int16Array]") {
return "int16array";
}
if (type === "[object Uint16Array]") {
return "uint16array";
}
if (type === "[object Int32Array]") {
return "int32array";
}
if (type === "[object Uint32Array]") {
return "uint32array";
}
if (type === "[object Float32Array]") {
return "float32array";
}
if (type === "[object Float64Array]") {
return "float64array";
}
return "object";
};
function createCommonjsModule(fn, module2) {
return module2 = { exports: {} }, fn(module2, module2.exports), module2.exports;
}
var renameKeys = createCommonjsModule(function(module2) {
(function() {
function rename(obj, fn) {
if (typeof fn !== "function") {
return obj;
}
var res = {};
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
res[fn(key, obj[key]) || key] = obj[key];
}
}
return res;
}
if (module2.exports) {
module2.exports = rename;
} else {
{
window.rename = rename;
}
}
})();
});
var deepRenameKeys = function renameDeep(obj, cb) {
var type = kindOf(obj);
if (type !== "object" && type !== "array") {
throw new Error("expected an object");
}
var res = [];
if (type === "object") {
obj = renameKeys(obj, cb);
res = {};
}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
var val = obj[key];
if (kindOf(val) === "object" || kindOf(val) === "array") {
res[key] = renameDeep(val, cb);
} else {
res[key] = val;
}
}
}
return res;
};
var eventemitter3 = createCommonjsModule(function(module2) {
var has = Object.prototype.hasOwnProperty, prefix = "~";
function Events() {}
if (Object.create) {
Events.prototype = Object.create(null);
if (!new Events().__proto__)
prefix = false;
}
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
function EventEmitter() {
this._events = new Events;
this._eventsCount = 0;
}
EventEmitter.prototype.eventNames = function eventNames() {
var names = [], events, name;
if (this._eventsCount === 0)
return names;
for (name in events = this._events) {
if (has.call(events, name))
names.push(prefix ? name.slice(1) : name);
}
if (Object.getOwnPropertySymbols) {
return names.concat(Object.getOwnPropertySymbols(events));
}
return names;
};
EventEmitter.prototype.listeners = function listeners(event, exists) {
var evt = prefix ? prefix + event : event, available = this._events[evt];
if (exists)
return !!available;
if (!available)
return [];
if (available.fn)
return [available.fn];
for (var i = 0, l = available.length, ee = new Array(l);i < l; i++) {
ee[i] = available[i].fn;
}
return ee;
};
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt])
return false;
var listeners = this._events[evt], len = arguments.length, args, i;
if (listeners.fn) {
if (listeners.once)
this.removeListener(event, listeners.fn, undefined, true);
switch (len) {
case 1:
return listeners.fn.call(listeners.context), true;
case 2:
return listeners.fn.call(listeners.context, a1), true;
case 3:
return listeners.fn.call(listeners.context, a1, a2), true;
case 4:
return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5:
return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6:
return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len - 1);i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length, j;
for (i = 0;i < length; i++) {
if (listeners[i].once)
this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1:
listeners[i].fn.call(listeners[i].context);
break;
case 2:
listeners[i].fn.call(listeners[i].context, a1);
break;
case 3:
listeners[i].fn.call(listeners[i].context, a1, a2);
break;
case 4:
listeners[i].fn.call(listeners[i].context, a1, a2, a3);
break;
default:
if (!args)
for (j = 1, args = new Array(len - 1);j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
EventEmitter.prototype.on = function on(event, fn, context) {
var listener = new EE(fn, context || this), evt = prefix ? prefix + event : event;
if (!this._events[evt])
this._events[evt] = listener, this._eventsCount++;
else if (!this._events[evt].fn)
this._events[evt].push(listener);
else
this._events[evt] = [this._events[evt], listener];
return this;
};
EventEmitter.prototype.once = function once(event, fn, context) {
var listener = new EE(fn, context || this, true), evt = prefix ? prefix + event : event;
if (!this._events[evt])
this._events[evt] = listener, this._eventsCount++;
else if (!this._events[evt].fn)
this._events[evt].push(listener);
else
this._events[evt] = [this._events[evt], listener];
return this;
};
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt])
return this;
if (!fn) {
if (--this._eventsCount === 0)
this._events = new Events;
else
delete this._events[evt];
return this;
}
var listeners = this._events[evt];
if (listeners.fn) {
if (listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context)) {
if (--this._eventsCount === 0)
this._events = new Events;
else
delete this._events[evt];
}
} else {
for (var i = 0, events = [], length = listeners.length;i < length; i++) {
if (listeners[i].fn !== fn || once && !listeners[i].once || context && listeners[i].context !== context) {
events.push(listeners[i]);
}
}
if (events.length)
this._events[evt] = events.length === 1 ? events[0] : events;
else if (--this._eventsCount === 0)
this._events = new Events;
else
delete this._events[evt];
}
return this;
};
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
var evt;
if (event) {
evt = prefix ? prefix + event : event;
if (this._events[evt]) {
if (--this._eventsCount === 0)
this._events = new Events;
else
delete this._events[evt];
}
} else {
this._events = new Events;
this._eventsCount = 0;
}
return this;
};
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
EventEmitter.prototype.setMaxListeners = function setMaxListeners() {
return this;
};
EventEmitter.prefixed = prefix;
EventEmitter.EventEmitter = EventEmitter;
{
module2.exports = EventEmitter;
}
});
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var noop = function noop2() {};
var State = {
data: "state-data",
cdata: "state-cdata",
tagBegin: "state-tag-begin",
tagName: "state-tag-name",
tagEnd: "state-tag-end",
attributeNameStart: "state-attribute-name-start",
attributeName: "state-attribute-name",
attributeNameEnd: "state-attribute-name-end",
attributeValueBegin: "state-attribute-value-begin",
attributeValue: "state-attribute-value"
};
var Action = {
lt: "action-lt",
gt: "action-gt",
space: "action-space",
equal: "action-equal",
quote: "action-quote",
slash: "action-slash",
char: "action-char",
error: "action-error"
};
var Type$1 = {
text: "text",
openTag: "open-tag",
closeTag: "close-tag",
attributeName: "attribute-name",
attributeValue: "attribute-value"
};
var charToAction = {
" ": Action.space,
"\t": Action.space,
"\n": Action.space,
"\r": Action.space,
"<": Action.lt,
">": Action.gt,
'"': Action.quote,
"'": Action.quote,
"=": Action.equal,
"/": Action.slash
};
var getAction = function getAction2(char) {
return charToAction[char] || Action.char;
};
var create$1 = function create2(options) {
var _State$data, _State$tagBegin, _State$tagName, _State$tagEnd, _State$attributeNameS, _State$attributeName, _State$attributeNameE, _State$attributeValue, _State$attributeValue2, _lexer$stateMachine;
options = Object.assign({ debug: false }, options);
var lexer2 = new eventemitter3;
var state = State.data;
var data = "";
var tagName = "";
var attrName = "";
var attrValue = "";
var isClosing = "";
var openingQuote = "";
var emit = function emit2(type, value) {
if (tagName[0] === "?" || tagName[0] === "!") {
return;
}
var event = { type, value };
if (options.debug) {
console.log("emit:", event);
}
lexer2.emit("data", event);
};
lexer2.stateMachine = (_lexer$stateMachine = {}, _defineProperty(_lexer$stateMachine, State.data, (_State$data = {}, _defineProperty(_State$data, Action.lt, function() {
if (data.trim()) {
emit(Type$1.text, data);
}
tagName = "";
isClosing = false;
state = State.tagBegin;
}), _defineProperty(_State$data, Action.char, function(char) {
data += char;
}), _State$data)), _defineProperty(_lexer$stateMachine, State.cdata, _defineProperty({}, Action.char, function(char) {
data += char;
if (data.substr(-3) === "]]>") {
emit(Type$1.text, data.slice(0, -3));
data = "";
state = State.data;
}
})), _defineProperty(_lexer$stateMachine, State.tagBegin, (_State$tagBegin = {}, _defineProperty(_State$tagBegin, Action.space, noop), _defineProperty(_State$tagBegin, Action.char, function(char) {
tagName = char;
state = State.tagName;
}), _defineProperty(_State$tagBegin, Action.slash, function() {
tagName = "";
isClosing = true;
}), _State$tagBegin)), _defineProperty(_lexer$stateMachine, State.tagName, (_State$tagName = {}, _defineProperty(_State$tagName, Action.space, function() {
if (isClosing) {
state = State.tagEnd;
} else {
state = State.attributeNameStart;
emit(Type$1.openTag, tagName);
}
}), _defineProperty(_State$tagName, Action.gt, function() {
if (isClosing) {
emit(Type$1.closeTag, tagName);
} else {
emit(Type$1.openTag, tagName);
}
data = "";
state = State.data;
}), _defineProperty(_State$tagName, Action.slash, function() {
state = State.tagEnd;
emit(Type$1.openTag, tagName);
}), _defineProperty(_State$tagName, Action.char, function(char) {
tagName += char;
if (tagName === "![CDATA[") {
state = State.cdata;
data = "";
tagName = "";
}
}), _State$tagName)), _defineProperty(_lexer$stateMachine, State.tagEnd, (_State$tagEnd = {}, _defineProperty(_State$tagEnd, Action.gt, function() {
emit(Type$1.closeTag, tagName);
data = "";
state = State.data;
}), _defineProperty(_State$tagEnd, Action.char, noop), _State$tagEnd)), _defineProperty(_lexer$stateMachine, State.attributeNameStart, (_State$attributeNameS = {}, _defineProperty(_State$attributeNameS, Action.char, function(char) {
attrName = char;
state = State.attributeName;
}), _defineProperty(_State$attributeNameS, Action.gt, function() {
data = "";
state = State.data;
}), _defineProperty(_State$attributeNameS, Action.space, noop), _defineProperty(_State$attributeNameS, Action.slash, function() {
isClosing = true;
state = State.tagEnd;
}), _State$attributeNameS)), _defineProperty(_lexer$stateMachine, State.attributeName, (_State$attributeName = {}, _defineProperty(_State$attributeName, Action.space, function() {
state = State.attributeNameEnd;
}), _defineProperty(_State$attributeName, Action.equal, function() {
emit(Type$1.attributeName, attrName);
state = State.attributeValueBegin;
}), _defineProperty(_State$attributeName, Action.gt, function() {
attrValue = "";
emit(Type$1.attributeName, attrName);
emit(Type$1.attributeValue, attrValue);
data = "";
state = State.data;
}), _defineProperty(_State$attributeName, Action.slash, function() {
isClosing = true;
attrValue = "";
emit(Type$1.attributeName, attrName);
emit(Type$1.attributeValue, attrValue);
state = State.tagEnd;
}), _defineProperty(_State$attributeName, Action.char, function(char) {
attrName += char;
}), _State$attributeName)), _defineProperty(_lexer$stateMachine, State.attributeNameEnd, (_State$attributeNameE = {}, _defineProperty(_State$attributeNameE, Action.space, noop), _defineProperty(_State$attributeNameE, Action.equal, function() {
emit(Type$1.attributeName, attrName);
state = State.attributeValueBegin;
}), _defineProperty(_State$attributeNameE, Action.gt, function() {
attrValue = "";
emit(Type$1.attributeName, attrName);
emit(Type$1.attributeValue, attrValue);
data = "";
state = State.data;
}), _defineProperty(_State$attributeNameE, Action.char, function(char) {
attrValue = "";
emit(Type$1.attributeName, attrName);
emit(Type$1.attributeValue, attrValue);
attrName = char;
state = State.attributeName;
}), _State$attributeNameE)), _defineProperty(_lexer$stateMachine, State.attributeValueBegin, (_State$attributeValue = {}, _defineProperty(_State$attributeValue, Action.space, noop), _defineProperty(_State$attributeValue, Action.quote, function(char) {
openingQuote = char;
attrValue = "";
state = State.attributeValue;
}), _defineProperty(_State$attributeValue, Action.gt, function() {
attrValue = "";
emit(Type$1.attributeValue, attrValue);
data = "";
state = State.data;
}), _defineProperty(_State$attributeValue, Action.char, function(char) {
openingQuote = "";
attrValue = char;
state = State.attributeValue;
}), _State$attributeValue)), _defineProperty(_lexer$stateMachine, State.attributeValue, (_State$attributeValue2 = {}, _defineProperty(_State$attributeValue2, Action.space, function(char) {
if (openingQuote) {
attrValue += char;
} else {
emit(Type$1.attributeValue, attrValue);
state = State.attributeNameStart;
}
}), _defineProperty(_State$attributeValue2, Action.quote, function(char) {
if (openingQuote === char) {
emit(Type$1.attributeValue, attrValue);
state = State.attributeNameStart;
} else {
attrValue += char;
}
}), _defineProperty(_State$attributeValue2, Action.gt, function(char) {
if (openingQuote) {
attrValue += char;
} else {
emit(Type$1.attributeValue, attrValue);
data = "";
state = State.data;
}
}), _defineProperty(_State$attributeValue2, Action.slash, function(char) {
if (openingQuote) {
attrValue += char;
} else {
emit(Type$1.attributeValue, attrValue);
isClosing = true;
state = State.tagEnd;
}
}), _defineProperty(_State$attributeValue2, Action.char, function(char) {
attrValue += char;
}), _State$attributeValue2)), _lexer$stateMachine);
var step = function step2(char) {
if (options.debug) {
console.log(state, char);
}
var actions = lexer2.stateMachine[state];
var action = actions[getAction(char)] || actions[Action.error] || actions[Action.char];
action(char);
};
lexer2.write = function(str) {
var len = str.length;
for (var i = 0;i < len; i++) {
step(str[i]);
}
};
return lexer2;
};
var lexer = {
State,
Action,
Type: Type$1,
create: create$1
};
var Type = lexer.Type;
var NodeType = {
element: "element",
text: "text"
};
var createNode = function createNode2(params) {
return Object.assign({
name: "",
type: NodeType.element,
value: "",
parent: null,
attributes: {},
children: []
}, params);
};
var create = function create2(options) {
options = Object.assign({
stream: false,
parentNodes: true,
doneEvent: "done",
tagPrefix: "tag:",
emitTopLevelOnly: false,
debug: false
}, options);
var lexer$1 = undefined, rootNode = undefined, current = undefined, attrName = undefined;
var reader2 = new eventemitter3;
var handleLexerData = function handleLexerData2(data) {
switch (data.type) {
case Type.openTag:
if (current === null) {
current = rootNode;
current.name = data.value;
} else {
var node = createNode({
name: data.value,
parent: current
});
current.children.push(node);
current = node;
}
break;
case Type.closeTag:
var parent = current.parent;
if (!options.parentNodes) {
current.parent = null;
}
if (current.name !== data.value) {
break;
}
if (options.stream && parent === rootNode) {
rootNode.children = [];
current.parent = null;
}
if (!options.emitTopLevelOnly || parent === rootNode) {
reader2.emit(options.tagPrefix + current.name, current);
reader2.emit("tag", current.name, current);
}
if (current === rootNode) {
lexer$1.removeAllListeners("data");
reader2.emit(options.doneEvent, current);
rootNode = null;
}
current = parent;
break;
case Type.text:
if (current) {
current.children.push(createNode({
type: NodeType.text,
value: data.value,
parent: options.parentNodes ? current : null
}));
}
break;
case Type.attributeName:
attrName = data.value;
current.attributes[attrName] = "";
break;
case Type.attributeValue:
current.attributes[attrName] = data.value;
break;
}
};
reader2.reset = function() {
lexer$1 = lexer.create({ debug: options.debug });
lexer$1.on("data", handleLexerData);
rootNode = createNode();
current = null;
attrName = "";
reader2.parse = lexer$1.write;
};
reader2.reset();
return reader2;
};
var parseSync = function parseSync2(xml, options) {
options = Object.assign({}, options, { stream: false, tagPrefix: ":" });
var reader2 = create(options);
var res = undefined;
reader2.on("done", function(ast) {
res = ast;
});
reader2.parse(xml);
return res;
};
var reader = {
parseSync,
create,
NodeType
};
var reader_1 = reader.parseSync;
var parseInput = function parseInput2(input) {
var parsed = reader_1("<root>".concat(input, "</root>"), {
parentNodes: false
});
var isValid = parsed.children && parsed.children.length > 0 && parsed.children.every(function(node) {
return node.name === "svg";
});
if (isValid) {
return parsed.children.length === 1 ? parsed.children[0] : parsed.children;
} else {
throw Error("nothing to parse");
}
};
var camelize = function camelize2(node) {
return deepRenameKeys(node, function(key) {
if (!notCamelcase(key)) {
return toCamelCase(key);
}
return key;
});
};
var toCamelCase = function toCamelCase2(prop) {
return prop.replace(/[-|:]([a-z])/gi, function(all, letter) {
return letter.toUpperCase();
});
};
var notCamelcase = function notCamelcase2(prop) {
return /^(data|aria)(-\w+)/.test(prop);
};
var escapeText = function escapeText2(text) {
if (text) {
var str = String(text);
return /[&<>]/.test(str) ? "<![CDATA[".concat(str.replace(/]]>/, "]]]]><![CDATA[>"), "]]>") : str;
}
return "";
};
var escapeAttr = function escapeAttr2(attr) {
return String(attr).replace(/&/g, "&").replace(/'/g, "'").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
};
var svgsonSync = function svgsonSync2(input) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref$transformNode = _ref.transformNode, transformNode = _ref$transformNode === undefined ? function(node) {
return node;
} : _ref$transformNode, _ref$camelcase = _ref.camelcase, camelcase = _ref$camelcase === undefined ? false : _ref$camelcase;
var applyFilters = function applyFilters2(input2) {
var n;
n = transformNode(input2);
if (camelcase) {
n = camelize(n);
}
return n;
};
return applyFilters(parseInput(input));
};
function svgson() {
for (var _len = arguments.length, args = new Array(_len), _key = 0;_key < _len; _key++) {
args[_key] = arguments[_key];
}
return new Promise(function(resolve, reject) {
try {
var res = svgsonSync.apply(undefined, args);
resolve(res);
} catch (e) {
reject(e);
}
});
}
var stringify = function stringify2(_ast) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref$transformAttr = _ref.transformAttr, transformAttr = _ref$transformAttr === undefined ? function(key, value, escape) {
return "".concat(key, '="').concat(escape(value), '"');
} : _ref$transformAttr, _ref$transformNode = _ref.transformNode, transformNode = _ref$transformNode === undefined ? function(node) {
return node;
} : _ref$transformNode, _ref$selfClose = _ref.selfClose, selfClose = _ref$selfClose === undefined ? true : _ref$selfClose;
if (Array.isArray(_ast)) {
return _ast.map(function(ast2) {
return stringify2(ast2, {
transformAttr,
selfClose,
transformNode
});
}).join("");
}
var ast = transformNode(_ast);
if (ast.type === "text") {
return escapeText(ast.value);
}
var attributes = "";
for (var attr in ast.attributes) {
var attrStr = transformAttr(attr, ast.attributes[attr], escapeAttr, ast.name);
attributes += attrStr ? " ".concat(attrStr) : "";
}
return ast.children && ast.children.length > 0 || !selfClose ? "<".concat(ast.name).concat(attributes, ">").concat(stringify2(ast.children, {
transformAttr,
transformNode,
selfClose
}), "</").concat(ast.name, ">") : "<".concat(ast.name).concat(attributes, "/>");
};
var indexUmd = Object.assign({}, {
parse: svgson,
parseSync: svgsonSync,
stringify
});
return indexUmd;
});
});
// node_modules/@tscircuit/fanout-solver/lib/fanout-output-ids.ts
function createFanoutOutputIds(input) {
const endpointKey = `${input.connectionName}:source-${input.sourcePointIndex}`;
return {
boundaryExitPointId: `fanout-exit:${endpointKey}`,
planeExitPointId: `fanout-plane:${endpointKey}`,
traceId: `fanout:${endpointKey}`,
viaObstacleId: `fanout-via:${endpointKey}`,
planeEndpointPointId: `fanout-plane-endpoint-point:${endpointKey}`,
planeEndpointTraceId: `fanout-plane-endpoint:${endpointKey}`,
planeEndpointViaObstacleId: `fanout-plane-endpoint-via:${endpointKey}`
};
}
function createFanoutCompletionTraceId(input) {
return `fanout-completion:${input.connectionName}:source-${input.sourcePointIndex}:${input.candidateIndex}`;
}
// node_modules/@tscircuit/fanout-solver/lib/geometry.ts
var EPSILON = 0.000000001;
function obstacleIsCircular(obstacle) {
return obstacle.shape === "circle";
}
function toObstacleLocalPoint(point, obstacle) {
const rotationRadians = -(obstacle.ccwRotationDegrees ?? 0) * Math.PI / 180;
const dx = point.x - obstacle.center.x;
const dy = point.y - obstacle.center.y;
return {
x: dx * Math.cos(rotationRadians) - dy * Math.sin(rotationRadians),
y: dx * Math.sin(rotationRadians) + dy * Math.cos(rotationRadians)
};
}
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function distancePointToSegment(point, start, end) {
const dx = end.x - start.x;
const dy = end.y - start.y;
const lengthSquared = dx * dx + dy * dy;
const rawT = lengthSquared < EPSILON ? 0 : ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared;
const t = Math.max(0, Math.min(1, rawT));
return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy));
}
function cross(origin, a, b) {
return (a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x);
}
function segmentsProperlyCross(a, b, c, d) {
const d1 = cross(c, d, a);
const d2 = cross(c, d, b);
const d3 = cross(a, b, c);
const d4 = cross(a, b, d);
return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
}
function distanceSegmentToSegment(firstStart, firstEnd, secondStart, secondEnd) {
if (segmentsProperlyCross(firstStart, firstEnd, secondStart, secondEnd)) {
return 0;
}
return Math.min(distancePointToSegment(firstStart, secondStart, secondEnd), distancePointToSegment(firstEnd, secondStart, secondEnd), distancePointToSegment(secondStart, firstStart, firstEnd), distancePointToSegment(secondEnd, firstStart, firstEnd));
}
function pointIsInsideObstacle(point, obstacle, tolerance = EPSILON) {
if (obstacleIsCircular(obstacle)) {
return distance(point, obstacle.center) <= obstacle.width / 2 + tolerance;
}
const localPoint = toObstacleLocalPoint(point, obstacle);
return Math.abs(localPoint.x) <= obstacle.width / 2 + tolerance && Math.abs(localPoint.y) <= obstacle.height / 2 + tolerance;
}
function circleFitsInsideObstacle(params) {
const { center, diameter, obstacle, tolerance = EPSILON } = params;
const radius = diameter / 2;
if (obstacleIsCircular(obstacle)) {
return distance(center, obstacle.center) + radius <= obstacle.width / 2 + tolerance;
}
const localCenter = toObstacleLocalPoint(center, obstacle);
return Math.abs(localCenter.x) + radius <= obstacle.width / 2 + tolerance && Math.abs(localCenter.y) + radius <= obstacle.height / 2 + tolerance;
}
function distancePointToObstacle(point, obstacle) {
if (obstacleIsCircular(obstacle)) {
return Math.max(0, distance(point, obstacle.center) - obstacle.width / 2);
}
const localPoint = toObstacleLocalPoint(point, obstacle);
const dx = Math.max(Math.abs(localPoint.x) - obstacle.width / 2, 0);
const dy = Math.max(Math.abs(localPoint.y) - obstacle.height / 2, 0);
return Math.hypot(dx, dy);
}
function distanceSegmentToObstacle(segment, obstacle) {
if (obstacleIsCircular(obstacle)) {
return Math.max(0, distancePointToSegment(obstacle.center, segment.start, segment.end) - obstacle.width / 2);
}
const localStart = toObstacleLocalPoint(segment.start, obstacle);
const localEnd = toObstacleLocalPoint(segment.end, obstacle);
if (Math.abs(localStart.x) <= obstacle.width / 2 + EPSILON && Math.abs(localStart.y) <= obstacle.height / 2 + EPSILON) {
return 0;
}
if (Math.abs(localEnd.x) <= obstacle.width / 2 + EPSILON && Math.abs(localEnd.y) <= obstacle.height / 2 + EPSILON) {
return 0;
}
const minX = -obstacle.width / 2;
const maxX = obstacle.width / 2;
const minY = -obstacle.height / 2;
const maxY = obstacle.height / 2;
const corners = [
{ x: minX, y: minY },
{ x: maxX, y: minY },
{ x: maxX, y: maxY },
{ x: minX, y: maxY }
];
let minimumDistance = Number.POSITIVE_INFINITY;
for (let index = 0;index < corners.length; index++) {
minimumDistance = Math.min(minimumDistance, distanceSegmentToSegment(localStart, localEnd, corners[index], corners[(index + 1) % corners.length]));
}
return minimumDistance;
}
function segmentsAreClear(first, second, clearance) {
if (first.layer !== second.layer)
return true;
const requiredDistance = (first.width + second.width) / 2 + clearance;
return distanceSegmentToSegment(first.start, first.end, second.start, second.end) >= requiredDistance - EPSILON;
}
// node_modules/@tscircuit/fanout-solver/lib/net-identity.ts
var identityCache = new WeakMap;
function getConnectionNetKey(connection) {
return connection.netConnectionName ?? connection.rootConnectionName ?? connection.name;
}
function addTokenNet(tokenNetKeys, token, netKey) {
if (!token)
return false;
const keys = tokenNetKeys.get(token) ?? new Set;
const sizeBefore = keys.size;
keys.add(netKey);
tokenNetKeys.set(token, keys);
return keys.size !== sizeBefore;
}
function getKnownNetKeys(tokenNetKeys, tokens) {
const keys = new Set;
for (const token of tokens) {
for (const key of tokenNetKeys.get(token) ?? [])
keys.add(key);
}
return keys;
}
function createElectricalNetIdentity(srj) {
const connectionNetKeys = new Map;
const tokenNetKeys = new Map;
for (const connection of srj.connections) {
const netKey = getConnectionNetKey(connection);
connectionNetKeys.set(connection.name, netKey);
addTokenNet(tokenNetKeys, connection.name, netKey);
addTokenNet(tokenNetKeys, connection.rootConnectionName, netKey);
addTokenNet(tokenNetKeys, connection.netConnectionName, netKey);
for (const point of connection.pointsToConnect) {
addTokenNet(tokenNetKeys, point.pointId, netKey);
addTokenNet(tokenNetKeys, point.pcb_port_id, netKey);
}
}
for (const trace of srj.traces ?? []) {
const traceTokens = [
trace.connection_name,
trace.pcb_trace_id,
...trace.connectsTo ?? []
];
const knownNetKeys = getKnownNetKeys(tokenNetKeys, traceTokens);
if (knownNetKeys.size !== 1)
continue;
const netKey = [...knownNetKeys][0];
connectionNetKeys.set(trace.connection_name, netKey);
for (const token of traceTokens) {
addTokenNet(tokenNetKeys, token, netKey);
}
}
for (let pass = 0;pass < 2; pass++) {
let changed = false;
for (const obstacle of srj.obstacles) {
const netKeys = getKnownNetKeys(tokenNetKeys, obstacle.connectedTo);
if (netKeys.size !== 1)
continue;
const netKey = [...netKeys][0];
for (const token of obstacle.connectedTo) {
changed = addTokenNet(tokenNetKeys, token, netKey) || changed;
}
}
if (!changed)
break;
}
const parentByNetKey = new Map;
const findRoot = (netKey) => {
const parent = parentByNetKey.get(netKey) ?? netKey;
parentByNetKey.set(netKey, parent);
if (parent === netKey)
return netKey;
const root = findRoot(parent);
parentByNetKey.set(netKey, root);
return root;
};
const union = (first, second) => {
const firstRoot = findRoot(first);
const secondRoot = findRoot(second);
if (firstRoot !== secondRoot)
parentByNetKey.set(secondRoot, firstRoot);
};
for (const netKeys of tokenNetKeys.values()) {
const [firstNetKey, ...otherNetKeys] = [...netKeys];
if (!firstNetKey)
continue;
for (const otherNetKey of otherNetKeys)
union(firstNetKey, otherNetKey);
}
for (const connectedTokens of [
...srj.obstacles.map((obstacle) => obstacle.connectedTo),
...(srj.traces ?? []).map((trace) => trace.connectsTo ?? [])
]) {
const [firstNetKey, ...otherNetKeys] = [
...getKnownNetKeys(tokenNetKeys, connectedTokens)
];
if (!firstNetKey)
continue;
for (const otherNetKey of otherNetKeys)
union(firstNetKey, otherNetKey);
}
for (const [connectionName, netKey] of connectionNetKeys) {
connectionNetKeys.set(connectionName, findRoot(netKey));
}
for (const [token, netKeys] of tokenNetKeys) {
tokenNetKeys.set(token, new Set([...netKeys].map((netKey) => findRoot(netKey))));
}
return { connectionNetKeys, tokenNetKeys };
}
function getElectricalNetIdentity(srj) {
const cached = identityCache.get(srj);
if (cached)
return cached;
const identity = createElectricalNetIdentity(srj);
identityCache.set(srj, identity);
return identity;
}
function connectionsShareElectricalNet(srj, firstConnectionName, secondConnectionName) {
const identity = getElectricalNetIdentity(srj);
const firstNet = identity.connectionNetKeys.get(firstConnectionName);
const secondNet = identity.connectionNetKeys.get(secondConnectionName);
return firstNet !== undefined && firstNet === secondNet;
}
function obstacleSharesElectricalNet(srj, obstacle, connectionName) {
const identity = getElectricalNetIdentity(srj);
const connectionNet = identity.connectionNetKeys.get(connectionName);
if (!connectionNet)
return false;
return obstacle.connectedTo.some((token) => identity.tokenNetKeys.get(token)?.has(connectionNet));
}
// node_modules/@tscircuit/fanout-solver/lib/layer-names.ts
function getCopperLayerNames(layerCount) {
if (!Number.isInteger(layerCount) || layerCount < 1) {
throw new Error(`FanoutSolver: layerCount must be a positive integer, received ${layerCount}`);
}
if (layerCount === 1)
return ["top"];
if (layerCount === 2)
return ["top", "bottom"];
return [
"top",
...Array.from({ length: layerCount - 2 }, (_, index) => `inner${index + 1}`),
"bottom"
];
}
function getLayerSpan(fromLayer, toLayer, layerNames) {
const fromIndex = layerNames.indexOf(fromLayer);
const toIndex = layerNames.indexOf(toLayer);
if (fromIndex < 0 || toIndex < 0) {
throw new Error(`FanoutSolver: cannot build via span from "${fromLayer}" to "${toLayer}"`);
}
const firstIndex = Math.min(fromIndex, toIndex);
const lastIndex = Math.max(fromIndex, toIndex);
return layerNames.slice(firstIndex, lastIndex + 1);
}
function getViaSpanLayers(params) {
const { fromLayer, toLayer, layerNames, allowBlindAndBuriedVias } = params;
const logicalSpan = getLayerSpan(fromLayer, toLayer, layerNames);
return allowBlindAndBuriedVias ? logicalSpan : [...layerNames];
}
function getRouteViaSpanLayers(params) {
const { fromLayer, toLayer, layers, layerNames, allowBlindAndBuriedVias } = params;
const configuredSpan = getViaSpanLayers({
fromLayer,
toLayer,
layerNames,
allowBlindAndBuriedVias
});
if (!layers)
return configuredSpan;
if (layers.length === 0 || !layers.includes(fromLayer) || !layers.includes(toLayer) || layers.some((layer) => !layerNames.includes(layer))) {
throw new Error(`FanoutSolver: route via from "${fromLayer}" to "${toLayer}" has an invalid physical layer span`);
}
return [...layers];
}
function generateLayerAssignments(params) {
const { busIds, layers, layersByBusId, maxAssignments } = params;
if (layers.length === 0) {
throw new Error("FanoutSolver: no escape layers are available");
}
if (!Number.isInteger(maxAssignments) || maxAssignments < 1) {
throw new Error(`FanoutSolver: maxLayerCombinations must be positive, received ${maxAssignments}`);
}
const availableLayersByBus = busIds.map((busId) => layersByBusId?.[busId] ?? layers);
const busWithoutLayersIndex = availableLayersByBus.findIndex((availableLayers) => availableLayers.length === 0);
if (busWithoutLayersIndex >= 0) {
throw new Error(`FanoutSolver: no escape layers are available for bus "${busIds[busWithoutLayersIndex]}"`);
}
const rawCombinationCount = availableLayersByBus.reduce((count, availableLayers) => count * availableLayers.length, 1);
const combinationCount = Math.min(maxAssignments, Number.isFinite(rawCombinationCount) ? rawCombinationCount : maxAssignments);
const assignments = [];
const seenAssignments = new Set;
function addAssignment(layerIndexes) {
const assignment = {};
for (let busIndex = 0;busIndex < busIds.length; busIndex++) {
assignment[busIds[busIndex]] = availableLayersByBus[busIndex][layerIndexes[busIndex]];
}
const key = JSON.stringify(assignment);
if (seenAssignments.has(key))
return;
seenAssignments.add(key);
assignments.push(assignment);
}
if (rawCombinationCount <= maxAssignments) {
for (let ordinal = 0;ordinal < combinationCount; ordinal++) {
const layerIndexes = [];
let remaining = ordinal;
for (let busIndex = 0;busIndex < busIds.length; busIndex++) {
const layerCount = availableLayersByBus[busIndex].length;
const digit = remaining % layerCount;
remaining = Math.floor(remaining / layerCount);
layerIndexes.push((digit + busIndex) % layerCount);
}
addAssignment(layerIndexes);
}
return assignments;
}
function mix32(value) {
let mixed = value | 0;
mixed = Math.imul(mixed ^ mixed >>> 16, 569420461);
mixed = Math.imul(mixed ^ mixed >>> 15, 1935289751);
return (mixed ^ mixed >>> 15) >>> 0;
}
const balancedLayerIndexes = busIds.map((_, busIndex) => busIndex % availableLayersByBus[busIndex].length);
addAssignment(balancedLayerIndexes);
const maximumAvailableLayerCount = Math.max(...availableLayersByBus.map((availableLayers) => availableLayers.length));
for (let globalShift = 1;globalShift < maximumAvailableLayerCount && assignments.length < combinationCount; globalShift++) {
addAssignment(balancedLayerIndexes.map((layerIndex, busIndex) => (layerIndex + globalShift) % availableLayersByBus[busIndex].length));
}
for (let busIndex = 0;busIndex < busIds.length && assignments.length < combinationCount; busIndex++) {
for (let shift = 1;shift < availableLayersByBus[busIndex].length && assignments.length < combinationCount; shift++) {
const layerIndexes = [...balancedLayerIndexes];
layerIndexes[busIndex] = (balancedLayerIndexes[busIndex] + shift) % availableLayersByBus[busIndex].length;
addAssignment(layerIndexes);
}
}
for (let seed = 1;assignments.length < combinationCount && seed < combinationCount * 20; seed++) {
addAssignment(busIds.map((_, busIndex) => mix32(seed * 2654435761 + busIndex * 2246822507) % availableLayersByBus[busIndex].length));
}
return assignments;
}
// node_modules/@tscircuit/fanout-solver/lib/validate-original-endpoint-connectivity.ts
var EPSILON2 = 0.000001;
function getPointLayers(point) {
return "layer" in point ? [point.layer] : point.layers;
}
function layersOverlap(first, second) {
return first.some((layer) => second.includes(layer));
}
function extractTraceCopper(trace, layerNames) {
const copper = [];
let previousWire;
for (const routePoint of trace.route) {
if (routePoint.route_type === "via") {
copper.push({
type: "via",
center: { x: routePoint.x, y: routePoint.y },
diameter: routePoint.via_diameter ?? 0,
layers: getLayerSpan(routePoint.from_layer, routePoint.to_layer, layerNames)
});
previousWire = undefined;
continue;
}
if (routePoint.route_type !== "wire")
continue;
if (previousWire && previousWire.layer === routePoint.layer && distance(previousWire, routePoint) > EPSILON2) {
copper.push({
type: "segment",
segment: {
start: { x: previousWire.x, y: previousWire.y },
end: { x: routePoint.x, y: routePoint.y },
width: routePoint.width,
layer: routePoint.layer
}
});
}
previousWire = routePoint;
}
return copper;
}
function primitivesTouch(first, second) {
if (first.type === "plane" && second.type === "plane") {
return first.layer === second.layer;
}
if (first.type === "plane" && second.type === "via") {
return second.layers.includes(first.layer);
}
if (first.type === "via" && second.type === "plane") {
return primitivesTouch(second, first);
}
if (first.type === "plane" && second.type === "segment") {
return first.layer === second.segment.layer;
}
if (first.type === "segment" && second.type === "plane") {
return primitivesTouch(second, first);
}
if (first.type === "endpoint" && second.type === "endpoint") {
return layersOverlap(first.layers, second.layers) && distance(first.point, second.point) <= EPSILON2;
}
if (first.type === "endpoint" && second.type === "obstacle") {
return layersOverlap(first.layers, second.obstacle.layers) && pointIsInsideObstacle(first.point, second.obstacle, EPSILON2);
}
if (first.type === "obstacle" && second.type === "endpoint") {
return primitivesTouch(second, first);
}
if (first.type === "endpoint" && second.type === "segment") {
return first.layers.includes(second.segment.layer) && distancePointToSegment(first.point, second.segment.start, second.segment.end) <= second.segment.width / 2 + EPSILON2;
}
if (first.type === "segment" && second.type === "endpoint") {
return primitivesTouch(second, first);
}
if (first.type === "endpoint" && second.type === "via") {
return layersOverlap(first.layers, second.layers) && distance(first.point, second.center) <= second.diameter / 2 + EPSILON2;
}
if (first.type === "via" && second.type === "endpoint") {
return primitivesTouch(second, first);
}
if (first.type === "obstacle" && second.type === "segment") {
return first.obstacle.layers.includes(second.segment.layer) && distanceSegmentToObstacle(second.segment, first.obstacle) <= second.segment.width / 2 + EPSILON2;
}
if (first.type === "segment" && second.type === "obstacle") {
return primitivesTouch(second, first);
}
if (first.type === "obstacle" && second.type === "via") {
return layersOverlap(first.obstacle.layers, second.layers) && distancePointToObstacle(second.center, first.obstacle) <= second.diameter / 2 + EPSILON2;
}
if (first.type === "via" && second.type === "obstacle") {
return primitivesTouch(second, first);
}
if (first.type === "segment" && second.type === "segment") {
return first.segment.layer === second.segment.layer && distanceSegmentToSegment(first.segment.start, first.segment.end, second.segment.start, second.segment.end) <= (first.segment.width + second.segment.width) / 2 + EPSILON2;
}
if (first.type === "segment" && second.type === "via") {
return second.layers.includes(first.segment.layer) && distancePointToSegment(second.center, first.segment.start, first.segment.end) <= second.diameter / 2 + first.segment.width / 2 + EPSILON2;
}
if (first.type === "via" && second.type === "segment") {
return primitivesTouch(second, first);
}
if (first.type === "via" && second.type === "via") {
return layersOverlap(first.layers, second.layers) && distance(first.center, second.center) <= (first.diameter + second.diameter) / 2 + EPSILON2;
}
return false;
}
class DisjointSet {
parent;
constructor(size) {
this.parent = Array.from({ length: size }, (_, index) => index);
}
find(index) {
const parent = this.parent[index];
if (parent === index)
return index;
const root = this.find(parent);
this.parent[index] = root;
return root;
}
union(first, second) {
const firstRoot = this.find(first);
const secondRoot = this.find(second);
if (firstRoot !== secondRoot)
this.parent[secondRoot] = firstRoot;
}
}
function traceBelongsToNet(inputSrj, trace, representativeConnection) {
return Boolean(trace.connection_name && connectionsShareElectricalNet(inputSrj, trace.connection_name, representativeConnection.name));
}
function validateOriginalEndpointConnectivity(params) {
const { inputSrj, routedSrj } = params;
const planeConnectivity = routedSrj.fanoutPlaneConnectivity;
const layerNames = getCopperLayerNames(routedSrj.layerCount);
const connectionsByNet = new Map;
for (const connection of inputSrj.connections) {
const netKey = getConnectionNetKey(connection);
const connections = connectionsByNet.get(netKey) ?? [];
connections.push(connection);
connectionsByNet.set(netKey, connections);
}
const issues = [];
let connectedConnectionCount = 0;
let connectedEndpointCount = 0;
for (const connections of connectionsByNet.values()) {
const representativeConnection = connections[0];
const endpointCopper = connections.flatMap((connection) => connection.pointsToConnect.map((point, endpointIndex) => ({
type: "endpoint",
connectionName: connection.name,
endpointIndex,
point,
layers: getPointLayers(point)
})));
const obstacleCopper = inputSrj.obstacles.filter((obstacle) => obstacleSharesElectricalNet(inputSrj, obstacle, representativeConnection.name)).map((obstacle) => ({ type: "obstacle", obstacle }));
const routeCopper = (routedSrj.traces ?? []).filter((trace) => traceBelongsToNet(inputSrj, trace, representativeConnection)).flatMap((trace) => extractTraceCopper(trace, layerNames));
const planeCopper = [
...new Set((planeConnectivity ?? []).flatMap((plane) => connectionsShareElectricalNet(inputSrj, plane.connectionName, representativeConnection.name) ? [plane.layer] : []))
].map((layer) => ({ type: "plane", layer }));
const copper = [
...endpointCopper,
...obstacleCopper,
...routeCopper,
...planeCopper
];
const connectedCopper = new DisjointSet(copper.length);
for (let firstIndex = 0;firstIndex < copper.length; firstIndex++) {
for (let secondIndex = firstIndex + 1;secondIndex < copper.length; secondIndex++) {
if (primitivesTouch(copper[firstIndex], copper[secondIndex])) {
connectedCopper.union(firstIndex, secondIndex);
}
}
}
const endpointIndexByConnection = new Map;
for (let index = 0;index < endpointCopper.length; index++) {
const endpoint = endpointCopper[index];
const indices = endpointIndexByConnection.get(endpoint.connectionName) ?? [];
indices[endpoint.endpointIndex] = index;
endpointIndexByConnection.set(endpoint.connectionName, indices);
}
for (const connection of connections) {
const endpointIndices = endpointIndexByConnection.get(connection.name) ?? [];
const firstEndpointIndex = endpointIndices[0];
const firstRoot = firstEndpointIndex === undefined ? undefined : connectedCopper.find(firstEndpointIndex);
const disconnectedEndpointIndices = endpointIndices.flatMap((endpointIndex, index) => firstRoot === undefined || connectedCopper.find(endpointIndex) !== firstRoot ? [index] : []);
connectedEndpointCount += endpointIndices.length - disconnectedEndpointIndices.length;
if (disconnectedEndpointIndices.length === 0) {
connectedConnectionCount++;
} else {
issues.push({
code: "original-endpoints-disconnected",
connectionName: connection.name,
disconnectedEndpointIndices,
message: `Connection ${connection.name} has no physical copper path from endpoint 0 to original endpoint${disconnectedEndpointIndices.length === 1 ? "" : "s"} ${disconnectedEndpointIndices.join(", ")}`
});
}
}
}
const checkedEndpointCount = inputSrj.connections.reduce((count, connection) => count + connection.pointsToConnect.length, 0);
return {
valid: issues.length === 0,
checkedConnectionCount: inputSrj.connections.length,
connectedConnectionCount,
checkedEndpointCount,
connectedEndpointCount,
issues
};
}
// node_modules/@tscircuit/fanout-solver/lib/validate-routed-copper-drc.ts
var EPSILON3 = 0.000001;
function segmentIsLegalTerminalBodyEscape(params) {
const { inputSrj, segment, bodyObstacle, connectionName } = params;
if (bodyObstacle.connectedTo.length > 0 || !bodyObstacle.componentId) {
return false;
}
const sameNetComponentPads = inputSrj.obstacles.filter((obstacle) => obstacle.componentId === bodyObstacle.componentId && obstacle.layers.includes(segment.layer) && obstacleSharesElectricalNet(inputSrj, obstacle, connectionName));
for (const pad of sameNetComponentPads) {
for (const [terminal, other] of [
[segment.start, segment.end],
[segment.end, segment.start]
]) {
if (!pointIsInsideObstacle(terminal, pad, EPSILON3))
continue;
const outwardFromBody = {
x: terminal.x - bodyObstacle.center.x,
y: terminal.y - bodyObstacle.center.y
};
const terminalToOther = {
x: other.x - terminal.x,
y: other.y - terminal.y
};
if (outwardFromBody.x * terminalToOther.x + outwardFromBody.y * terminalToOther.y > EPSILON3) {
return true;
}
}
}
return false;
}
function pointsMatch(first, second) {
return distance(first, second) <= EPSILON3;
}
function addIssue(issues, issue) {
issues.push(issue);
}
function extractTraceCopper2(params) {
const {
srj,
trace,
connectionName,
layerNames,
allowBlindAndBuriedVias,
issues
} = params;
const segments = [];
const vias = [];
let previousWire;
let pendingVia;
for (const routePoint of trace.route) {
if (routePoint.route_type === "via") {
const spanLayers = getRouteViaSpanLayers({
fromLayer: routePoint.from_layer,
toLayer: routePoint.to_layer,
layers: "layers" in routePoint && Array.isArray(routePoint.layers) ? routePoint.layers : undefined,
layerNames,
allowBlindAndBuriedVias
});
vias.push({
center: { x: routePoint.x, y: routePoint.y },
diameter: routePoint.via_diameter ?? srj.minViaPadDiameter ?? srj.minTraceWidth,
spanLayers
});
if (!previousWire || !pointsMatch(previousWire, routePoint) || previousWire.layer !== routePoint.from_layer) {
addIssue(issues, {
code: "disconnected-trace",
traceId: trace.pcb_trace_id,
connectionName,
message: `Trace ${trace.pcb_trace_id} reaches a via without a matching ${routePoint.from_layer} wire endpoint`
});
}
pendingVia = routePoint;
continue;
}
if (routePoint.route_type !== "wire") {
addIssue(issues, {
code: "unsupported-route-point",
traceId: trace.pcb_trace_id,
connectionName,
message: `Trace ${trace.pcb_trace_id} contains unsupported ${routePoint.route_type} geometry`
});
previousWire = undefined;
pendingVia = undefined;
continue;
}
if (pendingVia) {
if (!pointsMatch(routePoint, pendingVia) || routePoint.layer !== pendingVia.to_layer) {
addIssue(issues, {
code: "disconnected-trace",
traceId: trace.pcb_trace_id,
connectionName,
message: `Trace ${trace.pcb_trace_id} does not continue from its via on ${pendingVia.to_layer}`
});
}
previousWire = routePoint;
pendingVia = undefined;
continue;
}
if (previousWire) {
if (previousWire.layer !== routePoint.layer) {
addIssue(issues, {
code: "disconnected-trace",
traceId: trace.pcb_trace_id,
connectionName,
message: `Trace ${trace.pcb_trace_id} changes from ${previousWire.layer} to ${routePoint.layer} without a via`
});
} else if (!pointsMatch(previousWire, routePoint)) {
segments.push({
start: { x: previousWire.x, y: previousWire.y },
end: { x: routePoint.x, y: routePoint.y },
width: routePoint.width,
layer: routePoint.layer
});
}
}
previousWire = routePoint;
}
if (pendingVia) {
addIssue(issues, {
code: "disconnected-trace",
traceId: trace.pcb_trace_id,
connectionName,
message: `Trace ${trace.pcb_trace_id} ends at a via without a wire on ${pendingVia.to_layer}`
});
}
return { trace, connectionName, segments, vias };
}
function validateRoutedCopperDrc(params) {
const {
inputSrj,
routedSrj,
clearance,
allowBlindAndBuriedVias = true
} = params;
const issues = [];
const layerNames = getCopperLayerNames(routedSrj.layerCount);
const traceCopper = [];
const originalAndRoutedEndpoints = [inputSrj, routedSrj].flatMap((srj) => srj.connections.flatMap((connection) => connection.pointsToConnect.map((point) => ({
connectionName: connection.name,
point
}))));
for (const trace of routedSrj.traces ?? []) {
const connectionName = trace.connection_name;
if (!connectionName || !inputSrj.connections.some((connection) => connection.name === connectionName)) {
addIssue(issues, {
code: "unknown-trace-connection",
traceId: trace.pcb_trace_id,
...connectionName ? { connectionName } : {},
message: `Trace ${trace.pcb_trace_id} does not identify an input connection`
});
continue;
}
traceCopper.push(extractTraceCopper2({
srj: routedSrj,
trace,
connectionName,
layerNames,
allowBlindAndBuriedVias,
issues
}));
}
for (const copper of traceCopper) {
for (const segment of copper.segments) {
for (const obstacle of inputSrj.obstacles) {
if (!obstacle.layers.includes(segment.layer) || obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName)) {
continue;
}
if (segmentIsLegalTerminalBodyEscape({
inputSrj,
segment,
bodyObstacle: obstacle,
connectionName: copper.connectionName
})) {
continue;
}
const actual = distanceSegmentToObstacle(segment, obstacle);
const required = segment.width / 2 + clearance;
if (actual < required - EPSILON3) {
addIssue(issues, {
code: "trace-obstacle-clearance",
traceId: copper.trace.pcb_trace_id,
connectionName: copper.connectionName,
layer: segment.layer,
obstacleId: obstacle.obstacleId,
message: `Trace ${copper.trace.pcb_trace_id} on ${segment.layer} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId}; ${required.toFixed(4)}mm is required`
});
}
}
}
for (const via of copper.vias) {
const coincidentEndpoint = originalAndRoutedEndpoints.find(({ point }) => distance(via.center, point) <= EPSILON3);
const isAllowedContainedViaInPad = inputSrj.allowViaInPad === true && inputSrj.obstacles.some((obstacle) => obstacle.layers.some((layer) => via.spanLayers.includes(layer)) && obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName) && circleFitsInsideObstacle({
center: via.center,
diameter: via.diameter,
obstacle,
tolerance: EPSILON3
}));
if (coincidentEndpoint && !isAllowedContainedViaInPad) {
addIssue(issues, {
code: "via-at-endpoint",
traceId: copper.trace.pcb_trace_id,
connectionName: copper.connectionName,
otherConnectionName: coincidentEndpoint.connectionName,
message: `Via in ${copper.trace.pcb_trace_id} is placed directly at an original or routed connection endpoint`
});
}
for (const obstacle of inputSrj.obstacles) {
if (!obstacle.layers.some((layer) => via.spanLayers.includes(layer)) || obstacleSharesElectricalNet(inputSrj, obstacle, copper.connectionName)) {
continue;
}
const actual = distancePointToObstacle(via.center, obstacle);
const required = via.diameter / 2 + clearance;
if (actual < required - EPSILON3) {
addIssue(issues, {
code: "via-obstacle-clearance",
traceId: copper.trace.pcb_trace_id,
connectionName: copper.connectionName,
obstacleId: obstacle.obstacleId,
message: `Via in ${copper.trace.pcb_trace_id} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId} on its layer span; ${required.toFixed(4)}mm is required`
});
}
}
}
}
for (let firstIndex = 0;firstIndex < traceCopper.length; firstIndex++) {
const first = traceCopper[firstIndex];
for (let secondIndex = firstIndex + 1;secondIndex < traceCopper.length; secondIndex++) {
const second = traceCopper[secondIndex];
if (connectionsShareElectricalNet(inputSrj, first.connectionName, second.connectionName)) {
continue;
}
for (const firstSegment of first.segments) {
for (const secondSegment of second.segments) {
if (segmentsAreClear(firstSegment, secondSegment, clearance))
continue;
addIssue(issues, {
code: "different-net-trace-clearance",
traceId: first.trace.pcb_trace_id,
connectionName: first.connectionName,
otherTraceId: second.trace.pcb_trace_id,
otherConnectionName: second.connectionName,
layer: firstSegment.layer,
message: `Different-net traces ${first.trace.pcb_trace_id} and ${second.trace.pcb_trace_id} intersect or violate clearance on ${firstSegment.layer}`
});
}
for (const secondVia of second.vias) {
if (!secondVia.spanLayers.includes(firstSegment.layer) || distancePointToSegment(secondVia.center, firstSegment.start, firstSegment.end) >= secondVia.diameter / 2 + firstSegment.width / 2 + clearance - EPSILON3) {
continue;
}
addIssue(issues, {
code: "different-net-trace-via-clearance",
traceId: first.trace.pcb_trace_id,
connectionName: first.connectionName,
otherTraceId: second.trace.pcb_trace_id,
otherConnectionName: second.connectionName,
layer: firstSegment.layer,
message: `Trace ${first.trace.pcb_trace_id} violates via clearance to ${second.trace.pcb_trace_id} on ${firstSegment.layer}`
});
}
}
for (const firstVia of first.vias) {
for (const secondSegment of second.segments) {
if (!firstVia.spanLayers.includes(secondSegment.layer) || distancePointToSegment(firstVia.center, secondSegment.start, secondSegment.end) >= firstVia.diameter / 2 + secondSegment.width / 2 + clearance - EPSILON3) {
continue;
}
addIssue(issues, {
code: "different-net-trace-via-clearance",
traceId: first.trace.pcb_trace_id,
connectionName: first.connectionName,
otherTraceId: second.trace.pcb_trace_id,
otherConnectionName: second.connectionName,
layer: secondSegment.layer,
message: `Via in ${first.trace.pcb_trace_id} violates trace clearance to ${second.trace.pcb_trace_id} on ${secondSegment.layer}`
});
}
for (const secondVia of second.vias) {
if (!firstVia.spanLayers.some((layer) => secondVia.spanLayers.includes(layer)) || distance(firstVia.center, secondVia.center) >= (firstVia.diameter + secondVia.diameter) / 2 + clearance - EPSILON3) {
continue;
}
addIssue(issues, {
code: "different-net-via-clearance",
traceId: first.trace.pcb_trace_id,
connectionName: first.connectionName,
otherTraceId: second.trace.pcb_trace_id,
otherConnectionName: second.connectionName,
message: `Different-net vias in ${first.trace.pcb_trace_id} and ${second.trace.pcb_trace_id} violate clearance on an overlapping layer span`
});
}
}
}
}
return {
valid: issues.length === 0,
checkedTraceCount: routedSrj.traces?.length ?? 0,
checkedSegmentCount: traceCopper.reduce((count, copper) => count + copper.segments.length, 0),
checkedViaCount: traceCopper.reduce((count, copper) => count + copper.vias.length, 0),
issues
};
}
// node_modules/@tscircuit/fanout-solver/lib/complete-original-endpoints.ts
var EPSILON4 = 0.000000001;
function getPointLayer(point, preferredLayer) {
if ("layer" in point)
return point.layer;
if (preferredLayer && point.layers.includes(preferredLayer)) {
return preferredLayer;
}
const layer = point.layers[0];
if (!layer)
throw new Error("Endpoint completion received a layerless point");
return layer;
}
function uniquePoints(points) {
const unique = [];
for (const point of points) {
if (unique.at(-1) && distance(unique.at(-1), point) < EPSILON4)
continue;
unique.push(point);
}
return unique;
}
function chamferOrthogonalPolyline(rawPoints, requestedChamfer) {
const points = uniquePoints(rawPoints);
if (points.length < 3)
return points;
const chamfered = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = points[index - 1];
const corner = points[index];
const next = points[index + 1];
const incomingLength = distance(previous, corner);
const outgoingLength = distance(corner, next);
const incoming = {
x: (corner.x - previous.x) / incomingLength,
y: (corner.y - previous.y) / incomingLength
};
const outgoing = {
x: (next.x - corner.x) / outgoingLength,
y: (next.y - corner.y) / outgoingLength
};
if (Math.abs(incoming.x * outgoing.x + incoming.y * outgoing.y) > 0.000001) {
chamfered.push(corner);
continue;
}
const chamfer = Math.min(requestedChamfer, incomingLength / 2, outgoingLength / 2);
chamfered.push({
x: corner.x - incoming.x * chamfer,
y: corner.y - incoming.y * chamfer
});
chamfered.push({
x: corner.x + outgoing.x * chamfer,
y: corner.y + outgoing.y * chamfer
});
}
chamfered.push(points.at(-1));
return uniquePoints(chamfered);
}
function findEndpointPad(params) {
const { inputSrj, connectionName, target, targetLayer } = params;
return inputSrj.obstacles.find((obstacle) => obstacle.layers.includes(targetLayer) && obstacleSharesElectricalNet(inputSrj, obstacle, connectionName) && pointIsInsideObstacle(target, obstacle, 0.000001));
}
function getTerminalDirections(params) {
const { inputSrj, plan } = params;
const targetLayer = getPointLayer(plan.targetPoint);
const targetPad = findEndpointPad({
inputSrj,
connectionName: plan.connectionName,
target: plan.targetPoint,
targetLayer
});
const body = inputSrj.obstacles.find((obstacle) => obstacle.componentId === targetPad?.componentId && obstacle.connectedTo.length === 0);
const rawOutward = body ? {
x: plan.targetPoint.x - body.center.x,
y: plan.targetPoint.y - body.center.y
} : {
x: plan.targetPoint.x - plan.sourcePoint.x,
y: plan.targetPoint.y - plan.sourcePoint.y
};
const length = Math.hypot(rawOutward.x, rawOutward.y);
const outward = length > EPSILON4 ? { x: rawOutward.x / length, y: rawOutward.y / length } : { x: 1, y: 0 };
return {
outward,
perpendicular: { x: -outward.y, y: outward.x }
};
}
function createBranchTrace(params) {
const {
plan,
branchStart,
viaPoint,
assignedLayerPath,
terminalApproach,
traceWidth,
viaDiameter,
viaHoleDiameter,
candidateIndex,
chamfer
} = params;
const targetLayer = getPointLayer(plan.targetPoint);
const firstPath = chamferOrthogonalPolyline(assignedLayerPath, chamfer);
const terminalPath = chamferOrthogonalPolyline([
viaPoint,
...terminalApproach ? [terminalApproach] : [],
plan.targetPoint
], chamfer);
const route = firstPath.map((point) => ({
route_type: "wire",
x: point.x,
y: point.y,
width: traceWidth,
layer: branchStart.layer
}));
if (branchStart.layer !== targetLayer) {
route.push({
route_type: "via",
x: viaPoint.x,
y: viaPoint.y,
from_layer: branchStart.layer,
to_layer: targetLayer,
via_diameter: viaDiameter,
via_hole_diameter: viaHoleDiameter
});
route.push({
route_type: "wire",
x: viaPoint.x,
y: viaPoint.y,
width: traceWidth,
layer: targetLayer
});
}
for (const point of terminalPath.slice(1)) {
route.push({
route_type: "wire",
x: point.x,
y: point.y,
width: traceWidth,
layer: targetLayer
});
}
return {
type: "pcb_trace",
pcb_trace_id: createFanoutCompletionTraceId({
connectionName: plan.connectionName,
sourcePointIndex: plan.sourcePointIndex,
candidateIndex
}),
connection_name: plan.connectionName,
route
};
}
function connectionIsComplete(report, connectionName) {
return !report.issues.some((issue) => issue.connectionName === connectionName);
}
function traceHasViaAtEndpoint(params) {
const { trace, endpointSrjs } = params;
const endpoints = endpointSrjs.flatMap((srj) => srj.connections.flatMap((connection) => connection.pointsToConnect));
return trace.route.some((routePoint) => routePoint.route_type === "via" && endpoints.some((endpoint) => distance(routePoint, endpoint) <= 0.000001));
}
function findLocalBranch(params) {
const {
inputSrj,
fanoutSrj,
plan,
acceptedTraces,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
} = params;
const { outward, perpendicular } = getTerminalDirections({ inputSrj, plan });
const branchStarts = [
...plan.via ? [
{
x: plan.via.center.x,
y: plan.via.center.y,
layer: plan.targetLayer
}
] : [],
{
x: plan.sourcePoint.x,
y: plan.sourcePoint.y,
layer: plan.sourceLayer
}
];
let bestBlockingConnectionNames = [];
let bestIssueCount = Number.POSITIVE_INFINITY;
let candidateIndex = 0;
const maximumCandidateCount = inputSrj.connections.length > 32 ? 600 : Number.POSITIVE_INFINITY;
const planeTraceTransitionPoints = plan.termination.type === "plane" ? getPointsBackAlongTrace({
trace: plan.trace,
endpoint: plan.exitPoint,
layer: plan.sourceLayer,
distances: [0.2, 0.3, 0.4]
}) : [];
const candidateViaPoints = [
...plan.termination.type === "plane" && plan.via ? [{ ...plan.via.center }] : [],
...planeTraceTransitionPoints,
...[0.4, 0.8, 1.2, 1.6].flatMap((outwardDistance) => [0, 0.4, -0.4, 0.8, -0.8, 1.2, -1.2, 1.6, -1.6].map((perpendicularDistance) => ({
x: plan.sourcePoint.x + outward.x * outwardDistance + perpendicular.x * perpendicularDistance,
y: plan.sourcePoint.y + outward.y * outwardDistance + perpendicular.y * perpendicularDistance
})))
];
for (const viaPoint of candidateViaPoints) {
const candidateBranchStarts = [
...plan.termination.type === "plane" ? [{ ...viaPoint, layer: plan.targetLayer }] : [],
...branchStarts,
...planeTraceTransitionPoints.flatMap((transitionPoint) => distance(transitionPoint, viaPoint) <= 0.000001 ? [{ ...transitionPoint, layer: plan.sourceLayer }] : [])
];
for (const branchStart of candidateBranchStarts) {
const assignedLayerPaths = distance(branchStart, viaPoint) <= 0.000001 ? [[branchStart]] : [
[branchStart, viaPoint],
[branchStart, { x: viaPoint.x, y: branchStart.y }, viaPoint],
[branchStart, { x: branchStart.x, y: viaPoint.y }, viaPoint],
...[0.4, -0.4].map((corridorOffset) => [
branchStart,
{ x: branchStart.x + corridorOffset, y: branchStart.y },
{ x: branchStart.x + corridorOffset, y: viaPoint.y },
viaPoint
])
];
for (const assignedLayerPath of assignedLayerPaths) {
const terminalApproaches = [
undefined,
...[0.4, 0.8].map((terminalDistance) => ({
x: plan.targetPoint.x + outward.x * terminalDistance,
y: plan.targetPoint.y + outward.y * terminalDistance
})),
...[0.4, -0.4].map((sideDistance) => ({
x: plan.targetPoint.x + outward.x * 0.4 + perpendicular.x * sideDistance,
y: plan.targetPoint.y + outward.y * 0.4 + perpendicular.y * sideDistance
}))
];
for (const terminalApproach of terminalApproaches) {
if (candidateIndex >= maximumCandidateCount) {
return {
blockingConnectionNames: bestBlockingConnectionNames
};
}
const trace = createBranchTrace({
plan,
branchStart,
viaPoint,
assignedLayerPath,
terminalApproach,
traceWidth,
viaDiameter,
viaHoleDiameter,
candidateIndex: candidateIndex++,
chamfer: 0
});
if (traceHasViaAtEndpoint({
trace,
endpointSrjs: [inputSrj, fanoutSrj]
})) {
continue;
}
const candidateSrj = {
...fanoutSrj,
traces: [...fanoutSrj.traces ?? [], ...acceptedTraces, trace]
};
const drc = validateRoutedCopperDrc({
inputSrj,
routedSrj: candidateSrj,
clearance,
allowBlindAndBuriedVias
});
if (!drc.valid) {
if (drc.issues.length < bestIssueCount) {
bestIssueCount = drc.issues.length;
bestBlockingConnectionNames = [
...new Set(drc.issues.flatMap((issue) => [issue.connectionName, issue.otherConnectionName].flatMap((connectionName) => connectionName && connectionName !== plan.connectionName ? [connectionName] : [])))
];
}
continue;
}
const connectivity = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: candidateSrj
});
if (connectionIsComplete(connectivity, plan.connectionName)) {
return { trace, blockingConnectionNames: [] };
}
}
}
}
}
return {
blockingConnectionNames: bestBlockingConnectionNames
};
}
function runLocalCompletionPass(params) {
const {
inputSrj,
fanoutSrj,
plans,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
} = params;
const traces = [];
const failedConnectionNames = [];
const blockingConnectionNames = new Set;
for (const plan of plans) {
const result = findLocalBranch({
inputSrj,
fanoutSrj,
plan,
acceptedTraces: traces,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
});
if (result.trace) {
traces.push(result.trace);
} else {
failedConnectionNames.push(plan.connectionName);
for (const connectionName of result.blockingConnectionNames) {
blockingConnectionNames.add(connectionName);
}
}
}
const simpleRouteJson = {
...fanoutSrj,
traces: [...fanoutSrj.traces ?? [], ...traces]
};
return {
traces,
failedConnectionNames,
blockingConnectionNames: [...blockingConnectionNames],
connectivity: validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: simpleRouteJson
}),
drc: validateRoutedCopperDrc({
inputSrj,
routedSrj: simpleRouteJson,
clearance,
allowBlindAndBuriedVias
})
};
}
function traceLength(trace) {
let length = 0;
let previousWire;
for (const point of trace.route) {
if (point.route_type !== "wire") {
previousWire = undefined;
continue;
}
if (previousWire?.layer === point.layer)
length += distance(previousWire, point);
previousWire = point;
}
return length;
}
function trimCompletedFanoutTails(params) {
const { fanoutSrj, completionTraces } = params;
const branchStartByConnectionName = new Map(completionTraces.flatMap((trace) => {
const firstWire = trace.route.find((point) => point.route_type === "wire");
return firstWire ? [
[
trace.connection_name,
{
x: firstWire.x,
y: firstWire.y,
layer: firstWire.layer
}
]
] : [];
}));
const traces = (fanoutSrj.traces ?? []).map((trace) => {
if (!trace.pcb_trace_id.startsWith("fanout:"))
return trace;
const branchStart = branchStartByConnectionName.get(trace.connection_name);
if (!branchStart)
return trace;
let previousWireIndex;
for (let index = 0;index < trace.route.length; index++) {
const point = trace.route[index];
if (point?.route_type !== "wire") {
previousWireIndex = undefined;
continue;
}
if (previousWireIndex !== undefined) {
const previous = trace.route[previousWireIndex];
if (previous?.route_type === "wire" && previous.layer === branchStart.layer && point.layer === branchStart.layer && distancePointToSegment(branchStart, previous, point) <= 0.000001) {
const route = trace.route.slice(0, previousWireIndex + 1);
if (distance(previous, branchStart) > 0.000001) {
route.push({
route_type: "wire",
x: branchStart.x,
y: branchStart.y,
width: point.width,
layer: branchStart.layer
});
}
return { ...trace, route };
}
}
previousWireIndex = index;
}
return trace;
});
return { ...fanoutSrj, traces };
}
function acceptDownstreamTraces(params) {
const {
inputSrj,
fanoutSrj,
localTraces,
candidates,
clearance,
allowBlindAndBuriedVias
} = params;
const baselineTraces = [...fanoutSrj.traces ?? [], ...localTraces];
const baselineSrj = { ...fanoutSrj, traces: baselineTraces };
const baselineConnectivity = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: baselineSrj
});
const physicalCandidates = candidates.filter((trace) => trace.route.length > 1 && trace.route.every((routePoint) => routePoint.route_type === "wire" || routePoint.route_type === "via") && !traceHasViaAtEndpoint({
trace,
endpointSrjs: [inputSrj, fanoutSrj]
}));
const usefulCandidates = physicalCandidates.filter((trace) => {
const report = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: { ...fanoutSrj, traces: [...baselineTraces, trace] }
});
return report.connectedConnectionCount > baselineConnectivity.connectedConnectionCount && connectionIsComplete(report, trace.connection_name);
});
const combinedSrj = {
...fanoutSrj,
traces: [...baselineTraces, ...usefulCandidates]
};
if (validateRoutedCopperDrc({
inputSrj,
routedSrj: combinedSrj,
clearance,
allowBlindAndBuriedVias
}).valid) {
return usefulCandidates;
}
const accepted = [];
for (const trace of usefulCandidates.toSorted((first, second) => traceLength(first) - traceLength(second))) {
const candidateSrj = {
...fanoutSrj,
traces: [...baselineTraces, ...accepted, trace]
};
const drc = validateRoutedCopperDrc({
inputSrj,
routedSrj: candidateSrj,
clearance,
allowBlindAndBuriedVias
});
if (!drc.valid)
continue;
const before = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: {
...fanoutSrj,
traces: [...baselineTraces, ...accepted]
}
});
const after = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: candidateSrj
});
if (after.connectedConnectionCount > before.connectedConnectionCount && connectionIsComplete(after, trace.connection_name)) {
accepted.push(trace);
}
}
return accepted;
}
function getPointsBackAlongTrace(params) {
const { trace, endpoint, layer, distances } = params;
const endpointIndex = trace.route.findLastIndex((routePoint) => routePoint.route_type === "wire" && routePoint.layer === layer && distance(routePoint, endpoint) <= 0.000001);
if (endpointIndex < 0)
return [];
return distances.flatMap((requestedDistance) => {
let remainingDistance = requestedDistance;
let current = endpoint;
for (let index = endpointIndex - 1;index >= 0; index--) {
const previous = trace.route[index];
if (previous?.route_type !== "wire" || previous.layer !== layer)
break;
const segmentLength = distance(current, previous);
if (segmentLength <= EPSILON4)
continue;
if (remainingDistance < segmentLength - 0.000001) {
return [
{
x: current.x + (previous.x - current.x) * remainingDistance / segmentLength,
y: current.y + (previous.y - current.y) * remainingDistance / segmentLength
}
];
}
remainingDistance -= segmentLength;
current = previous;
}
return [];
});
}
function findDownstreamTerminalBranch(params) {
const {
inputSrj,
fanoutSrj,
plan,
acceptedTraces,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
} = params;
const branchStart = {
x: plan.exitPoint.x,
y: plan.exitPoint.y,
layer: plan.targetLayer
};
const target = plan.targetPoint;
const targetLayer = getPointLayer(target);
let candidateIndex = 0;
const tryCandidate = (candidate) => {
const trace = createBranchTrace({
plan,
branchStart: candidate.start,
viaPoint: candidate.viaPoint,
assignedLayerPath: [candidate.start, candidate.viaPoint],
terminalApproach: candidate.terminalApproach,
traceWidth,
viaDiameter,
viaHoleDiameter,
candidateIndex: 1e4 + candidateIndex++,
chamfer: Math.max(traceWidth, 0.1)
});
if (traceHasViaAtEndpoint({
trace,
endpointSrjs: [inputSrj, fanoutSrj]
})) {
return;
}
const candidateSrj = {
...fanoutSrj,
traces: [...fanoutSrj.traces ?? [], ...acceptedTraces, trace]
};
if (!validateRoutedCopperDrc({
inputSrj,
routedSrj: candidateSrj,
clearance,
allowBlindAndBuriedVias
}).valid) {
return;
}
if (connectionIsComplete(validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: candidateSrj
}), plan.connectionName)) {
return trace;
}
return;
};
if (branchStart.layer !== targetLayer) {
const existingTraceTransitionPoints = getPointsBackAlongTrace({
trace: plan.trace,
endpoint: plan.exitPoint,
layer: plan.targetLayer,
distances: [0.4, 0.8, 1.2, 1.6]
});
for (const viaPoint of existingTraceTransitionPoints) {
const transitionStart = { ...viaPoint, layer: plan.targetLayer };
for (const terminalApproach of [
undefined,
{ x: target.x, y: viaPoint.y },
{ x: viaPoint.x, y: target.y }
]) {
const trace = tryCandidate({
start: transitionStart,
viaPoint,
terminalApproach
});
if (trace)
return trace;
}
}
}
const routePaths = [
[target],
[{ x: target.x, y: branchStart.y }, target],
[{ x: branchStart.x, y: target.y }, target]
];
for (const routePath of routePaths) {
const firstTarget = routePath[0];
const firstSegmentLength = distance(branchStart, firstTarget);
const transitionDistances = branchStart.layer === targetLayer ? [0] : [0.4, 0.8, 1.2, 1.6];
for (const transitionDistance of transitionDistances) {
if (branchStart.layer !== targetLayer && (firstSegmentLength <= transitionDistance + 0.2 || transitionDistance <= 0.000001)) {
continue;
}
const viaPoint = branchStart.layer === targetLayer ? { x: branchStart.x, y: branchStart.y } : {
x: branchStart.x + (firstTarget.x - branchStart.x) * transitionDistance / firstSegmentLength,
y: branchStart.y + (firstTarget.y - branchStart.y) * transitionDistance / firstSegmentLength
};
const trace = tryCandidate({
start: branchStart,
viaPoint,
terminalApproach: routePath.length > 1 ? firstTarget : undefined
});
if (trace)
return trace;
}
}
return;
}
function completeOriginalEndpoints(params) {
const {
inputSrj,
fanoutSrj,
plans,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias = true,
effort = 1,
routeDownstreamConnections
} = params;
const errors = [];
const baselineDrc = validateRoutedCopperDrc({
inputSrj,
routedSrj: fanoutSrj,
clearance,
allowBlindAndBuriedVias
});
const baselineConnectivity = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: fanoutSrj
});
const localPlans = plans.filter((plan) => {
const sourceLayer = getPointLayer(plan.sourcePoint);
const targetLayer = getPointLayer(plan.targetPoint);
return sourceLayer !== targetLayer && distance(plan.sourcePoint, plan.targetPoint) <= 0.25 && !connectionIsComplete(baselineConnectivity, plan.connectionName);
});
let bestLocalAttempt = {
traces: [],
failedConnectionNames: localPlans.map((plan) => plan.connectionName),
blockingConnectionNames: [],
connectivity: baselineConnectivity,
drc: baselineDrc
};
let searchPassCount = 0;
if (!baselineDrc.valid) {
errors.push("Fanout prefix failed emitted-copper DRC; skipped completion");
} else {
let priorityConnectionNames = [];
const originalOrder = new Map(localPlans.map((plan, index) => [plan.connectionName, index]));
const maximumLocalPasses = inputSrj.connections.length > 32 ? 1 : 3;
for (let passIndex = 0;passIndex < maximumLocalPasses; passIndex++) {
const priority = new Map(priorityConnectionNames.map((connectionName, index) => [
connectionName,
index
]));
const orderedPlans = localPlans.toSorted((first, second) => (priority.get(first.connectionName) ?? Number.MAX_SAFE_INTEGER) - (priority.get(second.connectionName) ?? Number.MAX_SAFE_INTEGER) || (originalOrder.get(first.connectionName) ?? 0) - (originalOrder.get(second.connectionName) ?? 0));
const attempt = runLocalCompletionPass({
inputSrj,
fanoutSrj,
plans: orderedPlans,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
});
searchPassCount++;
if (attempt.drc.valid && (attempt.connectivity.connectedConnectionCount > bestLocalAttempt.connectivity.connectedConnectionCount || attempt.connectivity.connectedConnectionCount === bestLocalAttempt.connectivity.connectedConnectionCount && attempt.traces.length < bestLocalAttempt.traces.length)) {
bestLocalAttempt = attempt;
}
priorityConnectionNames = [
...new Set([
...attempt.failedConnectionNames,
...attempt.blockingConnectionNames,
...priorityConnectionNames
])
];
if (attempt.failedConnectionNames.length === 0)
break;
}
}
const localConnectionNames = new Set(localPlans.map((plan) => plan.connectionName));
const downstreamPlans = plans.filter((plan) => !localConnectionNames.has(plan.connectionName) && !connectionIsComplete(baselineConnectivity, plan.connectionName));
const directDownstreamTraces = [];
if (baselineDrc.valid) {
for (const plan of downstreamPlans) {
const terminalBranch = findDownstreamTerminalBranch({
inputSrj,
fanoutSrj,
plan,
acceptedTraces: [...bestLocalAttempt.traces, ...directDownstreamTraces],
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
});
if (terminalBranch)
directDownstreamTraces.push(terminalBranch);
}
}
const directSrj = {
...fanoutSrj,
traces: [
...fanoutSrj.traces ?? [],
...bestLocalAttempt.traces,
...directDownstreamTraces
]
};
const unresolvedConnectionNames = new Set(validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: directSrj
}).issues.map((issue) => issue.connectionName));
const downstreamConnections = fanoutSrj.connections.filter((connection) => !localConnectionNames.has(connection.name) && unresolvedConnectionNames.has(connection.name));
let downstreamTraces = [];
if (baselineDrc.valid && downstreamConnections.length > 0 && downstreamConnections.length <= 12 && routeDownstreamConnections) {
const downstreamConnectionNames = new Set(downstreamConnections.map((connection) => connection.name));
const downstreamInput = {
...fanoutSrj,
connections: downstreamConnections,
buses: fanoutSrj.buses?.map((bus) => ({
...bus,
connectionNames: bus.connectionNames.filter((connectionName) => downstreamConnectionNames.has(connectionName))
})).filter((bus) => bus.connectionNames.length > 0),
obstacles: fanoutSrj.obstacles,
traces: directSrj.traces
};
try {
const candidates = routeDownstreamConnections(downstreamInput, { effort });
downstreamTraces = acceptDownstreamTraces({
inputSrj,
fanoutSrj,
localTraces: [...bestLocalAttempt.traces, ...directDownstreamTraces],
candidates: candidates.filter((trace) => downstreamConnectionNames.has(trace.connection_name)),
clearance,
allowBlindAndBuriedVias
});
} catch (error) {
errors.push(`Downstream router failed: ${error instanceof Error ? error.message : String(error)}`);
}
} else if (!baselineDrc.valid && downstreamConnections.length > 0) {
errors.push("Skipped downstream router because the endpoint-completion baseline failed emitted-copper DRC");
} else if (downstreamConnections.length > 12) {
errors.push(`Skipped downstream router for ${downstreamConnections.length} unresolved connections (bounded at 12)`);
} else if (downstreamConnections.length > 0 && !routeDownstreamConnections) {
errors.push(`Skipped downstream router for ${downstreamConnections.length} unresolved connections because no routeDownstreamConnections callback was provided`);
}
const traces = [
...bestLocalAttempt.traces,
...directDownstreamTraces,
...downstreamTraces
];
for (const plan of downstreamPlans) {
const currentSrj = {
...fanoutSrj,
traces: [...fanoutSrj.traces ?? [], ...traces]
};
if (connectionIsComplete(validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: currentSrj
}), plan.connectionName)) {
continue;
}
const terminalBranch = findDownstreamTerminalBranch({
inputSrj,
fanoutSrj,
plan,
acceptedTraces: traces,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias
});
if (terminalBranch)
traces.push(terminalBranch);
}
const untrimmedSimpleRouteJson = {
...fanoutSrj,
traces: [...fanoutSrj.traces ?? [], ...traces]
};
const simpleRouteJson = trimCompletedFanoutTails({
fanoutSrj: untrimmedSimpleRouteJson,
completionTraces: traces
});
const connectivity = validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: simpleRouteJson
});
const drc = validateRoutedCopperDrc({
inputSrj,
routedSrj: simpleRouteJson,
clearance,
allowBlindAndBuriedVias
});
if (!drc.valid) {
errors.push("Final endpoint-completion output failed emitted-copper DRC");
return {
simpleRouteJson: fanoutSrj,
traces: [],
report: {
attemptedLocalConnectionCount: localPlans.length,
attemptedDownstreamConnectionCount: downstreamPlans.length,
completionTraceCount: 0,
searchPassCount,
errors,
connectivity: validateOriginalEndpointConnectivity({
inputSrj,
routedSrj: fanoutSrj
}),
drc: baselineDrc
}
};
}
return {
simpleRouteJson,
traces,
report: {
attemptedLocalConnectionCount: localPlans.length,
attemptedDownstreamConnectionCount: downstreamPlans.length,
completionTraceCount: traces.length,
searchPassCount,
errors,
connectivity,
drc
}
};
}
// node_modules/@tscircuit/fanout-solver/lib/fanout-exit-position.ts
var FANOUT_EXIT_POSITION_CONFIGS = {
topside_left: {
direction: "left",
preferredExit: "top-left",
exitEdge: "top"
},
topside_center: {
direction: "up",
preferredExit: "top",
exitEdge: "top"
},
topside_right: {
direction: "right",
preferredExit: "top-right",
exitEdge: "top"
},
rightside_top: {
direction: "up",
preferredExit: "top-right",
exitEdge: "right"
},
rightside_center: {
direction: "right",
preferredExit: "right",
exitEdge: "right"
},
rightside_bottom: {
direction: "down",
preferredExit: "bottom-right",
exitEdge: "right"
},
bottomside_right: {
direction: "right",
preferredExit: "bottom-right",
exitEdge: "bottom"
},
bottomside_center: {
direction: "down",
preferredExit: "bottom",
exitEdge: "bottom"
},
bottomside_left: {
direction: "left",
preferredExit: "bottom-left",
exitEdge: "bottom"
},
leftside_bottom: {
direction: "down",
preferredExit: "bottom-left",
exitEdge: "left"
},
leftside_center: {
direction: "left",
preferredExit: "left",
exitEdge: "left"
},
leftside_top: {
direction: "up",
preferredExit: "top-left",
exitEdge: "left"
},
center: {}
};
function getFanoutExitPositionConfig(exitPosition) {
const config = FANOUT_EXIT_POSITION_CONFIGS[exitPosition];
if (!config) {
throw new Error(`Invalid fanout exit position "${exitPosition}"`);
}
return config;
}
// node_modules/graphics-debug/dist/chunk-ZJJUR6DP.js
var import_svgson = __toESM(require_svgson_umd(), 1);
// node_modules/iobuffer/lib/text.js
var encoder = new TextEncoder;
// node_modules/iobuffer/lib/iobuffer.js
var defaultByteLength = 1024 * 8;
var hostBigEndian = (() => {
const array = new Uint8Array(4);
const view = new Uint32Array(array.buffer);
return !((view[0] = 1) & array[0]);
})();
var typedArrays = {
int8: globalThis.Int8Array,
uint8: globalThis.Uint8Array,
int16: globalThis.Int16Array,
uint16: globalThis.Uint16Array,
int32: globalThis.Int32Array,
uint32: globalThis.Uint32Array,
uint64: globalThis.BigUint64Array,
int64: globalThis.BigInt64Array,
float32: globalThis.Float32Array,
float64: globalThis.Float64Array
};
// node_modules/fast-png/lib/helpers/crc.js
var crcTable = [];
for (let n = 0;n < 256; n++) {
let c = n;
for (let k = 0;k < 8; k++) {
if (c & 1) {
c = 3988292384 ^ c >>> 1;
} else {
c = c >>> 1;
}
}
crcTable[n] = c;
}
// node_modules/fast-png/lib/helpers/decode_interlace_adam7.js
var uint16 = new Uint16Array([255]);
var uint8 = new Uint8Array(uint16.buffer);
var osIsLittleEndian = uint8[0] === 255;
// node_modules/fast-png/lib/helpers/decode_interlace_null.js
var uint162 = new Uint16Array([255]);
var uint82 = new Uint8Array(uint162.buffer);
var osIsLittleEndian2 = uint82[0] === 255;
var empty = new Uint8Array(0);
// node_modules/fast-png/lib/helpers/signature.js
var pngSignature = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
// node_modules/fast-png/lib/helpers/text.js
var latin1Decoder = new TextDecoder("latin1");
// node_modules/@tscircuit/alphabet/dist/index.js
var svgAlphabet = {
"0": "M0.301025 0.257813 L0.206593 0.283385 L0.122042 0.361307 L0.075923 0.474441 L0.064942 0.642221 L0.091296 0.78289 L0.159376 0.899463 L0.257104 0.961214 L0.344948 0.961214 L0.442678 0.899463 L0.510757 0.78289 L0.537111 0.642221 L0.526131 0.474441 L0.480012 0.361307 L0.39546 0.283385 L0.301025 0.257813",
"1": "M0.220426 0.40845 L0.381626 0.270996 L0.381626 0.948966",
"2": "M0.110409 0.39212 L0.125621 0.342293 L0.170571 0.295823 L0.244237 0.26431 L0.291518 0.257813 L0.373319 0.263799 L0.434683 0.290705 L0.468239 0.331966 L0.480292 0.379913 L0.479492 0.411806 L0.465088 0.465192 L0.402571 0.568988 L0.245134 0.766438 L0.07959 0.948045 L0.522462 0.948045",
"3": "M0.081606 0.345073 L0.119154 0.315633 L0.178996 0.284091 L0.255265 0.257813 L0.349133 0.263062 L0.43127 0.289348 L0.48994 0.336662 L0.525141 0.394491 L0.513407 0.457577 L0.466472 0.504892 L0.384334 0.541692 L0.290464 0.56272 L0.20833 0.573235 L0.290464 0.583749 L0.384334 0.610035 L0.466472 0.652092 L0.519274 0.709922 L0.531007 0.773007 L0.50754 0.841351 L0.454738 0.899179 L0.3726 0.946494 L0.310412 0.961214 L0.209503 0.959111 L0.127367 0.946494 L0.071046 0.929671",
"4": "M0.418494 0.270996 L0.048828 0.724199 L0.553224 0.724199 M0.414181 0.578167 L0.414181 0.94897",
"5": "M0.074951 0.270996 L0.074951 0.614701 L0.267655 0.5695 L0.405247 0.586502 L0.460002 0.617189 L0.498028 0.660731 L0.527101 0.779401 L0.513352 0.841049 L0.477274 0.889568 L0.406378 0.93463 L0.324445 0.958337 L0.247399 0.962139 L0.182516 0.949767 L0.130068 0.925508 L0.074951 0.875607 M0.074951 0.270996 L0.527101 0.270996",
"6": "M0.428549 0.257813 L0.341433 0.285053 L0.26073 0.312099 L0.192188 0.360182 L0.138902 0.410969 L0.098219 0.500422 L0.070249 0.584267 L0.067264 0.670716 L0.065164 0.756863 L0.087163 0.820973 L0.108168 0.861042 L0.157916 0.913131 L0.210981 0.948192 L0.29279 0.961214 L0.384548 0.956206 L0.456409 0.924151 L0.507262 0.881077 L0.5349 0.821975 L0.537111 0.771889 L0.521634 0.718798 L0.498418 0.67372 L0.463042 0.636657 L0.414398 0.613617 L0.34696 0.598591 L0.28284 0.595587 L0.215403 0.605604 L0.145756 0.641665 L0.109274 0.678729 L0.064942 0.724006",
"7": "M0.071534 0.270996 L0.530519 0.270996 L0.495935 0.291702 L0.477752 0.305407 L0.435186 0.346112 L0.412067 0.374136 L0.388565 0.40796 L0.365315 0.448098 L0.342943 0.495062 L0.322081 0.549366 L0.303364 0.611524 L0.287416 0.68205 L0.270077 0.80465 L0.26253 0.94897",
"8": "M0.286761 0.257813 L0.210684 0.27162 L0.1414 0.305622 L0.092494 0.382125 L0.111513 0.450128 L0.16857 0.502193 L0.228345 0.539382 L0.377781 0.594635 L0.470161 0.645637 L0.52586 0.706203 L0.538087 0.792269 L0.506841 0.87621 L0.438915 0.927212 L0.339743 0.961214 L0.263666 0.959089 L0.174004 0.929337 L0.102003 0.881523 L0.063965 0.811395 L0.066682 0.741266 L0.102003 0.671139 L0.180797 0.608448 L0.259591 0.580822 L0.358762 0.548945 L0.456576 0.49688 L0.486463 0.446941 L0.487822 0.362999 L0.453859 0.302434 L0.384574 0.265245 L0.286761 0.257813",
"9": "M0.511026 0.437812 L0.491026 0.347812 L0.451026 0.297812 L0.391026 0.267812 L0.311026 0.257813 L0.231026 0.267812 L0.161026 0.297812 L0.111026 0.347812 L0.091026 0.417812 L0.111026 0.487812 L0.161026 0.537812 L0.231026 0.567812 L0.311026 0.577812 L0.391026 0.557812 L0.451026 0.517812 L0.491026 0.477812 L0.511026 0.437812 M0.510026 0.438812 L0.434026 0.636812 L0.381026 0.777812 L0.337026 0.879812 L0.301026 0.957812",
"!": "M0.301026 0.270996 L0.301026 0.749566 M0.251466 0.94897 L0.350587 0.94897",
'"': "M0.16504 0.270996 L0.16504 0.523022 M0.437013 0.270996 L0.437013 0.523022",
"#": "M0.20958 0.282227 L0.146515 0.949754 M0.461844 0.282227 L0.398779 0.949754 M0.032999 0.526969 L0.600587 0.452796 M0.001465 0.82366 L0.569055 0.749486",
$: "M0.307121 0.240234 L0.307121 1.083502 M0.490031 0.415915 L0.404676 0.328075 L0.23396 0.310507 L0.136407 0.398347 L0.099828 0.521324 L0.124213 0.591596 L0.197379 0.644299 L0.429061 0.697003 L0.502224 0.749708 L0.526613 0.837547 L0.477836 0.960526 L0.355897 1.013228 L0.197379 0.99566 L0.112021 0.942957 L0.07544 0.837547",
"'": "M0.301026 0.270996 L0.301026 0.523022",
"(": "M0.413087 0.241211 L0.372337 0.28347 L0.331589 0.334184 L0.290838 0.393349 L0.25009 0.460965 L0.219528 0.528584 L0.199153 0.5962 L0.188966 0.655364 L0.199153 0.714529 L0.219528 0.782146 L0.25009 0.849764 L0.290838 0.91738 L0.331589 0.976544 L0.372337 1.027258 L0.413087 1.069491",
")": "M0.188966 0.241211 L0.229716 0.28347 L0.270466 0.334184 L0.311213 0.393349 L0.351964 0.460965 L0.382526 0.528584 L0.4029 0.5962 L0.413087 0.655364 L0.4029 0.714529 L0.382526 0.782146 L0.351964 0.849764 L0.311213 0.91738 L0.270466 0.976544 L0.229716 1.027258 L0.188966 1.069491",
"*": "M0.301026 0.458702 L0.301026 0.681942 M0.301026 0.458702 L0.520997 0.536834 M0.301026 0.458702 L0.439954 0.257813 M0.301026 0.458702 L0.162098 0.257813 M0.301026 0.458702 L0.081055 0.536834",
"+": "M0.042969 0.66845 L0.559083 0.66845 M0.301023 0.428223 L0.301023 0.908661",
",": "M0.38672 0.852051 L0.215333 1.030661 L0.301025 1.119971",
"-": "M0.173829 0.686035 L0.428224 0.686035",
".": "M0.362549 0.851074 L0.239503 0.851074",
"/": "M0.062501 1.035249 L0.539551 0.270996",
"<": "M0.559083 0.441895 L0.042969 0.66939 L0.042969 0.66939 L0.559083 0.896902",
"=": "M0.042969 0.545898 L0.559083 0.545898 M0.042969 0.80837 L0.559083 0.80837",
">": "M0.042969 0.441895 L0.559083 0.66939 L0.559083 0.66939 L0.042969 0.896902",
A: "M0.018067 0.94897 L0.290179 0.270996 L0.583986 0.94897 M0.155303 0.684521 L0.433548 0.684521",
B: "M0.063965 0.94897 L0.063965 0.278953 L0.209356 0.270996 L0.342486 0.294394 L0.432179 0.37292 L0.418224 0.48773 L0.312686 0.549166 L0.084728 0.575531 L0.306377 0.589434 L0.468877 0.647456 L0.538087 0.736658 L0.5324 0.85202 L0.501247 0.902983 L0.42355 0.944398 L0.063965 0.94897",
C: "M0.529054 0.419847 L0.498425 0.345196 L0.442238 0.292341 L0.33583 0.257813 L0.257199 0.268357 L0.187906 0.303748 L0.120243 0.385432 L0.072998 0.599844 L0.110073 0.815385 L0.165381 0.902183 L0.222883 0.942801 L0.334904 0.961214 L0.43253 0.937679 L0.504816 0.868999 L0.529054 0.772065",
D: "M0.064454 0.94897 L0.071838 0.285078 L0.171217 0.270996 L0.247348 0.272633 L0.37101 0.306078 L0.462461 0.378304 L0.514748 0.475555 L0.535591 0.576392 L0.537599 0.652313 L0.528065 0.717381 L0.508562 0.772419 L0.445948 0.855695 L0.362356 0.90871 L0.225203 0.945862 L0.064454 0.94897",
E: "M0.080078 0.270996 L0.080078 0.94897 M0.080078 0.270996 L0.521974 0.270996 M0.080078 0.609981 L0.389405 0.609981 M0.080078 0.94897 L0.521974 0.94897",
F: "M0.086426 0.270996 L0.086426 0.94897 M0.091304 0.275607 L0.515626 0.275607 M0.091304 0.607677 L0.447344 0.607677",
G: "M0.498034 0.284943 L0.374621 0.257813 L0.244219 0.268648 L0.160577 0.311997 L0.094556 0.398116 L0.056397 0.58404 L0.076752 0.785532 L0.120865 0.858197 L0.188947 0.916734 L0.271008 0.953591 L0.357069 0.961214 L0.43715 0.932047 L0.501262 0.858539 L0.545656 0.648611 L0.329595 0.648131",
H: "M0.066895 0.270996 L0.066895 0.94897 M0.066895 0.595658 L0.530009 0.595658 M0.535157 0.270996 L0.535157 0.94897",
I: "M0.301026 0.270996 L0.301026 0.94897",
J: "M0.506839 0.270996 L0.507814 0.782193 L0.498468 0.846168 L0.480733 0.880789 L0.455864 0.90832 L0.426095 0.929408 L0.360746 0.954812 L0.302508 0.962139 L0.223573 0.953638 L0.16538 0.930312 L0.137072 0.907715 L0.116428 0.880312 L0.102822 0.848819 L0.095633 0.813947 L0.094239 0.776413",
K: "M0.035401 0.270996 L0.035401 0.94897 M0.502258 0.316194 L0.035401 0.687823 M0.212484 0.582361 L0.566652 0.933903",
L: "M0.07544 0.270996 L0.07544 0.94897 L0.526613 0.94897",
M: "M0.042481 0.94897 L0.042481 0.270996 L0.301027 0.712492 L0.559571 0.270996 L0.559571 0.94897",
N: "M0.067871 0.94897 L0.067871 0.270996 L0.534181 0.94897 L0.525031 0.270996",
O: "M0.287293 0.961214 L0.19843 0.933567 L0.1172 0.849451 L0.057129 0.563945 L0.114348 0.350255 L0.18455 0.289392 L0.306101 0.257813 L0.42054 0.29572 L0.487543 0.365003 L0.544923 0.625294 L0.516366 0.789624 L0.465476 0.884596 L0.375084 0.949287 L0.287293 0.961214",
P: "M0.070557 0.94897 L0.070557 0.28379 L0.20704 0.270996 L0.329134 0.278993 L0.408525 0.300514 L0.444022 0.317908 L0.500637 0.368929 L0.525796 0.423728 L0.531496 0.50384 L0.508934 0.554205 L0.473724 0.582043 L0.417577 0.604365 L0.336375 0.620372 L0.226014 0.629268 L0.082368 0.630258",
Q: "M0.282435 1.050813 L0.195447 1.019646 L0.115933 0.924817 L0.057129 0.602943 L0.113139 0.362034 L0.18186 0.293418 L0.300844 0.257813 L0.412868 0.300553 L0.478457 0.37866 L0.534625 0.672103 L0.506671 0.857367 L0.456855 0.964437 L0.368371 1.037368 L0.282435 1.050813 M0.341225 0.815559 L0.544923 1.070653",
R: "M0.034912 0.944112 L0.034912 0.288556 L0.227127 0.270996 L0.362038 0.276867 L0.443423 0.294907 L0.477644 0.309614 L0.505809 0.328757 L0.526616 0.352817 L0.544963 0.418061 L0.543241 0.475921 L0.524463 0.517817 L0.49076 0.546518 L0.444265 0.564799 L0.355195 0.578739 L0.047653 0.589627 M0.303054 0.617306 L0.56714 0.94897",
S: "M0.483037 0.364333 L0.407613 0.267477 L0.22883 0.257813 L0.128623 0.327565 L0.087712 0.455539 L0.104656 0.518727 L0.168054 0.573307 L0.41928 0.617318 L0.503796 0.669342 L0.535157 0.759324 L0.493846 0.900055 L0.367668 0.961214 L0.205436 0.956661 L0.116711 0.907038 L0.066895 0.790334",
T: "M0.022949 0.270996 L0.579103 0.270996 M0.309284 0.270996 L0.309284 0.94897",
U: "M0.072022 0.270996 L0.07308 0.648875 L0.080372 0.75167 L0.099024 0.827813 L0.134786 0.885729 L0.181571 0.924023 L0.251464 0.95168 L0.339655 0.962139 L0.401553 0.951668 L0.45232 0.924086 L0.482701 0.891771 L0.495745 0.870529 L0.515699 0.81637 L0.525014 0.744626 L0.530031 0.272017",
V: "M0.027832 0.270996 L0.320083 0.94897 L0.57422 0.270996",
W: "M0 0.270996 L0.079799 0.940377 L0.285198 0.472884 L0.505255 0.94897 L0.602052 0.270996",
X: "M0.009034 0.270996 L0.586805 0.94897 M0.593019 0.270996 L0.015246 0.94897",
Y: "M0.018067 0.270996 L0.293132 0.574135 M0.583986 0.271959 L0.294954 0.57606 L0.305277 0.94897",
Z: "M0.053711 0.270996 L0.548341 0.270996 L0.053711 0.94897 L0.548341 0.94897",
"[": "M0.404542 0.240234 L0.197511 0.240234 L0.197511 1.069424 L0.404542 1.069424",
"\\": "M0.062501 0.270996 L0.539551 1.035249",
"]": "M0.19751 0.240234 L0.404542 0.240234 L0.404542 1.069424 L0.19751 1.069424",
"^": "M0.035156 0.523931 L0.301024 0.270996 L0.566896 0.523931",
_: "M0 1.160601 L0.602052 1.160601",
a: "M0.527101 0.439941 L0.527101 0.973965 M0.527101 0.553824 L0.451366 0.477535 L0.376759 0.439941 L0.263722 0.439941 L0.187989 0.477535 L0.112255 0.553824 L0.074951 0.66881 L0.074951 0.745099 L0.112255 0.85898 L0.187989 0.93527 L0.263722 0.973965 L0.376759 0.973965 L0.451366 0.93527 L0.527101 0.85898",
b: "M0.07666 0.240234 L0.07666 0.959985 M0.07666 0.582737 L0.151822 0.514237 L0.225865 0.480483 L0.338045 0.480483 L0.413211 0.514237 L0.488374 0.582737 L0.525392 0.685983 L0.525392 0.754485 L0.488374 0.856739 L0.413211 0.92524 L0.338045 0.959985 L0.225865 0.959985 L0.151822 0.92524 L0.07666 0.856739",
c: "M0.512452 0.553823 L0.441624 0.477535 L0.371854 0.439941 L0.26614 0.439941 L0.195312 0.477535 L0.124486 0.553823 L0.0896 0.668808 L0.0896 0.745099 L0.124486 0.858979 L0.195312 0.93527 L0.26614 0.973965 L0.371854 0.973965 L0.441624 0.93527 L0.512452 0.858979",
d: "M0.525392 0.240234 L0.525392 0.959985 M0.525392 0.582737 L0.450232 0.514237 L0.376189 0.480483 L0.264006 0.480483 L0.188844 0.514237 L0.11368 0.582737 L0.076661 0.685983 L0.076661 0.754485 L0.11368 0.856739 L0.188844 0.92524 L0.264006 0.959985 L0.376189 0.959985 L0.450232 0.92524 L0.525392 0.856739",
e: "M0.059571 0.668808 L0.542482 0.668808 M0.542482 0.592521 L0.502641 0.516231 L0.461594 0.477535 L0.381914 0.439941 L0.261186 0.439941 L0.180299 0.477535 L0.099412 0.553823 L0.059571 0.668808 L0.059571 0.745099 L0.099412 0.858979 L0.180299 0.93527 L0.261186 0.973965 L0.381914 0.973965 L0.461594 0.93527 L0.542482 0.858979",
f: "M0.512941 0.240234 L0.406984 0.240234 L0.301026 0.274346 L0.248047 0.374731 L0.248047 0.946817 M0.089112 0.476088 L0.459965 0.476088",
g: "M0.525392 0.439941 L0.525392 0.989395 L0.488373 1.091918 L0.45023 1.126757 L0.376189 1.160601 L0.264004 1.160601 L0.188844 1.126757 L0.113681 1.058075 L0.076661 0.954555 M0.525392 0.542467 L0.45023 0.473786 L0.376189 0.439941 L0.264004 0.439941 L0.188844 0.473786 L0.113681 0.542467 L0.076661 0.645988 L0.076661 0.714669 L0.113681 0.817193 L0.188844 0.885875 L0.264004 0.920713 L0.376189 0.920713 L0.45023 0.885875 L0.525392 0.817193",
h: "M0.092041 0.240234 L0.092041 0.946817 M0.092041 0.61058 L0.205937 0.509225 L0.282214 0.476086 L0.396109 0.476086 L0.47187 0.509225 L0.510011 0.61058 L0.510011 0.946817",
i: "M0.261026 0.240234 L0.341026 0.240234 M0.300978 0.476089 L0.300978 0.946817",
j: "M0.3853 0.240234 L0.447023 0.240234 M0.416159 0.465708 L0.416159 1.011689 L0.383887 1.108586 L0.252832 1.140264 L0.15503 1.108586",
k: "M0.065186 0.240234 L0.065186 0.946817 M0.493824 0.476086 L0.065186 0.812323 M0.236758 0.677829 L0.536867 0.946817",
l: "M0.301026 0.234863 L0.301026 0.946441",
m: "M0.050538 0.439941 L0.050538 0.960796 M0.050538 0.588758 L0.114405 0.476607 L0.157178 0.439941 L0.236864 0.439941 L0.279637 0.476607 L0.32241 0.588758 L0.32241 0.960796 M0.32241 0.588758 L0.386281 0.476607 L0.428467 0.439941 L0.50874 0.439941 L0.551515 0.476607 L0.551515 0.960796",
n: "M0.092041 0.439941 L0.092041 0.960796 M0.092041 0.588758 L0.205936 0.476607 L0.282216 0.439941 L0.396113 0.439941 L0.471871 0.476607 L0.510011 0.588758 L0.510011 0.960796",
o: "M0.247175 0.439941 L0.175178 0.477535 L0.103186 0.553823 L0.066895 0.668808 L0.066895 0.745099 L0.103186 0.858979 L0.175178 0.93527 L0.247175 0.973965 L0.354874 0.973965 L0.426872 0.93527 L0.498866 0.858979 L0.535157 0.745099 L0.535157 0.668808 L0.498866 0.553823 L0.426872 0.477535 L0.354874 0.439941 L0.247175 0.439941",
p: "M0.076904 0.439941 L0.076904 1.154244 M0.076904 0.541563 L0.151986 0.473487 L0.225944 0.439941 L0.338005 0.439941 L0.413087 0.473487 L0.488166 0.541563 L0.525148 0.644169 L0.525148 0.712246 L0.488166 0.813866 L0.413087 0.881942 L0.338005 0.916471 L0.225944 0.916471 L0.151986 0.881942 L0.076904 0.813866",
q: "M0.525148 0.441895 L0.525148 1.156195 M0.525148 0.5435 L0.450067 0.47544 L0.376106 0.441895 L0.264042 0.441895 L0.188966 0.47544 L0.113885 0.5435 L0.076905 0.646108 L0.076905 0.714187 L0.113885 0.815809 L0.188966 0.883886 L0.264042 0.918418 L0.376106 0.918418 L0.450067 0.883886 L0.525148 0.815809",
r: "M0.107422 0.439941 L0.107422 0.960796 M0.107422 0.663164 L0.155824 0.551014 L0.252626 0.476607 L0.349428 0.439941 L0.49463 0.439941",
s: "M0.475556 0.477535 L0.400757 0.439941 L0.276092 0.439941 L0.176361 0.477535 L0.101563 0.553823 L0.101563 0.630112 L0.176361 0.668808 L0.301024 0.668808 L0.425688 0.745099 L0.50049 0.821387 L0.50049 0.897677 L0.425688 0.93527 L0.325956 0.973965 L0.201295 0.973965 L0.126496 0.93527 L0.101563 0.858979",
t: "M0.246032 0.297852 L0.246032 0.826549 L0.301023 0.919323 L0.411011 0.950848 L0.520997 0.950848 M0.081055 0.515799 L0.466004 0.515799",
u: "M0.092041 0.454102 L0.092041 0.826134 L0.13018 0.937211 L0.205936 0.974954 L0.319832 0.974954 L0.396113 0.937211 L0.510011 0.826134 M0.510011 0.454102 L0.510011 0.974954",
v: "M0.048828 0.453125 L0.301026 0.961719 M0.553224 0.453125 L0.301026 0.961719",
w: "M0 0.453125 L0.150512 0.961719 M0.301024 0.453125 L0.150512 0.961719 M0.301024 0.453125 L0.451539 0.961719 M0.602052 0.453125 L0.451539 0.961719",
x: "M0.03711 0.453125 L0.564943 0.961719 M0.564943 0.453125 L0.03711 0.961719",
y: "M0.084618 0.453125 L0.320232 0.921476 M0.55713 0.453125 L0.320232 0.921476 L0.242119 1.055289 L0.163369 1.122196 L0.084618 1.155166 L0.044922 1.155166",
z: "M0.505373 0.452148 L0.09668 0.961651 M0.09668 0.452148 L0.505373 0.452148 M0.09668 0.961651 L0.505373 0.961651"
};
var lineAlphabet = {};
for (const letter in svgAlphabet) {
lineAlphabet[letter] = [];
const segs = svgAlphabet[letter].split("M").slice(1).map((seg) => seg.split("L").map((pr) => pr.trim().split(" ").map(parseFloat)));
for (const seg of segs) {
for (let i = 0;i < seg.length - 1; i++) {
lineAlphabet[letter].push({
x1: seg[i][0],
y1: 1 - seg[i][1],
x2: seg[i + 1][0],
y2: 1 - seg[i + 1][1]
});
}
}
}
// node_modules/graphics-debug/dist/chunk-K37Y2KHE.js
var mergeGraphics = (graphics1, graphics2) => {
return {
...graphics1,
rects: [...graphics1.rects ?? [], ...graphics2.rects ?? []],
points: [...graphics1.points ?? [], ...graphics2.points ?? []],
lines: [...graphics1.lines ?? [], ...graphics2.lines ?? []],
infiniteLines: [
...graphics1.infiniteLines ?? [],
...graphics2.infiniteLines ?? []
],
polygons: [...graphics1.polygons ?? [], ...graphics2.polygons ?? []],
circles: [...graphics1.circles ?? [], ...graphics2.circles ?? []],
arrows: [...graphics1.arrows ?? [], ...graphics2.arrows ?? []],
texts: [...graphics1.texts ?? [], ...graphics2.texts ?? []]
};
};
// node_modules/@tscircuit/solver-utils/dist/index.js
var BaseSolver = class {
MAX_ITERATIONS = 1e5;
solved = false;
failed = false;
iterations = 0;
progress = 0;
error = null;
activeSubSolver;
failedSubSolvers;
timeToSolve;
stats = {};
_setupDone = false;
getSolverName() {
return this.constructor.name;
}
setup() {
if (this._setupDone)
return;
this._setup();
this._setupDone = true;
}
_setup() {}
step() {
if (!this._setupDone) {
this.setup();
}
if (this.solved)
return;
if (this.failed)
return;
this.iterations++;
try {
this._step();
} catch (e) {
this.error = `${this.getSolverName()} error: ${e}`;
this.failed = true;
throw e;
}
if (!this.solved && this.iterations >= this.MAX_ITERATIONS) {
this.tryFinalAcceptance();
}
if (!this.solved && this.iterations >= this.MAX_ITERATIONS) {
this.error = `${this.getSolverName()} ran out of iterations`;
this.failed = true;
}
if ("computeProgress" in this) {
this.progress = this.computeProgress();
}
}
_step() {}
getConstructorParams() {
throw new Error("getConstructorParams not implemented");
}
getOutput() {
return null;
}
solve() {
const startTime = Date.now();
while (!this.solved && !this.failed) {
this.step();
}
const endTime = Date.now();
this.timeToSolve = endTime - startTime;
}
visualize() {
return {
lines: [],
points: [],
rects: [],
circles: []
};
}
tryFinalAcceptance() {}
preview() {
return {
lines: [],
points: [],
rects: [],
circles: []
};
}
};
// node_modules/@tscircuit/fanout-solver/lib/select-compatible-candidates.ts
function selectCompatibleCandidates(params) {
const candidates = params.candidateSets.flat();
const count = candidates.length;
const compatibility = new Uint8Array(count * count);
const emptyDomainIndices = new Set;
const revisionParent = new Map;
let nextIndex = 0;
const initialDomains = params.candidateSets.map((set) => set.map(() => nextIndex++));
let searchStates = 0;
const compatible = (first, second) => {
const index = Math.min(first, second) * count + Math.max(first, second);
const cached = compatibility[index];
if (cached)
return cached === 1;
const result2 = params.areCompatible(candidates[first], candidates[second]);
compatibility[index] = result2 ? 1 : 2;
return result2;
};
const neighbors = initialDomains.map(() => []);
for (let first = 0;first < initialDomains.length; first++) {
for (let second = first + 1;second < initialDomains.length; second++) {
if (initialDomains[first].some((a) => initialDomains[second].some((b) => !compatible(a, b)))) {
neighbors[first].push(second);
neighbors[second].push(first);
}
}
}
const propagate = (domains, indices, changed) => {
const queue = [];
let allDomainsRemainNonempty = true;
for (const first of indices) {
if (domains[first].length === 0) {
emptyDomainIndices.add(first);
allDomainsRemainNonempty = false;
continue;
}
for (const second of neighbors[first]) {
if (indices.includes(second) && (changed === undefined || second === changed))
queue.push([first, second]);
}
}
for (let cursor = 0;cursor < queue.length; cursor++) {
const [first, second] = queue[cursor];
if (domains[first].length === 0 || domains[second].length === 0)
continue;
const retained = domains[first].filter((candidate) => domains[second].some((other) => compatible(candidate, other)));
if (retained.length === domains[first].length)
continue;
revisionParent.set(first, second);
domains[first] = retained;
if (retained.length === 0) {
emptyDomainIndices.add(first);
allDomainsRemainNonempty = false;
continue;
}
for (const neighbor of neighbors[first]) {
if (neighbor !== second && indices.includes(neighbor))
queue.push([neighbor, first]);
}
}
return allDomainsRemainNonempty;
};
const allIndices = initialDomains.map((_, index) => index);
const getConflictDomainIndices = (seeds) => {
const pending = [...seeds];
const seen = new Set(pending);
for (let cursor = 0;cursor < pending.length; cursor++) {
const parent = revisionParent.get(pending[cursor]);
if (parent !== undefined && !seen.has(parent)) {
seen.add(parent);
pending.push(parent);
}
}
return [...seen];
};
const rootDomains = initialDomains.map((domain) => [...domain]);
if (!propagate(rootDomains, allIndices)) {
return {
selection: null,
searchStates,
emptyDomainIndices: [...emptyDomainIndices],
conflictDomainIndices: getConflictDomainIndices([...emptyDomainIndices])
};
}
const findLocallyCompatibleSelection = () => {
const restartCount = 12;
const stepsPerRestart = Math.max(1, Math.min(5000, Math.floor(params.maximumSearchStates / restartCount)));
for (let restart = 0;restart < restartCount; restart++) {
const selection = rootDomains.map((domain, index) => domain[(restart * 17 + index * 7) % domain.length]);
for (let step = 0;step < stepsPerRestart; step++) {
const conflictCounts = selection.map(() => 0);
for (let first = 0;first < selection.length; first++) {
for (const second of neighbors[first]) {
if (second <= first || compatible(selection[first], selection[second]))
continue;
conflictCounts[first]++;
conflictCounts[second]++;
}
}
const maximumConflictCount = Math.max(...conflictCounts);
if (maximumConflictCount === 0)
return selection;
const conflicted = conflictCounts.map((conflicts, index) => ({ conflicts, index })).filter(({ conflicts }) => conflicts === maximumConflictCount);
const selected = conflicted[(step + restart) % conflicted.length].index;
const scored = rootDomains[selected].map((candidate, index) => ({
candidate,
index,
conflicts: neighbors[selected].filter((neighbor) => !compatible(candidate, selection[neighbor])).length
})).toSorted((first, second) => first.conflicts - second.conflicts || (first.index - step - restart) % rootDomains[selected].length - (second.index - step - restart) % rootDomains[selected].length);
selection[selected] = scored[0].candidate;
}
}
return null;
};
const locallyCompatibleSelection = findLocallyCompatibleSelection();
if (locallyCompatibleSelection) {
return {
selection: locallyCompatibleSelection.map((index) => candidates[index]),
searchStates,
emptyDomainIndices: [],
conflictDomainIndices: []
};
}
const search = (domains, indices, changed) => {
if (!propagate(domains, indices, changed))
return null;
const pending = new Set(indices.filter((index) => domains[index].length > 1));
const components = [];
while (pending.size > 0) {
const component = [pending.values().next().value];
pending.delete(component[0]);
for (let cursor = 0;cursor < component.length; cursor++) {
for (const neighbor of neighbors[component[cursor]]) {
if (pending.delete(neighbor))
component.push(neighbor);
}
}
components.push(component);
}
if (components.length > 1) {
let combined = domains;
for (const component of components) {
const solved = search(combined.slice(), component);
if (!solved)
return null;
combined = solved;
}
return combined;
}
let selected = -1;
for (const index of indices) {
if (domains[index].length > 1 && (selected < 0 || domains[index].length < domains[selected].length))
selected = index;
}
if (selected < 0)
return domains;
const orderedCandidates = domains[selected].map((candidate) => {
const supportCounts = neighbors[selected].filter((neighbor) => indices.includes(neighbor) && domains[neighbor].length > 1).map((neighbor) => domains[neighbor].filter((other) => compatible(candidate, other)).length);
return {
candidate,
minimumSupport: Math.min(...supportCounts),
totalSupport: supportCounts.reduce((sum, count2) => sum + count2, 0)
};
}).toSorted((first, second) => second.minimumSupport - first.minimumSupport || second.totalSupport - first.totalSupport);
for (const { candidate } of orderedCandidates) {
if (searchStates >= params.maximumSearchStates)
return null;
searchStates++;
const nextDomains = domains.slice();
nextDomains[selected] = [candidate];
const result2 = search(nextDomains, indices, selected);
if (result2)
return result2;
}
return null;
};
const result = search(rootDomains, allIndices);
const diagnosticIndices = emptyDomainIndices.size > 0 || result ? [...emptyDomainIndices] : [
allIndices.toSorted((first, second) => neighbors[second].length - neighbors[first].length || rootDomains[first].length - rootDomains[second].length)[0]
];
return {
selection: result?.map((domain) => candidates[domain[0]]) ?? null,
searchStates,
emptyDomainIndices: diagnosticIndices,
conflictDomainIndices: result ? [] : getConflictDomainIndices(diagnosticIndices)
};
}
// node_modules/@tscircuit/fanout-solver/lib/refine-adaptive-plane-reservations.ts
var EPSILON5 = 0.000000001;
function refineAdaptivePlaneReservationCore(params) {
const core = params.candidateBusIds.filter((busId) => !params.activeBusIds.has(busId));
if (core.length < 2)
return core;
const terminalIndex = core.length - 1;
const terminal = params.planeBuses.find((bus) => bus.busId === core[terminalIndex]);
const source = terminal?.connections[0]?.sourcePoint;
if (!terminal || !source || terminal.connections.length !== 1 || terminal.termination.type !== "plane")
return core;
const terminalLayer = terminal.termination.layer;
const perpendicularAxis = terminal.direction === "up" || terminal.direction === "down" ? "x" : "y";
const parallelAxis = perpendicularAxis === "x" ? "y" : "x";
const componentCoordinates = perpendicularAxis === "x" ? terminal.xCoordinates : terminal.yCoordinates;
const pitch = perpendicularAxis === "x" ? terminal.pitchX : terminal.pitchY;
if (componentCoordinates.length === 0 || !Number.isFinite(pitch) || pitch <= EPSILON5) {
return core;
}
const componentCenter = (Math.min(...componentCoordinates) + Math.max(...componentCoordinates)) / 2;
const inwardSign = Math.sign(componentCenter - source[perpendicularAxis]);
if (inwardSign === 0)
return core;
const coreIds = new Set(core);
const replacement = params.planeBuses.filter((bus) => {
const candidateSource = bus.connections[0]?.sourcePoint;
if (bus === terminal || bus.componentId !== terminal.componentId || bus.termination.type !== "plane" || bus.termination.layer !== terminalLayer || bus.direction !== terminal.direction || bus.connections.length !== 1 || !candidateSource || params.activeBusIds.has(bus.busId) || coreIds.has(bus.busId) || Math.abs(candidateSource[parallelAxis] - source[parallelAxis]) > EPSILON5) {
return false;
}
const inwardDistance = (candidateSource[perpendicularAxis] - source[perpendicularAxis]) * inwardSign;
return inwardDistance > EPSILON5 && inwardDistance <= pitch + EPSILON5;
}).toSorted((first, second) => {
const firstSource = first.connections[0].sourcePoint;
const secondSource = second.connections[0].sourcePoint;
return Math.abs(firstSource[perpendicularAxis] - source[perpendicularAxis]) - Math.abs(secondSource[perpendicularAxis] - source[perpendicularAxis]) || first.connections[0].connectionIndex - second.connections[0].connectionIndex;
})[0];
if (!replacement)
return core;
return core.map((busId, index) => index === terminalIndex ? replacement.busId : busId);
}
// node_modules/@tscircuit/fanout-solver/lib/should-use-adaptive-dense-plane-routing.ts
function getSourceFieldFacingEdge(buses) {
const firstBus = buses[0];
if (!firstBus || buses.some((bus) => bus.componentId !== firstBus.componentId))
return;
const sourcePoints = buses.flatMap((bus) => bus.connections.map((connection) => connection.sourcePoint));
if (sourcePoints.length === 0)
return;
const center = sourcePoints.reduce((sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }), { x: 0, y: 0 });
center.x /= sourcePoints.length;
center.y /= sourcePoints.length;
const { minX, maxX, minY, maxY } = firstBus.componentBounds;
const edgesByDistance = [
{ edge: "left", distance: Math.abs(center.x - minX) },
{ edge: "right", distance: Math.abs(maxX - center.x) },
{ edge: "bottom", distance: Math.abs(center.y - minY) },
{ edge: "top", distance: Math.abs(maxY - center.y) }
].toSorted((first, second) => first.distance - second.distance);
if (Math.abs(edgesByDistance[0].distance - edgesByDistance[1].distance) <= 0.000000001)
return;
return edgesByDistance[0].edge;
}
function shouldUseAdaptiveDensePlaneRouting(buses, allowBlindAndBuriedVias) {
if (allowBlindAndBuriedVias)
return false;
const boundaryBuses = buses.filter((bus) => bus.termination.type === "boundary");
const singletonBoundaryBuses = boundaryBuses.filter((bus) => bus.connections.length === 1);
const pairBoundaryBuses = boundaryBuses.filter((bus) => bus.connections.length === 2);
const wideBoundaryBuses = boundaryBuses.filter((bus) => bus.connections.length >= 8);
const planeCount = buses.filter((bus) => bus.termination.type === "plane" && bus.connections.length === 1).length;
const sharedExitEdge = wideBoundaryBuses[0]?.exitEdge;
if (boundaryBuses.length !== 9 || singletonBoundaryBuses.length !== 3 || pairBoundaryBuses.length !== 3 || wideBoundaryBuses.length !== 3 || planeCount < 64 || sharedExitEdge === undefined || wideBoundaryBuses.some((bus) => bus.exitEdge !== sharedExitEdge))
return false;
const sourceFacingEdge = getSourceFieldFacingEdge(wideBoundaryBuses);
if (sourceFacingEdge === undefined)
return false;
const sourceFacesHorizontalEdge = sourceFacingEdge === "left" || sourceFacingEdge === "right";
const exitUsesHorizontalEdge = sharedExitEdge === "left" || sharedExitEdge === "right";
return sourceFacesHorizontalEdge !== exitUsesHorizontalEdge;
}
// node_modules/@tscircuit/fanout-solver/lib/add-via-layer-metadata.ts
function addViaLayerMetadataToTrace(params) {
const { trace, layerNames, allowBlindAndBuriedVias } = params;
return {
...trace,
route: trace.route.map((routePoint) => routePoint.route_type === "via" && !allowBlindAndBuriedVias ? {
...routePoint,
layers: getViaSpanLayers({
fromLayer: routePoint.from_layer,
toLayer: routePoint.to_layer,
layerNames,
allowBlindAndBuriedVias
})
} : { ...routePoint })
};
}
function addViaLayerMetadataToSrj(params) {
const { srj, layerNames, allowBlindAndBuriedVias } = params;
return {
...srj,
traces: (srj.traces ?? []).map((trace) => addViaLayerMetadataToTrace({
trace,
layerNames,
allowBlindAndBuriedVias
}))
};
}
// node_modules/@tscircuit/fanout-solver/lib/boundary-exit.ts
function getExitEdgeForDirection(direction) {
switch (direction) {
case "left":
return "left";
case "right":
return "right";
case "up":
return "top";
case "down":
return "bottom";
}
}
function getDirectionForExitEdge(exitEdge) {
switch (exitEdge) {
case "left":
return "left";
case "right":
return "right";
case "top":
return "up";
case "bottom":
return "down";
}
}
function borderTargetIncludesEdge(preferredExit, exitEdge) {
return preferredExit === exitEdge || preferredExit.includes(exitEdge);
}
function getCornerBandSide(exitEdge, preferredExit) {
if (!exitEdge || !preferredExit?.includes("-"))
return;
if (!borderTargetIncludesEdge(preferredExit, exitEdge))
return;
if (exitEdge === "left" || exitEdge === "right") {
return preferredExit.startsWith("top-") ? "maximum" : "minimum";
}
return preferredExit.endsWith("-right") ? "maximum" : "minimum";
}
// node_modules/@tscircuit/fanout-solver/lib/build-output.ts
function createViaObstacle(plan, layerNames, via, viaIndex) {
const zLayers = via.spanLayers.map((layer) => {
const layerIndex = layerNames.indexOf(layer);
if (layerIndex < 0) {
throw new Error(`FanoutSolver: via for "${plan.connectionName}" uses unknown layer "${layer}"`);
}
return layerIndex;
});
const outputIds = createFanoutOutputIds(plan);
return {
obstacleId: viaIndex === "endpoint" ? outputIds.planeEndpointViaObstacleId : viaIndex === 0 ? outputIds.viaObstacleId : `${outputIds.viaObstacleId}:${viaIndex}`,
type: "rect",
center: via.center,
width: via.diameter,
height: via.diameter,
layers: via.spanLayers,
zLayers,
__zLayers: zLayers,
connectedTo: [plan.connectionName, plan.trace.pcb_trace_id]
};
}
function buildOutputSimpleRouteJson(params) {
const { inputSrj, plans, layerNames } = params;
const outputConnections = inputSrj.connections.map((connection) => ({
...connection,
pointsToConnect: connection.pointsToConnect.map((point) => ({ ...point }))
}));
const viaObstacles = [];
const planeTerminatedConnectionNames = new Set;
for (const plan of plans) {
const connection = outputConnections[plan.connectionIndex];
if (!connection) {
throw new Error(`FanoutSolver: output connection index ${plan.connectionIndex} is missing`);
}
const outputIds = createFanoutOutputIds(plan);
connection.pointsToConnect[plan.sourcePointIndex] = {
x: plan.exitPoint.x,
y: plan.exitPoint.y,
layer: plan.targetLayer,
pointId: plan.termination.type === "plane" ? outputIds.planeExitPointId : outputIds.boundaryExitPointId
};
if (plan.termination.type === "plane") {
planeTerminatedConnectionNames.add(plan.connectionName);
}
if (plan.via) {
const viaObstacle = createViaObstacle(plan, layerNames, plan.via, 0);
if (viaObstacle)
viaObstacles.push(viaObstacle);
}
for (const [additionalViaIndex, via] of (plan.additionalVias ?? []).entries()) {
const viaObstacle = createViaObstacle(plan, layerNames, via, additionalViaIndex + 1);
if (viaObstacle)
viaObstacles.push(viaObstacle);
}
if (plan.planeEndpointVia) {
const endpointViaObstacle = createViaObstacle(plan, layerNames, plan.planeEndpointVia, "endpoint");
if (endpointViaObstacle)
viaObstacles.push(endpointViaObstacle);
}
}
const planTraces = plans.flatMap((plan) => [
plan.trace,
...plan.planeEndpointTrace ? [plan.planeEndpointTrace] : []
]);
const coordinateRoutePoints = planTraces.flatMap((trace) => trace.route.filter((routePoint) => ("x" in routePoint) && ("y" in routePoint)));
const boundsMargin = Math.max(inputSrj.defaultObstacleMargin ?? 0, inputSrj.minTraceWidth);
const outputBounds = coordinateRoutePoints.length === 0 ? { ...inputSrj.bounds } : {
minX: Math.min(inputSrj.bounds.minX, ...coordinateRoutePoints.map((routePoint) => routePoint.x - boundsMargin)),
maxX: Math.max(inputSrj.bounds.maxX, ...coordinateRoutePoints.map((routePoint) => routePoint.x + boundsMargin)),
minY: Math.min(inputSrj.bounds.minY, ...coordinateRoutePoints.map((routePoint) => routePoint.y - boundsMargin)),
maxY: Math.max(inputSrj.bounds.maxY, ...coordinateRoutePoints.map((routePoint) => routePoint.y + boundsMargin))
};
return {
...inputSrj,
fanoutPlaneConnectivity: plans.flatMap((plan) => plan.termination.type === "plane" ? [
{
connectionName: plan.connectionName,
layer: plan.termination.layer
}
] : []),
bounds: outputBounds,
connections: outputConnections.filter((connection) => !planeTerminatedConnectionNames.has(connection.name)),
buses: inputSrj.buses?.map((bus) => ({
...bus,
connectionNames: bus.connectionNames.filter((connectionName) => !planeTerminatedConnectionNames.has(connectionName))
})).filter((bus) => bus.connectionNames.length > 0),
obstacles: [
...inputSrj.obstacles.map((obstacle) => ({
...obstacle,
center: { ...obstacle.center },
layers: [...obstacle.layers],
connectedTo: [...obstacle.connectedTo]
})),
...viaObstacles
],
traces: [
...(inputSrj.traces ?? []).map((trace) => ({
...trace,
route: trace.route.map((routePoint) => ({ ...routePoint }))
})),
...planTraces
]
};
}
// node_modules/@tscircuit/fanout-solver/lib/get-routed-trace-copper.ts
function getRoutedTraceCopper(srj, trace, allowBlindAndBuriedVias = true) {
const layerNames = getCopperLayerNames(srj.layerCount);
const segments = [];
const vias = [];
let previousWire;
for (const routePoint of trace.route) {
if (routePoint.route_type === "via") {
vias.push({
center: { x: routePoint.x, y: routePoint.y },
diameter: routePoint.via_diameter ?? srj.minViaPadDiameter ?? srj.min_via_pad_diameter ?? srj.minViaDiameter ?? srj.minTraceWidth,
spanLayers: getRouteViaSpanLayers({
fromLayer: routePoint.from_layer,
toLayer: routePoint.to_layer,
layers: "layers" in routePoint && Array.isArray(routePoint.layers) ? routePoint.layers : undefined,
layerNames,
allowBlindAndBuriedVias
})
});
previousWire = undefined;
continue;
}
if (routePoint.route_type !== "wire") {
previousWire = undefined;
continue;
}
if (previousWire?.layer === routePoint.layer && (previousWire.x !== routePoint.x || previousWire.y !== routePoint.y)) {
segments.push({
start: { x: previousWire.x, y: previousWire.y },
end: { x: routePoint.x, y: routePoint.y },
width: Math.max(previousWire.width, routePoint.width),
layer: routePoint.layer
});
}
previousWire = routePoint;
}
return {
trace,
connectionName: trace.connection_name,
segments,
vias
};
}
function getAllRoutedTraceCopper(srj, allowBlindAndBuriedVias = true) {
return (srj.traces ?? []).map((trace) => getRoutedTraceCopper(srj, trace, allowBlindAndBuriedVias));
}
// node_modules/@tscircuit/fanout-solver/lib/get-dogbone-side-variants.ts
function getDogboneSideVariants(connections, direction) {
const axis = direction === "up" || direction === "down" ? "y" : "x";
const variants = connections.slice(0, 32).map(({ connectionIndex }) => [connectionIndex]);
for (let first = 0;first < connections.length; first++) {
for (let second = first + 1;second < connections.length; second++) {
if (variants.length >= 32)
return variants;
const a = connections[first];
const b = connections[second];
if (Math.abs(a.sourcePoint[axis] - b.sourcePoint[axis]) <= 0.000000001) {
variants.push([a.connectionIndex, b.connectionIndex]);
}
}
}
return variants;
}
// node_modules/@tscircuit/fanout-solver/lib/match-component-dogbone-via-sites.ts
var EPSILON6 = 0.000000001;
var DEFAULT_MAXIMUM_SEARCH_STATES = 1e5;
function assertGeometryRules(rules) {
for (const [name, value] of [
["viaDiameter", rules.viaDiameter],
["traceWidth", rules.traceWidth]
]) {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`FanoutSolver: dogbone ${name} must be a positive finite number, received ${value}`);
}
}
if (!Number.isFinite(rules.clearance) || rules.clearance < 0) {
throw new Error(`FanoutSolver: dogbone clearance must be a non-negative finite number, received ${rules.clearance}`);
}
if (rules.viaHoleDiameter !== undefined && (!Number.isFinite(rules.viaHoleDiameter) || rules.viaHoleDiameter <= 0)) {
throw new Error(`FanoutSolver: dogbone viaHoleDiameter must be a positive finite number, received ${rules.viaHoleDiameter}`);
}
if (rules.holeToHoleClearance !== undefined && (!Number.isFinite(rules.holeToHoleClearance) || rules.holeToHoleClearance < 0)) {
throw new Error(`FanoutSolver: dogbone holeToHoleClearance must be a non-negative finite number, received ${rules.holeToHoleClearance}`);
}
if (rules.holeToHoleClearance !== undefined && rules.viaHoleDiameter === undefined) {
throw new Error("FanoutSolver: dogbone holeToHoleClearance requires viaHoleDiameter");
}
const maximumSearchStates = rules.maximumSearchStates ?? DEFAULT_MAXIMUM_SEARCH_STATES;
if (!Number.isInteger(maximumSearchStates) || maximumSearchStates < 1) {
throw new Error(`FanoutSolver: dogbone maximumSearchStates must be a positive integer, received ${maximumSearchStates}`);
}
return maximumSearchStates;
}
function uniqueSortedCoordinates(values) {
const result = [];
for (const value of values.toSorted((first, second) => first - second)) {
if (!Number.isFinite(value))
continue;
if (result.length === 0 || Math.abs(result.at(-1) - value) > EPSILON6) {
result.push(value);
}
}
return result;
}
function getComponentMatchingInputs(preparedBuses) {
const byComponent = new Map;
const componentByConnectionIndex = new Map;
const obstacleSetByComponentId = new Map;
for (const bus of preparedBuses) {
let component = byComponent.get(bus.componentId);
if (!component) {
component = {
componentId: bus.componentId,
connections: [],
obstacles: [],
xCoordinates: [],
yCoordinates: [],
pitchX: Number.POSITIVE_INFINITY,
pitchY: Number.POSITIVE_INFINITY
};
byComponent.set(bus.componentId, component);
obstacleSetByComponentId.set(bus.componentId, new Set);
}
component.xCoordinates.push(...bus.xCoordinates);
component.yCoordinates.push(...bus.yCoordinates);
if (Number.isFinite(bus.pitchX) && bus.pitchX > EPSILON6) {
component.pitchX = Math.min(component.pitchX, bus.pitchX);
}
if (Number.isFinite(bus.pitchY) && bus.pitchY > EPSILON6) {
component.pitchY = Math.min(component.pitchY, bus.pitchY);
}
const obstacleSet = obstacleSetByComponentId.get(bus.componentId);
for (const obstacle of bus.componentObstacles) {
if (!obstacleSet.has(obstacle)) {
obstacleSet.add(obstacle);
component.obstacles.push(obstacle);
}
}
for (const preparedConnection of bus.connections) {
const existingComponent = componentByConnectionIndex.get(preparedConnection.connectionIndex);
if (existingComponent !== undefined) {
if (existingComponent !== bus.componentId) {
throw new Error(`FanoutSolver: connection index ${preparedConnection.connectionIndex} belongs to multiple components`);
}
continue;
}
componentByConnectionIndex.set(preparedConnection.connectionIndex, bus.componentId);
component.connections.push({
preparedConnection,
busId: bus.busId,
direction: bus.direction,
terminationType: bus.termination.type
});
}
}
return [...byComponent.values()].map((component) => ({
...component,
connections: component.connections.toSorted((first, second) => first.preparedConnection.connectionIndex - second.preparedConnection.connectionIndex),
xCoordinates: uniqueSortedCoordinates(component.xCoordinates),
yCoordinates: uniqueSortedCoordinates(component.yCoordinates)
})).toSorted((first, second) => first.componentId.localeCompare(second.componentId));
}
function getInterstitialCoordinates(params) {
const { coordinates, pitch } = params;
if (coordinates.length === 0 || !Number.isFinite(pitch) || pitch <= EPSILON6) {
return [];
}
const interstitialCoordinates = [coordinates[0] - pitch / 2];
for (let index = 1;index < coordinates.length; index++) {
interstitialCoordinates.push((coordinates[index - 1] + coordinates[index]) / 2);
}
interstitialCoordinates.push(coordinates.at(-1) + pitch / 2);
return uniqueSortedCoordinates(interstitialCoordinates);
}
function getAdjacentInterstitialCoordinates(params) {
const { sourceCoordinate, coordinates, pitch } = params;
const interstitialCoordinates = getInterstitialCoordinates({
coordinates,
pitch
});
const before = interstitialCoordinates.filter((coordinate) => coordinate < sourceCoordinate - EPSILON6).at(-1);
const after = interstitialCoordinates.find((coordinate) => coordinate > sourceCoordinate + EPSILON6);
return [before, after].filter((coordinate) => coordinate !== undefined);
}
function directSegmentIsStraightOr45(start, end) {
const absoluteX = Math.abs(end.x - start.x);
const absoluteY = Math.abs(end.y - start.y);
return absoluteX <= EPSILON6 || absoluteY <= EPSILON6 || Math.abs(absoluteX - absoluteY) <= EPSILON6;
}
function getOutwardRank(params) {
const { source, site, direction } = params;
const outwardDisplacement = direction === "right" ? site.x - source.x : direction === "left" ? source.x - site.x : direction === "up" ? site.y - source.y : source.y - site.y;
return outwardDisplacement > EPSILON6 ? 0 : Math.abs(outwardDisplacement) <= EPSILON6 ? 1 : 2;
}
function viaSiteClearsObstacles(params) {
const { point, obstacles, viaDiameter, clearance } = params;
const requiredClearance = viaDiameter / 2 + clearance;
return obstacles.every((obstacle) => distancePointToObstacle(point, obstacle) >= requiredClearance - EPSILON6);
}
function sourceSegmentClearsOtherObstacles(params) {
const { segment, sourceObstacle, obstacles, clearance } = params;
const requiredClearance = segment.width / 2 + clearance;
return obstacles.every((obstacle) => obstacle === sourceObstacle || distanceSegmentToObstacle(segment, obstacle) >= requiredClearance - EPSILON6);
}
function getConnectionCandidates(params) {
const { connection, component, rules } = params;
const { preparedConnection, direction } = connection;
const obstacles = rules.additionalObstacles ? [...new Set([...component.obstacles, ...rules.additionalObstacles])] : component.obstacles;
const source = {
x: preparedConnection.sourcePoint.x,
y: preparedConnection.sourcePoint.y
};
const adjacentX = getAdjacentInterstitialCoordinates({
sourceCoordinate: source.x,
coordinates: component.xCoordinates,
pitch: component.pitchX
});
const adjacentY = getAdjacentInterstitialCoordinates({
sourceCoordinate: source.y,
coordinates: component.yCoordinates,
pitch: component.pitchY
});
const rawPoints = [
...adjacentX.map((x) => ({ x, y: source.y })),
...adjacentY.map((y) => ({ x: source.x, y })),
...adjacentX.flatMap((x) => adjacentY.map((y) => ({ x, y })))
];
const uniquePoints2 = [];
for (const point of rawPoints) {
if (!uniquePoints2.some((candidate) => distance(candidate, point) <= EPSILON6)) {
uniquePoints2.push(point);
}
}
const fixedViaPoint = rules.fixedViaPointsByConnectionIndex?.get(preparedConnection.connectionIndex);
const candidatePoints = fixedViaPoint ? [fixedViaPoint] : uniquePoints2;
const candidates = [];
for (const point of candidatePoints) {
if (connection.terminationType === "plane" && !directSegmentIsStraightOr45(source, point)) {
continue;
}
if (!viaSiteClearsObstacles({
point,
obstacles,
viaDiameter: rules.viaDiameter,
clearance: rules.clearance
})) {
continue;
}
const sourceSegment = {
start: source,
end: point,
width: rules.traceWidth,
layer: preparedConnection.sourceLayer
};
if (!sourceSegmentClearsOtherObstacles({
segment: sourceSegment,
sourceObstacle: preparedConnection.sourceObstacle,
obstacles,
clearance: rules.clearance
})) {
continue;
}
const candidateClearsRoutedCopper = (rules.blockingSegments ?? []).every((blocker) => {
if (blocker.connectionIndex === preparedConnection.connectionIndex) {
return true;
}
if (rules.canShareCopper?.(preparedConnection.connectionIndex, blocker.connectionIndex)) {
return true;
}
const viaToTraceClearance = rules.viaDiameter / 2 + blocker.segment.width / 2 + rules.clearance;
if (distancePointToSegment(point, blocker.segment.start, blocker.segment.end) < viaToTraceClearance - EPSILON6) {
return false;
}
return blocker.segment.layer !== sourceSegment.layer || segmentsAreClear(sourceSegment, blocker.segment, rules.clearance);
});
if (!candidateClearsRoutedCopper)
continue;
const candidateClearsRoutedVias = (rules.blockingVias ?? []).every((blocker) => {
if (blocker.connectionIndex === preparedConnection.connectionIndex) {
return true;
}
if (distance(point, blocker.center) < (rules.viaDiameter + blocker.diameter) / 2 + rules.clearance - EPSILON6) {
return false;
}
if (blocker.spanLayers.includes(sourceSegment.layer) && distancePointToSegment(blocker.center, sourceSegment.start, sourceSegment.end) < blocker.diameter / 2 + sourceSegment.width / 2 + rules.clearance - EPSILON6) {
return false;
}
return true;
});
if (!candidateClearsRoutedVias)
continue;
candidates.push({
connectionIndex: preparedConnection.connectionIndex,
point,
sourceSegment,
outwardRank: getOutwardRank({ source, site: point, direction })
});
}
const perpendicularCoordinates = direction === "left" || direction === "right" ? component.yCoordinates : component.xCoordinates;
const sourcePerpendicularCoordinate = direction === "left" || direction === "right" ? source.y : source.x;
const perpendicularGridIndex = perpendicularCoordinates.findIndex((coordinate) => Math.abs(coordinate - sourcePerpendicularCoordinate) <= EPSILON6);
const planeCheckerboardSide = perpendicularGridIndex >= 0 ? perpendicularGridIndex % 2 === 0 ? 1 : -1 : undefined;
const preferredPerpendicularSide = connection.terminationType === "boundary" ? rules.preferredBoundaryPerpendicularSideByBusId?.get(connection.busId) : rules.preferPlaneCheckerboardSites && connection.terminationType === "plane" && (direction === "left" || direction === "right") ? planeCheckerboardSide : undefined;
const preferOutward = connection.terminationType === "boundary" ? rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true : true;
const preferredViaPoint = rules.preferredViaPointsByConnectionIndex?.get(preparedConnection.connectionIndex);
const getPerpendicularPreferenceRank = (candidate) => {
if (preferredPerpendicularSide === undefined)
return 0;
const displacement = direction === "left" || direction === "right" ? candidate.point.y - source.y : candidate.point.x - source.x;
return displacement * preferredPerpendicularSide > EPSILON6 ? 0 : Math.abs(displacement) <= EPSILON6 ? 1 : 2;
};
return candidates.toSorted((first, second) => (preferredViaPoint ? Number(distance(first.point, preferredViaPoint) > EPSILON6) - Number(distance(second.point, preferredViaPoint) > EPSILON6) : 0) || (preferOutward ? first.outwardRank - second.outwardRank : second.outwardRank - first.outwardRank) || getPerpendicularPreferenceRank(first) - getPerpendicularPreferenceRank(second) || distance(source, first.point) - distance(source, second.point) || first.point.x - second.point.x || first.point.y - second.point.y);
}
function candidatesAreMutuallyClear(params) {
const { first, second, rules } = params;
const canShareCopper = rules.canShareCopper?.(first.connectionIndex, second.connectionIndex) ?? false;
const requiredHoleSeparation = rules.viaHoleDiameter ? rules.viaHoleDiameter + (rules.holeToHoleClearance ?? rules.clearance) : 0;
const requiredViaSeparation = canShareCopper ? requiredHoleSeparation : Math.max(rules.viaDiameter + rules.clearance, requiredHoleSeparation);
if (distance(first.point, second.point) < requiredViaSeparation - EPSILON6) {
return false;
}
const requiredViaToTraceClearance = rules.viaDiameter / 2 + rules.traceWidth / 2 + rules.clearance;
if (!canShareCopper) {
if (distancePointToSegment(first.point, second.sourceSegment.start, second.sourceSegment.end) < requiredViaToTraceClearance - EPSILON6 || distancePointToSegment(second.point, first.sourceSegment.start, first.sourceSegment.end) < requiredViaToTraceClearance - EPSILON6) {
return false;
}
}
if (canShareCopper)
return true;
return segmentsAreClear(first.sourceSegment, second.sourceSegment, rules.clearance);
}
function matchComponent(params) {
const { component, rules, consumeSearchState } = params;
const entries = component.connections.map((connection) => ({
connection,
candidates: getConnectionCandidates({ connection, component, rules })
}));
if (entries.some((entry) => entry.candidates.length === 0))
return null;
const compatibilityCache = new Map;
const candidatesAreCompatible = (first, second) => {
const cached = compatibilityCache.get(first)?.get(second);
if (cached !== undefined)
return cached;
const compatible = candidatesAreMutuallyClear({ first, second, rules });
const firstCache = compatibilityCache.get(first) ?? new Map;
firstCache.set(second, compatible);
compatibilityCache.set(first, firstCache);
const secondCache = compatibilityCache.get(second) ?? new Map;
secondCache.set(first, compatible);
compatibilityCache.set(second, secondCache);
return compatible;
};
const forcedCandidates = entries.flatMap((entry) => entry.candidates.length === 1 ? [entry.candidates[0]] : []);
for (let candidateIndex = 0;candidateIndex < forcedCandidates.length; candidateIndex++) {
const candidate = forcedCandidates[candidateIndex];
for (let previousIndex = 0;previousIndex < candidateIndex; previousIndex++) {
if (!candidatesAreCompatible(candidate, forcedCandidates[previousIndex])) {
return null;
}
}
}
const assignedCandidates = new Map(forcedCandidates.map((candidate) => [candidate.connectionIndex, candidate]));
const remaining = new Set(entries.flatMap((entry) => entry.candidates.length > 1 ? [entry.connection.preparedConnection.connectionIndex] : []));
const entryByConnectionIndex = new Map(entries.map((entry) => [
entry.connection.preparedConnection.connectionIndex,
entry
]));
if (remaining.size === 0) {
return new Map([...assignedCandidates.entries()].map(([connectionIndex, candidate]) => [
connectionIndex,
{ ...candidate.point }
]));
}
const getViableCandidates = (entry) => entry.candidates.filter((candidate) => [...assignedCandidates.values()].every((assignedCandidate) => candidatesAreCompatible(candidate, assignedCandidate)));
const augmentMatching = () => {
if (!consumeSearchState())
return false;
if (remaining.size === 0)
return true;
let selectedEntry;
let selectedCandidates = [];
for (const connectionIndex2 of [...remaining].toSorted((first, second) => first - second)) {
const entry = entryByConnectionIndex.get(connectionIndex2);
const viableCandidates = getViableCandidates(entry);
if (viableCandidates.length === 0)
return false;
if (!selectedEntry || viableCandidates.length < selectedCandidates.length || viableCandidates.length === selectedCandidates.length && connectionIndex2 < selectedEntry.connection.preparedConnection.connectionIndex) {
selectedEntry = entry;
selectedCandidates = viableCandidates;
}
}
const connectionIndex = selectedEntry.connection.preparedConnection.connectionIndex;
remaining.delete(connectionIndex);
for (const candidate of selectedCandidates) {
assignedCandidates.set(connectionIndex, candidate);
if (augmentMatching())
return true;
assignedCandidates.delete(connectionIndex);
}
remaining.add(connectionIndex);
return false;
};
if (!augmentMatching())
return null;
return new Map([...assignedCandidates.entries()].toSorted(([first], [second]) => first - second).map(([connectionIndex, candidate]) => [
connectionIndex,
{ ...candidate.point }
]));
}
function matchComponentDogboneViaSites(preparedBuses, rules) {
const maximumSearchStates = assertGeometryRules(rules);
if (preparedBuses.length === 0)
return new Map;
let consumedSearchStates = 0;
const consumeSearchState = () => {
consumedSearchStates++;
return consumedSearchStates <= maximumSearchStates;
};
const result = new Map;
for (const component of getComponentMatchingInputs(preparedBuses)) {
const componentResult = matchComponent({
component,
rules,
consumeSearchState
});
if (!componentResult)
return null;
for (const [connectionIndex, point] of componentResult) {
result.set(connectionIndex, point);
}
}
return result;
}
function getComponentDogboneViaSiteCandidates(preparedBuses, rules) {
assertGeometryRules(rules);
return getComponentMatchingInputs(preparedBuses).flatMap((component) => component.connections.flatMap((connection) => getConnectionCandidates({ connection, component, rules }).map((candidate) => ({
connectionIndex: candidate.connectionIndex,
point: { ...candidate.point }
}))));
}
// node_modules/@tscircuit/fanout-solver/lib/route-via-minimal-winding.ts
var EPSILON7 = 0.0000001;
var MAX_GRID_NODE_COUNT = 120000;
var MAX_EXPANDED_STATE_COUNT = 240000;
var EXPANDED_STATES_PER_STEP = 5000;
var MAX_CONNECTOR_COUNT = 24;
var CONNECTOR_RADIUS_IN_STEPS = 3.25;
function getObstacleAxisAlignedBounds(obstacle) {
const shapeAwareObstacle = obstacle;
if (shapeAwareObstacle.shape === "circle") {
const radius = obstacle.width / 2;
return {
minX: obstacle.center.x - radius,
maxX: obstacle.center.x + radius,
minY: obstacle.center.y - radius,
maxY: obstacle.center.y + radius,
xRadius: radius
};
}
const rotationRadians = (shapeAwareObstacle.ccwRotationDegrees ?? 0) * Math.PI / 180;
const absoluteCosine = Math.abs(Math.cos(rotationRadians));
const absoluteSine = Math.abs(Math.sin(rotationRadians));
const halfWidth = obstacle.width / 2;
const halfHeight = obstacle.height / 2;
const xRadius = absoluteCosine * halfWidth + absoluteSine * halfHeight;
const yRadius = absoluteSine * halfWidth + absoluteCosine * halfHeight;
return {
minX: obstacle.center.x - xRadius,
maxX: obstacle.center.x + xRadius,
minY: obstacle.center.y - yRadius,
maxY: obstacle.center.y + yRadius,
xRadius
};
}
class ObstacleSpatialIndex {
obstaclesByCenterX;
maximumXRadius;
constructor(obstacles) {
this.obstaclesByCenterX = obstacles.map((obstacle) => ({
obstacle,
...getObstacleAxisAlignedBounds(obstacle)
})).toSorted((first, second) => first.obstacle.center.x - second.obstacle.center.x);
this.maximumXRadius = this.obstaclesByCenterX.reduce((maximum, obstacle) => Math.max(maximum, obstacle.xRadius), 0);
}
querySegment(segment, margin) {
const segmentMinX = Math.min(segment.start.x, segment.end.x);
const segmentMaxX = Math.max(segment.start.x, segment.end.x);
const segmentMinY = Math.min(segment.start.y, segment.end.y);
const segmentMaxY = Math.max(segment.start.y, segment.end.y);
const minimumCenterX = segmentMinX - margin - this.maximumXRadius;
const maximumCenterX = segmentMaxX + margin + this.maximumXRadius;
let low = 0;
let high = this.obstaclesByCenterX.length;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if (this.obstaclesByCenterX[middle].obstacle.center.x < minimumCenterX) {
low = middle + 1;
} else {
high = middle;
}
}
const candidates = [];
for (let obstacleIndex = low;obstacleIndex < this.obstaclesByCenterX.length; obstacleIndex++) {
const indexedObstacle = this.obstaclesByCenterX[obstacleIndex];
if (indexedObstacle.obstacle.center.x > maximumCenterX)
break;
if (indexedObstacle.maxX < segmentMinX - margin || indexedObstacle.minX > segmentMaxX + margin || indexedObstacle.maxY < segmentMinY - margin || indexedObstacle.minY > segmentMaxY + margin) {
continue;
}
candidates.push(indexedObstacle.obstacle);
}
return candidates;
}
}
function* iterateUniqueRouteOrders(params) {
const {
initialOrderFactories,
rotationBase,
getItemKey,
maximumOrderCount = Number.POSITIVE_INFINITY
} = params;
const seenOrderKeys = new Set;
let yieldedOrderCount = 0;
const getOrderKey = (order) => order.map(getItemKey).join("\x00");
for (const createOrder of initialOrderFactories) {
if (yieldedOrderCount >= maximumOrderCount)
return;
const order = createOrder();
const key = getOrderKey(order);
if (seenOrderKeys.has(key))
continue;
seenOrderKeys.add(key);
yieldedOrderCount++;
yield order;
}
for (let offset = 1;offset < rotationBase.length; offset++) {
if (yieldedOrderCount >= maximumOrderCount)
return;
const order = [
...rotationBase.slice(offset),
...rotationBase.slice(0, offset)
];
const key = getOrderKey(order);
if (seenOrderKeys.has(key))
continue;
seenOrderKeys.add(key);
yieldedOrderCount++;
yield order;
}
}
class MinHeap {
values = [];
get size() {
return this.values.length;
}
sampleEntries(maximumCount) {
if (this.values.length <= maximumCount)
return this.values;
const stride = Math.ceil(this.values.length / maximumCount);
return this.values.filter((_, index) => index % stride === 0);
}
push(value) {
this.values.push(value);
let index = this.values.length - 1;
while (index > 0) {
const parent = Math.floor((index - 1) / 2);
if (this.values[parent].score <= value.score)
break;
this.values[index] = this.values[parent];
index = parent;
}
this.values[index] = value;
}
pop() {
const result = this.values[0];
const last = this.values.pop();
if (!result || !last || this.values.length === 0)
return result;
let index = 0;
while (true) {
const left = index * 2 + 1;
const right = left + 1;
if (left >= this.values.length)
break;
const child = right < this.values.length && this.values[right].score < this.values[left].score ? right : left;
if (this.values[child].score >= last.score)
break;
this.values[index] = this.values[child];
index = child;
}
this.values[index] = last;
return result;
}
}
function getPerpendicularAxis(point, direction) {
return direction === "left" || direction === "right" ? point.y : point.x;
}
function getConnectorVariants(start, end) {
const deltaX = end.x - start.x;
const deltaY = end.y - start.y;
const absoluteX = Math.abs(deltaX);
const absoluteY = Math.abs(deltaY);
if (absoluteX < EPSILON7 || absoluteY < EPSILON7 || Math.abs(absoluteX - absoluteY) < EPSILON7) {
return [[start, end]];
}
if (absoluteX > absoluteY) {
return [
[start, { x: start.x + Math.sign(deltaX) * absoluteY, y: end.y }, end],
[start, { x: end.x - Math.sign(deltaX) * absoluteY, y: start.y }, end]
];
}
return [
[start, { x: end.x, y: start.y + Math.sign(deltaY) * absoluteX }, end],
[start, { x: start.x, y: end.y - Math.sign(deltaY) * absoluteX }, end]
];
}
function compressPath(points) {
if (points.length < 3)
return points;
const compressed = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = compressed.at(-1);
const current = points[index];
const next = points[index + 1];
const incomingX = Math.sign(current.x - previous.x);
const incomingY = Math.sign(current.y - previous.y);
const outgoingX = Math.sign(next.x - current.x);
const outgoingY = Math.sign(next.y - current.y);
if (incomingX !== outgoingX || incomingY !== outgoingY) {
compressed.push(current);
}
}
compressed.push(points.at(-1));
return compressed;
}
function getSegments(points, width, layer) {
return points.slice(1).flatMap((point, index) => {
const start = points[index];
return distance(start, point) < EPSILON7 ? [] : [{ start, end: point, width, layer }];
});
}
function segmentIsStraightOr45Degrees(segment) {
const deltaX = Math.abs(segment.end.x - segment.start.x);
const deltaY = Math.abs(segment.end.y - segment.start.y);
return deltaX < EPSILON7 || deltaY < EPSILON7 || Math.abs(deltaX - deltaY) < EPSILON7;
}
function pathHasNoProperSelfCrossing(segments) {
for (let firstIndex = 0;firstIndex < segments.length; firstIndex++) {
for (let secondIndex = firstIndex + 2;secondIndex < segments.length; secondIndex++) {
if (firstIndex === 0 && secondIndex === segments.length - 1 && distance(segments[firstIndex].start, segments[secondIndex].end) < EPSILON7) {
continue;
}
if (distanceSegmentToSegment(segments[firstIndex].start, segments[firstIndex].end, segments[secondIndex].start, segments[secondIndex].end) < EPSILON7) {
return false;
}
}
}
return true;
}
function getPlanVias(plan) {
return [
plan.via,
...plan.additionalVias ?? [],
plan.planeEndpointVia
].filter((via) => Boolean(via));
}
function getBlockingCopper(params) {
const { srj, acceptedPlans, allowBlindAndBuriedVias } = params;
const routedTraceCopper = getAllRoutedTraceCopper(srj, allowBlindAndBuriedVias);
return {
segments: [
...routedTraceCopper.flatMap((copper) => copper.segments.map((segment) => ({
connectionName: copper.connectionName,
segment
}))),
...acceptedPlans.flatMap((plan) => [...plan.segments, ...plan.planeEndpointSegments ?? []].map((segment) => ({
connectionName: plan.connectionName,
segment
})))
],
vias: [
...routedTraceCopper.flatMap((copper) => copper.vias.map((via) => ({
connectionName: copper.connectionName,
via
}))),
...acceptedPlans.flatMap((plan) => getPlanVias(plan).map((via) => ({
connectionName: plan.connectionName,
via
})))
]
};
}
function buildPlan(params) {
const {
bus,
terminal,
targetLayer,
targetLayerPoints,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
allowBlindAndBuriedVias
} = params;
const connection = terminal.connection;
const sourcePoint = {
x: connection.sourcePoint.x,
y: connection.sourcePoint.y
};
const sourceSegment = {
start: sourcePoint,
end: terminal.viaPoint,
width: traceWidth,
layer: connection.sourceLayer
};
const hasSourceDogbone = distance(sourcePoint, terminal.viaPoint) > EPSILON7;
const changesLayer = connection.sourceLayer !== targetLayer;
const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer);
const spanLayers = getViaSpanLayers({
fromLayer: connection.sourceLayer,
toLayer: targetLayer,
layerNames,
allowBlindAndBuriedVias
});
const via = {
center: terminal.viaPoint,
diameter: viaDiameter,
holeDiameter: viaHoleDiameter,
fromLayer: connection.sourceLayer,
toLayer: targetLayer,
spanLayers
};
const route = [
{
route_type: "wire",
...sourcePoint,
width: traceWidth,
layer: connection.sourceLayer,
...connection.sourcePoint.pcb_port_id ? { start_pcb_port_id: connection.sourcePoint.pcb_port_id } : {}
},
...hasSourceDogbone ? [
{
route_type: "wire",
...terminal.viaPoint,
width: traceWidth,
layer: connection.sourceLayer
}
] : [],
...changesLayer ? [
{
route_type: "via",
...terminal.viaPoint,
from_layer: connection.sourceLayer,
to_layer: targetLayer,
via_diameter: viaDiameter,
via_hole_diameter: viaHoleDiameter
},
{
route_type: "wire",
...terminal.viaPoint,
width: traceWidth,
layer: targetLayer
}
] : [],
...targetLayerPoints.slice(1).map((point) => ({
route_type: "wire",
...point,
width: traceWidth,
layer: targetLayer
}))
];
const outputIds = createFanoutOutputIds({
connectionName: connection.connection.name,
sourcePointIndex: connection.sourcePointIndex
});
const segments = [
...hasSourceDogbone ? [sourceSegment] : [],
...targetSegments
];
const cornerBandSide = getCornerBandSide(bus.exitEdge, bus.preferredExit);
return {
busId: bus.busId,
connectionName: connection.connection.name,
connectionIndex: connection.connectionIndex,
sourcePointIndex: connection.sourcePointIndex,
sourcePoint: connection.sourcePoint,
sourceObstacle: connection.sourceObstacle,
sourceLayer: connection.sourceLayer,
targetPoint: connection.targetPoint,
targetLayer,
termination: bus.termination,
direction: bus.direction,
...bus.exitEdge ? { exitEdge: bus.exitEdge } : {},
...cornerBandSide ? { cornerBandSide } : {},
exitPoint: terminal.exitPoint,
trace: {
type: "pcb_trace",
pcb_trace_id: outputIds.traceId,
connection_name: connection.connection.name,
connectsTo: [
...connection.sourcePoint.pointId ? [connection.sourcePoint.pointId] : [],
...connection.sourcePoint.pcb_port_id ? [connection.sourcePoint.pcb_port_id] : [],
outputIds.boundaryExitPointId
],
route
},
segments,
via: changesLayer ? via : undefined,
length: segments.reduce((total, segment) => total + distance(segment.start, segment.end), 0)
};
}
function* routeViaMinimalWindingAlternativesSteps(params, maximumAlternatives = 1, includeVisualization = false) {
if (!Number.isInteger(maximumAlternatives) || maximumAlternatives < 1) {
throw new Error(`FanoutSolver: maximum winding alternatives must be a positive integer, received ${maximumAlternatives}`);
}
const {
srj,
bus,
targetLayer,
terminals,
acceptedPlans,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias = true,
allowSameNetMerges = false,
maximumRouteOrderAttempts,
reservedVias = [],
gridStepDivisor = 1,
preferTargetDirectedLaneBias = false,
allowSourceLayerRouting = false,
adaptiveRouteOrder = false,
alignGridToPads = false,
includeReverseTargetRotation = false
} = params;
if (maximumRouteOrderAttempts !== undefined && (!Number.isInteger(maximumRouteOrderAttempts) || maximumRouteOrderAttempts < 1)) {
throw new Error(`FanoutSolver: maximumRouteOrderAttempts must be a positive integer, received ${maximumRouteOrderAttempts}`);
}
if (gridStepDivisor !== 1 && gridStepDivisor !== 2) {
throw new Error(`FanoutSolver: gridStepDivisor must be 1 or 2, received ${gridStepDivisor}`);
}
if (terminals.length === 0 || !bus.exitEdge || !allowSourceLayerRouting && terminals.some((terminal) => terminal.connection.sourceLayer === targetLayer)) {
return [];
}
const baseGridStep = (traceWidth + clearance) / gridStepDivisor;
const pitch = Math.min(bus.pitchX, bus.pitchY);
const alignGridToPitch = alignGridToPads && gridStepDivisor === 2 && Number.isFinite(pitch);
const gridStep = alignGridToPitch ? pitch / (2 * Math.ceil(pitch / (2 * baseGridStep))) : baseGridStep;
if (!Number.isFinite(gridStep) || gridStep <= 0)
return [];
const { minX, maxX, minY, maxY } = bus.sharedBoundary;
const originX = bus.xCoordinates[0] ?? minX;
const originY = bus.yCoordinates[0] ?? minY;
const gridMinX = alignGridToPitch ? originX + Math.ceil((minX - originX) / gridStep) * gridStep : minX;
const gridMinY = alignGridToPitch ? originY + Math.ceil((minY - originY) / gridStep) * gridStep : minY;
const columnCount = Math.floor((maxX - gridMinX) / gridStep) + 1;
const rowCount = Math.floor((maxY - gridMinY) / gridStep) + 1;
const nodeCount = columnCount * rowCount;
if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
return [];
}
const nodes = Array.from({ length: nodeCount }, (_, index) => {
const column = index % columnCount;
const row = Math.floor(index / columnCount);
return {
column,
row,
point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep }
};
});
const sampledGridPoints = includeVisualization ? nodes.filter((_, index) => index % Math.max(1, Math.ceil(nodes.length / 1000)) === 0).map((node) => node.point) : [];
const targetLayerObstacles = srj.obstacles.filter((obstacle) => obstacle.layers.includes(targetLayer));
const targetLayerObstacleIndex = new ObstacleSpatialIndex(targetLayerObstacles);
const blockingCopper = getBlockingCopper({
srj,
acceptedPlans,
allowBlindAndBuriedVias
});
const blockingSegments = blockingCopper.segments.filter(({ segment }) => {
if (segment.layer !== targetLayer)
return false;
const margin = (segment.width + traceWidth) / 2 + clearance;
return !(Math.max(segment.start.x, segment.end.x) < minX - margin || Math.min(segment.start.x, segment.end.x) > maxX + margin || Math.max(segment.start.y, segment.end.y) < minY - margin || Math.min(segment.start.y, segment.end.y) > maxY + margin);
});
if (allowSourceLayerRouting) {
blockingSegments.push(...reservedVias.flatMap((reserved) => reserved.sourceEscapeSegment?.layer === targetLayer ? [
{
connectionName: reserved.connectionName,
segment: reserved.sourceEscapeSegment
}
] : []));
}
const blockingVias = blockingCopper.vias.filter(({ via }) => {
if (!via.spanLayers.includes(targetLayer))
return false;
const margin = via.diameter / 2 + traceWidth / 2 + clearance;
return !(via.center.x < minX - margin || via.center.x > maxX + margin || via.center.y < minY - margin || via.center.y > maxY + margin);
});
blockingVias.push(...reservedVias.filter(({ via }) => {
if (!via.spanLayers.includes(targetLayer))
return false;
const margin = via.diameter / 2 + traceWidth / 2 + clearance;
return !(via.center.x < minX - margin || via.center.x > maxX + margin || via.center.y < minY - margin || via.center.y > maxY + margin);
}));
const terminalVias = terminals.map((terminal) => ({
connectionName: terminal.connection.connection.name,
via: {
center: terminal.viaPoint,
diameter: viaDiameter,
spanLayers: getViaSpanLayers({
fromLayer: terminal.connection.sourceLayer,
toLayer: targetLayer,
layerNames,
allowBlindAndBuriedVias
})
}
}));
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge);
const sharesNet = (first, second) => first === second || allowSameNetMerges && connectionsShareElectricalNet(srj, first, second);
const allBlockingVias = [...blockingVias, ...terminalVias];
const maximumViaToTraceDistance = allBlockingVias.reduce((maximum, { via }) => Math.max(maximum, via.diameter / 2 + traceWidth / 2 + clearance), traceWidth / 2 + clearance);
const viasByX = allBlockingVias.toSorted((first, second) => first.via.center.x - second.via.center.x);
const getFirstViaAtOrAfterX = (minimumX) => {
let low = 0;
let high = viasByX.length;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if (viasByX[middle].via.center.x < minimumX)
low = middle + 1;
else
high = middle;
}
return low;
};
const segmentIsClear = (params2) => {
const { segment, terminal, acceptedAttemptSegments } = params2;
const connectionName = terminal.connection.connection.name;
const requiredObstacleClearance = segment.width / 2 + clearance;
for (const obstacle of targetLayerObstacleIndex.querySegment(segment, requiredObstacleClearance)) {
if (obstacle.connectedTo.includes(connectionName) || allowSameNetMerges && obstacleSharesElectricalNet(srj, obstacle, connectionName)) {
continue;
}
if (distanceSegmentToObstacle(segment, obstacle) < requiredObstacleClearance - EPSILON7) {
return false;
}
}
for (const blocker of blockingSegments) {
if (sharesNet(connectionName, blocker.connectionName))
continue;
if (distanceSegmentToSegment(segment.start, segment.end, blocker.segment.start, blocker.segment.end) < (segment.width + blocker.segment.width) / 2 + clearance - EPSILON7) {
return false;
}
}
for (const blocker of acceptedAttemptSegments) {
if (sharesNet(connectionName, blocker.connectionName))
continue;
const margin = (segment.width + blocker.segment.width) / 2 + clearance;
if (Math.max(segment.start.x, segment.end.x) + margin < Math.min(blocker.segment.start.x, blocker.segment.end.x) || Math.min(segment.start.x, segment.end.x) - margin > Math.max(blocker.segment.start.x, blocker.segment.end.x) || Math.max(segment.start.y, segment.end.y) + margin < Math.min(blocker.segment.start.y, blocker.segment.end.y) || Math.min(segment.start.y, segment.end.y) - margin > Math.max(blocker.segment.start.y, blocker.segment.end.y))
continue;
if (distanceSegmentToSegment(segment.start, segment.end, blocker.segment.start, blocker.segment.end) < (segment.width + blocker.segment.width) / 2 + clearance - EPSILON7) {
return false;
}
}
const segmentMinX = Math.min(segment.start.x, segment.end.x);
const segmentMaxX = Math.max(segment.start.x, segment.end.x);
const segmentMinY = Math.min(segment.start.y, segment.end.y);
const segmentMaxY = Math.max(segment.start.y, segment.end.y);
for (let viaIndex = getFirstViaAtOrAfterX(segmentMinX - maximumViaToTraceDistance);viaIndex < viasByX.length; viaIndex++) {
const blocker = viasByX[viaIndex];
if (blocker.via.center.x > segmentMaxX + maximumViaToTraceDistance) {
break;
}
if (sharesNet(connectionName, blocker.connectionName))
continue;
const requiredDistance = blocker.via.diameter / 2 + segment.width / 2 + clearance;
if (blocker.via.center.x < segmentMinX - requiredDistance || blocker.via.center.x > segmentMaxX + requiredDistance || blocker.via.center.y < segmentMinY - requiredDistance || blocker.via.center.y > segmentMaxY + requiredDistance) {
continue;
}
if (distancePointToSegment(blocker.via.center, segment.start, segment.end) < requiredDistance - EPSILON7) {
return false;
}
}
return true;
};
const connectorCandidates = (params2) => {
const { terminal, endpoint, acceptedAttemptSegments } = params2;
const candidates = [];
for (let nodeIndex = 0;nodeIndex < nodes.length; nodeIndex++) {
const node = nodes[nodeIndex];
const connectorDistance = distance(endpoint, node.point);
if (connectorDistance > gridStep * CONNECTOR_RADIUS_IN_STEPS)
continue;
for (const points of getConnectorVariants(endpoint, node.point)) {
const segments = getSegments(points, traceWidth, targetLayer);
if (!segments.every((segment) => segmentIsClear({
segment,
terminal,
acceptedAttemptSegments
}))) {
continue;
}
candidates.push({
nodeIndex,
points,
radialDistance: connectorDistance,
length: segments.reduce((total, segment) => total + distance(segment.start, segment.end), 0)
});
}
}
return candidates.toSorted((first, second) => first.radialDistance - second.radialDistance || first.length - second.length || first.nodeIndex - second.nodeIndex).slice(0, MAX_CONNECTOR_COUNT);
};
const visualizeSearch = (params2) => {
const {
terminal,
acceptedAttemptSegments,
expandedPoints,
frontierPoints,
bestPath,
expandedStateCount,
searchBatch,
connectionComplete
} = params2;
return {
title: `Winding ${bus.busId}: ${terminal.connection.connection.name}`,
rects: [
{
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
width: maxX - minX,
height: maxY - minY,
fill: "rgba(0, 0, 0, 0)",
stroke: "rgba(14, 165, 233, 0.9)",
label: "A* search boundary"
}
],
points: [
...sampledGridPoints.map((point) => ({
...point,
color: "rgba(148, 163, 184, 0.22)"
})),
...frontierPoints.map((point) => ({
...point,
color: "rgba(6, 182, 212, 0.75)",
label: "open frontier"
})),
...expandedPoints.map((point) => ({
...point,
color: "rgba(244, 63, 94, 0.8)",
label: "expanded in current step"
})),
{
...terminal.connection.sourcePoint,
color: "#f97316",
label: `source: ${terminal.connection.connection.name}`
},
{
...terminal.exitPoint,
color: "#a855f7",
label: "target exit"
}
],
circles: terminals.map((candidate) => ({
center: candidate.viaPoint,
radius: viaDiameter / 2,
fill: candidate === terminal ? "rgba(250, 204, 21, 0.85)" : "rgba(250, 204, 21, 0.25)",
stroke: candidate === terminal ? "#ca8a04" : "#a16207",
label: candidate === terminal ? "active via" : `reserved via: ${candidate.connection.connection.name}`
})),
lines: [
...acceptedAttemptSegments.map(({ segment, connectionName }) => ({
points: [segment.start, segment.end],
strokeColor: "rgba(34, 197, 94, 0.9)",
strokeWidth: Math.max(traceWidth, gridStep * 0.35),
label: `accepted: ${connectionName}`
})),
{
points: [terminal.viaPoint, terminal.exitPoint],
strokeColor: "rgba(168, 85, 247, 0.45)",
strokeWidth: Math.max(traceWidth * 0.5, gridStep * 0.15),
strokeDash: [gridStep, gridStep],
label: "active via-to-exit search"
},
...bestPath ? [
{
points: [...bestPath],
strokeColor: "#facc15",
strokeWidth: Math.max(traceWidth, gridStep * 0.45),
label: "best path so far"
}
] : []
],
texts: [
{
x: minX,
y: maxY + gridStep * 2,
text: connectionComplete ? `connection complete · ${expandedStateCount.toLocaleString()} states` : `A* batch ${searchBatch} · ${expandedStateCount.toLocaleString()} states`,
color: "#0f172a",
fontSize: Math.max(gridStep * 2.5, 0.5),
anchorSide: "bottom_left"
}
]
};
};
const routeOneSteps = function* (params2) {
const { terminal, acceptedAttemptSegments, laneBias } = params2;
const starts = connectorCandidates({
terminal,
endpoint: terminal.viaPoint,
acceptedAttemptSegments
});
const ends = connectorCandidates({
terminal,
endpoint: terminal.exitPoint,
acceptedAttemptSegments
});
if (starts.length === 0 || ends.length === 0) {
return { points: null, expandedStateCount: 0 };
}
const endByNode = new Map;
for (const end of ends) {
const values = endByNode.get(end.nodeIndex) ?? [];
values.push(end);
endByNode.set(end.nodeIndex, values);
}
const stateCount = nodeCount * 9;
const edgeClearance = new Uint8Array(nodeCount * 8);
const distances = new Float64Array(stateCount).fill(Number.POSITIVE_INFINITY);
const previous = new Int32Array(stateCount).fill(-1);
const heap = new MinHeap;
const heuristic = (point) => {
const deltaX = Math.abs(point.x - terminal.exitPoint.x);
const deltaY = Math.abs(point.y - terminal.exitPoint.y);
return Math.max(deltaX, deltaY) + (Math.SQRT2 - 1) * Math.min(deltaX, deltaY);
};
for (const start of starts) {
const state = start.nodeIndex * 9 + 8;
if (start.length >= distances[state])
continue;
distances[state] = start.length;
const remaining = heuristic(nodes[start.nodeIndex].point);
heap.push({
node: start.nodeIndex,
direction: 8,
score: start.length + remaining
});
}
const directions = [
[1, 0],
[1, 1],
[0, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[0, -1],
[1, -1]
];
const startsByNode = new Map;
for (const start of starts) {
const values = startsByNode.get(start.nodeIndex) ?? [];
values.push(start);
startsByNode.set(start.nodeIndex, values);
}
let bestGoalCost = Number.POSITIVE_INFINITY;
let bestGoalPoints = null;
let expandedStateCount = 0;
let expandedStatesSinceYield = 0;
let searchBatch = 0;
let expandedBatchPoints = [];
while (heap.size > 0 && expandedStateCount < MAX_EXPANDED_STATE_COUNT) {
const current = heap.pop();
if (current.score >= bestGoalCost - EPSILON7)
break;
const state = current.node * 9 + current.direction;
const currentDistance = distances[state];
if (current.score > currentDistance + heuristic(nodes[current.node].point) + EPSILON7)
continue;
expandedStateCount++;
expandedStatesSinceYield++;
if (includeVisualization && expandedStatesSinceYield % 50 === 0) {
expandedBatchPoints.push(nodes[current.node].point);
}
const endConnectors = endByNode.get(current.node);
if (endConnectors) {
const gridPoints = [];
let pathState = state;
while (pathState >= 0) {
gridPoints.push(nodes[Math.floor(pathState / 9)].point);
pathState = previous[pathState];
}
gridPoints.reverse();
let firstState = state;
while (previous[firstState] >= 0) {
firstState = previous[firstState];
}
const startNodeIndex = Math.floor(firstState / 9);
const startConnectors = startsByNode.get(startNodeIndex) ?? [];
const shortestStartLength = Math.min(...startConnectors.map((candidate) => candidate.length));
for (const startConnector of startConnectors) {
for (const endConnector of endConnectors) {
const candidateCost = currentDistance - shortestStartLength + startConnector.length + endConnector.length;
if (candidateCost >= bestGoalCost - EPSILON7)
continue;
const points = compressPath([
...startConnector.points,
...gridPoints.slice(1),
...endConnector.points.toReversed().slice(1)
]);
const segments = getSegments(points, traceWidth, targetLayer);
if (!segments.every(segmentIsStraightOr45Degrees) || !pathHasNoProperSelfCrossing(segments) || !segments.every((segment) => segmentIsClear({
segment,
terminal,
acceptedAttemptSegments
}))) {
continue;
}
bestGoalCost = candidateCost;
bestGoalPoints = points;
}
}
}
const node = nodes[current.node];
for (let directionIndex = 0;directionIndex < directions.length; directionIndex++) {
if (current.direction !== 8) {
const rawDirectionDelta = Math.abs(current.direction - directionIndex);
if (Math.min(rawDirectionDelta, 8 - rawDirectionDelta) > 1) {
continue;
}
}
const [deltaColumn, deltaRow] = directions[directionIndex];
const column = node.column + deltaColumn;
const row = node.row + deltaRow;
if (column < 0 || column >= columnCount || row < 0 || row >= rowCount) {
continue;
}
const nextNode = row * columnCount + column;
const nextPoint = nodes[nextNode].point;
const addsTurn = current.direction !== 8 && current.direction !== directionIndex;
const nextTrack = getPerpendicularAxis(nextPoint, boundaryDirection);
const targetTrack = getPerpendicularAxis(terminal.exitPoint, boundaryDirection);
const lanePenalty = laneBias === 0 ? 0 : laneBias > 0 ? Math.max(0, targetTrack - nextTrack) * 0.2 : Math.max(0, nextTrack - targetTrack) * 0.2;
const nextDistance = currentDistance + (deltaColumn !== 0 && deltaRow !== 0 ? gridStep * Math.SQRT2 : gridStep) + (addsTurn ? gridStep * 0.2 : 0) + lanePenalty;
const nextState = nextNode * 9 + directionIndex;
if (nextDistance >= distances[nextState] - EPSILON7)
continue;
const edgeIndex = current.node * 8 + directionIndex;
if (edgeClearance[edgeIndex] === 0) {
const clear = segmentIsClear({
segment: {
start: node.point,
end: nextPoint,
width: traceWidth,
layer: targetLayer
},
terminal,
acceptedAttemptSegments
});
edgeClearance[edgeIndex] = clear ? 1 : 2;
edgeClearance[nextNode * 8 + (directionIndex + 4) % 8] = clear ? 1 : 2;
}
if (edgeClearance[edgeIndex] === 2)
continue;
distances[nextState] = nextDistance;
previous[nextState] = state;
const remaining = heuristic(nextPoint);
heap.push({
node: nextNode,
direction: directionIndex,
score: nextDistance + remaining
});
}
if (expandedStatesSinceYield >= EXPANDED_STATES_PER_STEP) {
expandedStatesSinceYield = 0;
searchBatch++;
yield {
expandedStateCount,
...includeVisualization ? {
visualization: visualizeSearch({
terminal,
acceptedAttemptSegments,
expandedPoints: expandedBatchPoints,
frontierPoints: heap.sampleEntries(120).map((entry) => nodes[entry.node].point),
bestPath: bestGoalPoints,
expandedStateCount,
searchBatch,
connectionComplete: false
})
} : {}
};
expandedBatchPoints = [];
}
}
return { points: bestGoalPoints, expandedStateCount };
};
const targetOrderedTerminals = terminals.toSorted((first, second) => {
const axisDifference = getPerpendicularAxis(first.exitPoint, boundaryDirection) - getPerpendicularAxis(second.exitPoint, boundaryDirection);
return axisDifference || first.connection.connection.name.localeCompare(second.connection.connection.name);
});
const viaTracks = terminals.map((terminal) => getPerpendicularAxis(terminal.viaPoint, boundaryDirection));
const targetTracks = targetOrderedTerminals.map((terminal) => getPerpendicularAxis(terminal.exitPoint, boundaryDirection));
const meanViaTrack = viaTracks.reduce((sum, track) => sum + track, 0) / viaTracks.length;
const meanTargetTrack = targetTracks.reduce((sum, track) => sum + track, 0) / targetTracks.length;
const viasAreBeforeTargets = Math.max(...viaTracks) < Math.min(...targetTracks) - EPSILON7;
const viasAreAfterTargets = Math.min(...viaTracks) > Math.max(...targetTracks) + EPSILON7;
const laneBiases = preferTargetDirectedLaneBias ? viasAreBeforeTargets ? [0, 1, -1] : viasAreAfterTargets ? [0, -1, 1] : bus.direction === boundaryDirection && meanTargetTrack > meanViaTrack + EPSILON7 ? [1, 0, -1] : bus.direction === boundaryDirection && meanTargetTrack < meanViaTrack - EPSILON7 ? [-1, 0, 1] : [0, 1, -1] : viasAreBeforeTargets ? [1, 0, -1] : viasAreAfterTargets ? [-1, 0, 1] : [0, 1, -1];
const initialRouteOrderFactories = [];
if (alignGridToPads && preferTargetDirectedLaneBias && viasAreBeforeTargets) {
initialRouteOrderFactories.push(() => targetOrderedTerminals);
}
if (viasAreBeforeTargets) {
initialRouteOrderFactories.push(() => [
...targetOrderedTerminals.slice(1),
targetOrderedTerminals[0]
]);
} else if (viasAreAfterTargets) {
initialRouteOrderFactories.push(() => [...targetOrderedTerminals].reverse());
if (includeReverseTargetRotation && targetOrderedTerminals.length > 2) {
initialRouteOrderFactories.push(() => [
...targetOrderedTerminals.slice(0, -1).toReversed(),
targetOrderedTerminals.at(-1)
]);
}
}
initialRouteOrderFactories.push(...preferTargetDirectedLaneBias && bus.direction === boundaryDirection && meanTargetTrack < meanViaTrack - EPSILON7 && !viasAreBeforeTargets && !viasAreAfterTargets ? [
() => [...targetOrderedTerminals].reverse(),
() => targetOrderedTerminals
] : [
() => targetOrderedTerminals,
() => [...targetOrderedTerminals].reverse()
], () => terminals.toSorted((first, second) => first.viaPoint.x - second.viaPoint.x || first.viaPoint.y - second.viaPoint.y), () => terminals.toSorted((first, second) => second.viaPoint.x - first.viaPoint.x || second.viaPoint.y - first.viaPoint.y), () => terminals.toSorted((first, second) => first.viaPoint.y - second.viaPoint.y || first.viaPoint.x - second.viaPoint.x), () => terminals.toSorted((first, second) => second.viaPoint.y - first.viaPoint.y || second.viaPoint.x - first.viaPoint.x));
const maximumRouteOrderCount = maximumRouteOrderAttempts === undefined ? undefined : Math.ceil(maximumRouteOrderAttempts / laneBiases.length);
const routeOrders = iterateUniqueRouteOrders({
initialOrderFactories: initialRouteOrderFactories,
rotationBase: targetOrderedTerminals,
getItemKey: (terminal) => terminal.connection.connection.name,
maximumOrderCount: maximumRouteOrderCount
});
const pendingRouteOrders = [];
const seenAdaptiveOrders = new Set;
const adaptiveRouteOrders = function* () {
while (true) {
const pending = pendingRouteOrders.shift();
const next = pending ? { done: false, value: pending } : routeOrders.next();
if (next.done)
return;
const key = next.value.map((terminal) => terminal.connection.connection.name).join("|");
if (seenAdaptiveOrders.has(key))
continue;
seenAdaptiveOrders.add(key);
yield next.value;
}
};
const alternatives = [];
const seenAlternativeKeys = new Set;
let routeOrderAttemptCount = 0;
for (const routeOrder of adaptiveRouteOrders()) {
for (const laneBias of laneBiases) {
if (maximumRouteOrderAttempts !== undefined && routeOrderAttemptCount >= maximumRouteOrderAttempts) {
return alternatives;
}
routeOrderAttemptCount++;
const acceptedAttemptSegments = [];
const routedPointsByConnectionName = new Map;
let failed = false;
for (let terminalIndex = 0;terminalIndex < routeOrder.length; terminalIndex++) {
const terminal = routeOrder[terminalIndex];
const connectionSteps = routeOneSteps({
terminal,
acceptedAttemptSegments,
laneBias
});
let connectionResult = connectionSteps.next();
let searchBatch = 0;
let expandedStateCount = 0;
while (!connectionResult.done) {
expandedStateCount = connectionResult.value.expandedStateCount;
yield {
phase: "route-connection",
routeOrderAttempt: routeOrderAttemptCount,
connectionIndex: terminalIndex,
connectionCount: routeOrder.length,
connectionName: terminal.connection.connection.name,
searchBatch: ++searchBatch,
expandedStateCount,
connectionComplete: false,
...connectionResult.value.visualization ? { visualization: connectionResult.value.visualization } : {}
};
connectionResult = connectionSteps.next();
}
const { points, expandedStateCount: finalExpandedStateCount } = connectionResult.value;
expandedStateCount = finalExpandedStateCount;
yield {
phase: "route-connection",
routeOrderAttempt: routeOrderAttemptCount,
connectionIndex: terminalIndex,
connectionCount: routeOrder.length,
connectionName: terminal.connection.connection.name,
searchBatch,
expandedStateCount,
connectionComplete: true,
...includeVisualization ? {
visualization: visualizeSearch({
terminal,
acceptedAttemptSegments,
expandedPoints: [],
frontierPoints: [],
bestPath: points,
expandedStateCount,
searchBatch,
connectionComplete: true
})
} : {}
};
if (!points) {
if (adaptiveRouteOrder && maximumRouteOrderAttempts !== undefined && terminalIndex > 0) {
const others = routeOrder.filter((candidate) => candidate !== terminal);
pendingRouteOrders.push([terminal, ...others]);
}
failed = true;
break;
}
const connectionName = terminal.connection.connection.name;
routedPointsByConnectionName.set(connectionName, points);
acceptedAttemptSegments.push(...getSegments(points, traceWidth, targetLayer).map((segment) => ({
connectionName,
segment
})));
}
if (failed)
continue;
const plans = terminals.map((terminal) => {
const targetLayerPoints = routedPointsByConnectionName.get(terminal.connection.connection.name);
if (!targetLayerPoints) {
throw new Error(`FanoutSolver: via-minimal winding route omitted "${terminal.connection.connection.name}"`);
}
return buildPlan({
bus,
terminal,
targetLayer,
targetLayerPoints,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
allowBlindAndBuriedVias
});
});
const alternativeKey = plans.map((plan) => plan.segments.map((segment) => `${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`).join(";")).join("|");
if (seenAlternativeKeys.has(alternativeKey))
continue;
seenAlternativeKeys.add(alternativeKey);
alternatives.push(plans);
if (alternatives.length >= maximumAlternatives) {
return alternatives;
}
}
}
return alternatives;
}
function routeViaMinimalWindingAlternatives(params, maximumAlternatives = 1) {
const steps = routeViaMinimalWindingAlternativesSteps(params, maximumAlternatives);
let result = steps.next();
while (!result.done)
result = steps.next();
return result.value;
}
function routeViaMinimalWinding(params) {
return routeViaMinimalWindingAlternatives(params, 1)[0] ?? null;
}
// node_modules/@tscircuit/fanout-solver/lib/route-bus.ts
function allowsViaInPad(srj) {
return srj.allowViaInPad === true;
}
function isHorizontal(direction) {
return direction === "left" || direction === "right";
}
function directionSign(direction) {
return direction === "right" || direction === "up" ? 1 : -1;
}
function getAxis(point, direction) {
return isHorizontal(direction) ? point.x : point.y;
}
function getPerpendicularAxis2(point, direction) {
return isHorizontal(direction) ? point.y : point.x;
}
function getCornerSide(bus) {
return getCornerBandSide(bus.exitEdge, bus.preferredExit);
}
function getLocalCornerSide(bus) {
return getCornerBandSide(getExitEdgeForDirection(bus.direction), bus.preferredExit);
}
function busUsesCoordinatedWindingChannel(bus) {
return Boolean(bus.exitEdge && bus.termination.type === "boundary" && bus.connections.length > 0 && bus.connections.every((connection) => connection.hasExplicitLayeredExitTarget === true));
}
function getStableConnectionIdentity(connection) {
if ("source_trace_id" in connection && typeof connection.source_trace_id === "string") {
return connection.source_trace_id;
}
return connection.name;
}
function getWindingTargetOrders(params) {
const { bus, boundaryDirection, layerNames, targetLayer } = params;
const getTargetLayer = (candidate) => candidate.exitTargetPoint?.layer ?? getPointLayer2(candidate.targetPoint);
const compareWithinLayer = (first, second) => {
const axisDifference = getPerpendicularAxis2(first.exitTargetPoint ?? first.targetPoint, boundaryDirection) - getPerpendicularAxis2(second.exitTargetPoint ?? second.targetPoint, boundaryDirection);
if (axisDifference !== 0)
return axisDifference;
const firstStableId = getStableConnectionIdentity(first.connection);
const secondStableId = getStableConnectionIdentity(second.connection);
const stableIdentityDifference = firstStableId.localeCompare(secondStableId);
if (stableIdentityDifference !== 0)
return stableIdentityDifference;
return first.connection.name.localeCompare(second.connection.name) || first.connectionIndex - second.connectionIndex;
};
const legacyOrderedConnections = bus.connections.toSorted((first, second) => {
const axisDifference = getPerpendicularAxis2(first.exitTargetPoint ?? first.targetPoint, boundaryDirection) - getPerpendicularAxis2(second.exitTargetPoint ?? second.targetPoint, boundaryDirection);
if (axisDifference !== 0)
return axisDifference;
return first.connection.name.localeCompare(second.connection.name) || getStableConnectionIdentity(first.connection).localeCompare(getStableConnectionIdentity(second.connection)) || layerNames.indexOf(getTargetLayer(first)) - layerNames.indexOf(getTargetLayer(second)) || first.connectionIndex - second.connectionIndex;
});
const connectionsByLayer = new Map;
for (const candidate of bus.connections) {
const layer = getTargetLayer(candidate);
const layerConnections = connectionsByLayer.get(layer) ?? [];
layerConnections.push(candidate);
connectionsByLayer.set(layer, layerConnections);
}
const orderedLayers = [...connectionsByLayer.keys()].toSorted((first, second) => Number(second === targetLayer) - Number(first === targetLayer) || layerNames.indexOf(first) - layerNames.indexOf(second) || first.localeCompare(second));
const layerOrderByName = new Map(orderedLayers.map((layer, layerOrder) => [layer, layerOrder]));
const rankWithinLayerByConnectionIndex = new Map;
for (const layer of orderedLayers) {
for (const [rank, candidate] of connectionsByLayer.get(layer).toSorted(compareWithinLayer).entries()) {
rankWithinLayerByConnectionIndex.set(candidate.connectionIndex, rank);
}
}
const canonicalOrderedConnections = bus.connections.toSorted((first, second) => (rankWithinLayerByConnectionIndex.get(first.connectionIndex) ?? 0) - (rankWithinLayerByConnectionIndex.get(second.connectionIndex) ?? 0) || (layerOrderByName.get(getTargetLayer(first)) ?? 0) - (layerOrderByName.get(getTargetLayer(second)) ?? 0) || compareWithinLayer(first, second));
const candidateOrders = [canonicalOrderedConnections];
for (let index = 0;index + 1 < canonicalOrderedConnections.length; index++) {
const first = canonicalOrderedConnections[index];
const second = canonicalOrderedConnections[index + 1];
if (getTargetLayer(first) === getTargetLayer(second))
continue;
const adjacentExtension = [...canonicalOrderedConnections];
adjacentExtension[index] = second;
adjacentExtension[index + 1] = first;
candidateOrders.push(adjacentExtension);
}
candidateOrders.push(legacyOrderedConnections);
const seenOrders = new Set;
const orders = candidateOrders.filter((order) => {
const key = order.map((candidate) => candidate.connectionIndex).join(",");
if (seenOrders.has(key))
return false;
seenOrders.add(key);
return true;
});
return { orders, legacyOrder: legacyOrderedConnections };
}
function getWindingTargetRank(params) {
const { connection, windingOrderIndex = 0 } = params;
const { orders, legacyOrder } = getWindingTargetOrders(params);
const orderedConnections = params.windingOrderIndex === undefined ? legacyOrder : orders[windingOrderIndex] ?? orders[0] ?? legacyOrder;
const rank = orderedConnections.findIndex((candidate) => candidate.connectionIndex === connection.connectionIndex);
if (rank < 0) {
throw new Error(`FanoutSolver: connection "${connection.connection.name}" is missing from winding order`);
}
return { rank, connectionCount: orderedConnections.length };
}
function getWindingCrossoverLayer(params) {
const { bus, escapeLayer } = params;
return (bus.routableEscapeLayers ?? bus.allowedLayers)?.find((layer) => layer !== escapeLayer);
}
function getBoundaryTargetTrack(params) {
const requestedTrack = getPerpendicularAxis2(params.connection.exitTargetPoint ?? params.connection.targetPoint, params.boundaryDirection);
const boundaryMinimum = isHorizontal(params.boundaryDirection) ? params.bus.sharedBoundary.minY : params.bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal(params.boundaryDirection) ? params.bus.sharedBoundary.maxY : params.bus.sharedBoundary.maxX;
return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack));
}
function getCornerTargetTrack(params) {
const {
bus,
connection,
cornerExitLaneOffset,
traceWidth,
viaDiameter,
clearance,
layerNames,
targetLayer,
windingOrderIndex,
cornerBandTargetTrackOffset = 0
} = params;
const side = getCornerSide(bus);
if (!side || !bus.exitEdge) {
return getPerpendicularAxis2(connection.exitTargetPoint ?? connection.targetPoint, bus.direction);
}
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge);
const boundaryMinimum = isHorizontal(boundaryDirection) ? bus.sharedBoundary.minY : bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal(boundaryDirection) ? bus.sharedBoundary.maxY : bus.sharedBoundary.maxX;
const pitch = Math.max(traceWidth + clearance, viaDiameter + clearance);
const baseBandCenter = boundaryMinimum + (boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75) + cornerBandTargetTrackOffset;
const windingTarget = busUsesCoordinatedWindingChannel(bus) ? getWindingTargetRank({
bus,
connection,
boundaryDirection,
layerNames,
targetLayer,
windingOrderIndex
}) : undefined;
const bandConnectionCount = Math.max(bus.connections.length, bus.cornerBandConnectionCount ?? bus.connections.length, cornerExitLaneOffset + (windingTarget?.connectionCount ?? 0));
const firstTrack = baseBandCenter - (bandConnectionCount - 1) * pitch / 2;
const rank = windingTarget?.rank ?? getConnectionRank(bus, connection);
const globalSlot = cornerExitLaneOffset + rank;
const reverseSlotOrder = !windingTarget && side === "maximum" === directionSign(boundaryDirection) > 0;
const orientedSlot = reverseSlotOrder ? bandConnectionCount - 1 - globalSlot : globalSlot;
return firstTrack + orientedSlot * pitch;
}
function getCornerLaneOffsets(bus, acceptedPlans) {
const side = getCornerSide(bus);
if (!side || !bus.exitEdge) {
return { exit: 0, localChannel: 0, boundaryChannel: 0 };
}
const cornerPlans = acceptedPlans.filter((plan) => plan.exitEdge && plan.cornerBandSide !== undefined);
const plansOnExitEdge = cornerPlans.filter((plan) => plan.exitEdge === bus.exitEdge && plan.cornerBandSide === side);
const plansOnLocalEdge = cornerPlans.filter((plan) => plan.direction === bus.direction);
return {
exit: plansOnExitEdge.length,
localChannel: plansOnLocalEdge.length,
boundaryChannel: plansOnExitEdge.length
};
}
function makePoint(axis, perpendicularAxis, direction) {
return isHorizontal(direction) ? { x: axis, y: perpendicularAxis } : { x: perpendicularAxis, y: axis };
}
function getExitAxis(bus, direction = bus.direction) {
switch (direction) {
case "right":
return bus.sharedBoundary.maxX;
case "left":
return bus.sharedBoundary.minX;
case "up":
return bus.sharedBoundary.maxY;
case "down":
return bus.sharedBoundary.minY;
}
}
function getDirectionalPitch(bus) {
return isHorizontal(bus.direction) ? bus.pitchX : bus.pitchY;
}
function getPerpendicularPitch(bus) {
return isHorizontal(bus.direction) ? bus.pitchY : bus.pitchX;
}
function chamferOrthogonalCorners(points, requestedChamfer) {
if (points.length < 3)
return [...points];
const output = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = points[index - 1];
const current = points[index];
const next = points[index + 1];
const incoming = { x: current.x - previous.x, y: current.y - previous.y };
const outgoing = { x: next.x - current.x, y: next.y - current.y };
const incomingLength = Math.hypot(incoming.x, incoming.y);
const outgoingLength = Math.hypot(outgoing.x, outgoing.y);
const incomingIsAxisAligned = Math.abs(incoming.x) <= 0.000000001 || Math.abs(incoming.y) <= 0.000000001;
const outgoingIsAxisAligned = Math.abs(outgoing.x) <= 0.000000001 || Math.abs(outgoing.y) <= 0.000000001;
const isOrthogonal = Math.abs(incoming.x * outgoing.x + incoming.y * outgoing.y) <= 0.000000001;
if (incomingLength <= 0.000000001 || outgoingLength <= 0.000000001 || !incomingIsAxisAligned || !outgoingIsAxisAligned || !isOrthogonal) {
output.push(current);
continue;
}
const chamfer = Math.min(requestedChamfer, incomingLength / 3, outgoingLength / 3);
output.push({
x: current.x - incoming.x / incomingLength * chamfer,
y: current.y - incoming.y / incomingLength * chamfer
});
output.push({
x: current.x + outgoing.x / outgoingLength * chamfer,
y: current.y + outgoing.y / outgoingLength * chamfer
});
}
output.push(points.at(-1));
return output.filter((point, index) => index === 0 || distance(point, output[index - 1]) > 0.000000001);
}
function getStraightOr45ConnectorVariants(start, end) {
const deltaX = end.x - start.x;
const deltaY = end.y - start.y;
const absoluteX = Math.abs(deltaX);
const absoluteY = Math.abs(deltaY);
if (absoluteX <= 0.000000001 || absoluteY <= 0.000000001 || Math.abs(absoluteX - absoluteY) <= 0.000000001) {
return [[start, end]];
}
if (absoluteX > absoluteY) {
return [
[start, { x: start.x + Math.sign(deltaX) * absoluteY, y: end.y }, end],
[start, { x: end.x - Math.sign(deltaX) * absoluteY, y: start.y }, end]
];
}
return [
[start, { x: end.x, y: start.y + Math.sign(deltaY) * absoluteX }, end],
[start, { x: start.x, y: end.y - Math.sign(deltaY) * absoluteX }, end]
];
}
function getDepthInRows(bus) {
const directionalCoordinates = (isHorizontal(bus.direction) ? bus.xCoordinates : bus.yCoordinates).toSorted((a, b) => a - b);
const averageDirectionalSource = bus.connections.reduce((sum, candidate) => sum + getAxis(candidate.sourcePoint, bus.direction), 0) / bus.connections.length;
const outwardCoordinate = directionSign(bus.direction) > 0 ? directionalCoordinates.at(-1) : directionalCoordinates[0];
return Math.abs(outwardCoordinate - averageDirectionalSource) / getDirectionalPitch(bus);
}
function busIsOnOutwardComponentEdge(bus) {
const directionalCoordinates = isHorizontal(bus.direction) ? bus.xCoordinates : bus.yCoordinates;
const averageDirectionalSource = bus.connections.reduce((sum, connection) => sum + getAxis(connection.sourcePoint, bus.direction), 0) / bus.connections.length;
const outwardCoordinate = directionSign(bus.direction) > 0 ? Math.max(...directionalCoordinates) : Math.min(...directionalCoordinates);
return Math.abs(averageDirectionalSource - outwardCoordinate) < 0.000001;
}
function getConnectionRank(bus, connection) {
const connectionRank = [...bus.connections].sort((a, b) => getPerpendicularAxis2(a.sourcePoint, bus.direction) - getPerpendicularAxis2(b.sourcePoint, bus.direction)).findIndex((candidate) => candidate.connectionIndex === connection.connectionIndex);
if (connectionRank < 0) {
throw new Error(`FanoutSolver: connection "${connection.connection.name}" is missing from bus "${bus.busId}"`);
}
return connectionRank;
}
function getRoutableBounds(srjBounds, sharedBoundary) {
return {
minX: Math.min(srjBounds.minX, sharedBoundary.minX),
maxX: Math.max(srjBounds.maxX, sharedBoundary.maxX),
minY: Math.min(srjBounds.minY, sharedBoundary.minY),
maxY: Math.max(srjBounds.maxY, sharedBoundary.maxY)
};
}
function pointIsInsideBounds(point, bounds) {
return point.x >= bounds.minX - 0.000001 && point.x <= bounds.maxX + 0.000001 && point.y >= bounds.minY - 0.000001 && point.y <= bounds.maxY + 0.000001;
}
function getTracksInSpan(minimum, maximum, traceWidth, clearance, kind) {
const freeWidth = maximum - minimum;
const trackCount = Math.floor((freeWidth - clearance) / (traceWidth + clearance) + 0.000000001);
if (trackCount < 1)
return [];
const usedWidth = trackCount * traceWidth + (trackCount - 1) * clearance;
const firstTrack = minimum + (freeWidth - usedWidth) / 2 + traceWidth / 2;
return Array.from({ length: trackCount }, (_, index) => ({
value: firstTrack + index * (traceWidth + clearance),
kind
}));
}
function getTrackCandidates(params) {
const { bus, connection, preferredTrack, traceWidth, clearance } = params;
const coordinates = (isHorizontal(bus.direction) ? bus.yCoordinates : bus.xCoordinates).toSorted((a, b) => a - b);
const obstacleHalfSize = Math.max(...bus.componentObstacles.map((obstacle) => isHorizontal(bus.direction) ? obstacle.height / 2 : obstacle.width / 2));
const boundaryMinimum = isHorizontal(bus.direction) ? bus.sharedBoundary.minY : bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal(bus.direction) ? bus.sharedBoundary.maxY : bus.sharedBoundary.maxX;
const maximumJog = boundaryMaximum - boundaryMinimum;
const ladderMinimum = boundaryMinimum;
const ladderMaximum = boundaryMaximum;
const tracks = [
{ value: preferredTrack, kind: "preferred" },
...getTracksInSpan(ladderMinimum, coordinates[0] - obstacleHalfSize, traceWidth, clearance, "margin")
];
for (let index = 0;index < coordinates.length; index++) {
tracks.push({ value: coordinates[index], kind: "corridor" });
if (index < coordinates.length - 1) {
tracks.push(...getTracksInSpan(coordinates[index] + obstacleHalfSize, coordinates[index + 1] - obstacleHalfSize, traceWidth, clearance, "gap"));
}
}
tracks.push(...getTracksInSpan(coordinates.at(-1) + obstacleHalfSize, ladderMaximum, traceWidth, clearance, "margin"));
const sourceTrack = getPerpendicularAxis2(connection.sourcePoint, bus.direction);
const componentCenter = (coordinates[0] + coordinates.at(-1)) / 2;
return tracks.filter((track) => Math.abs(track.value - sourceTrack) <= maximumJog + 0.000000001).filter((track, index, candidates) => candidates.findIndex((candidate) => Math.abs(candidate.value - track.value) < 0.000000001) === index).sort((a, b) => Math.abs(a.value - preferredTrack) - Math.abs(b.value - preferredTrack) - (Math.abs(a.value - componentCenter) - Math.abs(b.value - componentCenter)) * 0.001);
}
function getPreferredTrack(params) {
const preferredTrack = getCornerSide(params.bus) || busUsesCoordinatedWindingChannel(params.bus) ? getPerpendicularAxis2(params.connection.sourcePoint, params.bus.direction) : getPerpendicularAxis2(params.connection.exitTargetPoint ?? params.connection.targetPoint, params.bus.direction);
const boundaryMinimum = isHorizontal(params.bus.direction) ? params.bus.sharedBoundary.minY : params.bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal(params.bus.direction) ? params.bus.sharedBoundary.maxY : params.bus.sharedBoundary.maxX;
return Math.max(boundaryMinimum + params.traceWidth / 2, Math.min(boundaryMaximum - params.traceWidth / 2, preferredTrack));
}
function getLegacyPreferredTrack(params) {
const {
bus,
connection,
targetUsesVia,
interstitialEscape,
compactBusTracks,
traceWidth,
viaDiameter,
clearance
} = params;
const perpendicularCoordinates = (isHorizontal(bus.direction) ? bus.yCoordinates : bus.xCoordinates).toSorted((a, b) => a - b);
const sourceTrack = getPerpendicularAxis2(connection.sourcePoint, bus.direction);
if (compactBusTracks) {
const connectionRank2 = getConnectionRank(bus, connection);
const componentCenter = (perpendicularCoordinates[0] + perpendicularCoordinates.at(-1)) / 2;
return componentCenter + (connectionRank2 - (bus.connections.length - 1) / 2) * (traceWidth + clearance);
}
if (!targetUsesVia || !interstitialEscape)
return sourceTrack;
const depthInRows = getDepthInRows(bus);
const trackPitch = traceWidth + clearance;
const halfConnectionCount = Math.ceil(bus.connections.length / 2);
const sideBandWidth = (halfConnectionCount - 1) * trackPitch;
const bandSeparation = viaDiameter / 2 + traceWidth / 2 + clearance + 0.001;
const depthIndex = Math.round(depthInRows) + 1;
const nearOffset = depthIndex * bandSeparation + (depthIndex - 1) * sideBandWidth;
const connectionRank = getConnectionRank(bus, connection);
const componentMinimum = perpendicularCoordinates[0];
const componentMaximum = perpendicularCoordinates.at(-1);
const requestedTrack = connectionRank < halfConnectionCount ? componentMinimum - nearOffset - (halfConnectionCount - connectionRank - 1) * trackPitch : componentMaximum + nearOffset + (connectionRank - halfConnectionCount) * trackPitch;
const boundaryMinimum = isHorizontal(bus.direction) ? bus.sharedBoundary.minY : bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal(bus.direction) ? bus.sharedBoundary.maxY : bus.sharedBoundary.maxX;
return Math.max(boundaryMinimum + traceWidth / 2, Math.min(boundaryMaximum - traceWidth / 2, requestedTrack));
}
function getConnectionOrders(bus) {
const sign = directionSign(bus.direction);
const outwardFirst = [...bus.connections].sort((a, b) => {
const directionalDifference = sign * (getAxis(b.sourcePoint, bus.direction) - getAxis(a.sourcePoint, bus.direction));
if (Math.abs(directionalDifference) > 0.000001) {
return directionalDifference;
}
return getPerpendicularAxis2(a.sourcePoint, bus.direction) - getPerpendicularAxis2(b.sourcePoint, bus.direction);
});
const perpendicularFirst = [...bus.connections].sort((a, b) => getPerpendicularAxis2(a.sourcePoint, bus.direction) - getPerpendicularAxis2(b.sourcePoint, bus.direction) || sign * (getAxis(b.sourcePoint, bus.direction) - getAxis(a.sourcePoint, bus.direction)));
const orders = [
outwardFirst,
[...outwardFirst].reverse(),
perpendicularFirst,
[...perpendicularFirst].reverse()
];
for (let offset = 1;offset < Math.min(outwardFirst.length, 8); offset++) {
orders.push([
...outwardFirst.slice(offset),
...outwardFirst.slice(0, offset)
]);
}
return orders;
}
function appendSegment(segments, start, end, width, layer) {
if (distance(start, end) < 0.000000001)
return;
segments.push({ start, end, width, layer });
}
function chamferOrthogonalPolyline2(points, requestedChamfer) {
if (points.length < 3)
return points;
const chamfered = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = points[index - 1];
const corner = points[index];
const next = points[index + 1];
const incomingLength = distance(previous, corner);
const outgoingLength = distance(corner, next);
if (incomingLength < 0.000000001 || outgoingLength < 0.000000001)
continue;
const incomingUnit = {
x: (corner.x - previous.x) / incomingLength,
y: (corner.y - previous.y) / incomingLength
};
const outgoingUnit = {
x: (next.x - corner.x) / outgoingLength,
y: (next.y - corner.y) / outgoingLength
};
const dot = incomingUnit.x * outgoingUnit.x + incomingUnit.y * outgoingUnit.y;
if (Math.abs(dot) > 0.000001) {
chamfered.push(corner);
continue;
}
const chamfer = Math.min(requestedChamfer, incomingLength / 2, outgoingLength / 2);
chamfered.push({
x: corner.x - incomingUnit.x * chamfer,
y: corner.y - incomingUnit.y * chamfer
});
chamfered.push({
x: corner.x + outgoingUnit.x * chamfer,
y: corner.y + outgoingUnit.y * chamfer
});
}
chamfered.push(points.at(-1));
return chamfered;
}
function getInitialViaPoint(params) {
const {
preparedConnection,
bus,
targetLayer,
traceWidth,
viaDiameter,
clearance,
viaHandedness
} = params;
const sourcePoint = {
x: preparedConnection.sourcePoint.x,
y: preparedConnection.sourcePoint.y
};
const targetUsesVia = targetLayer !== preparedConnection.sourceLayer;
const directionalPitch = getDirectionalPitch(bus);
const perpendicularPitch = getPerpendicularPitch(bus);
const directionalPadSize = isHorizontal(bus.direction) ? preparedConnection.sourceObstacle.width : preparedConnection.sourceObstacle.height;
const initialEscapeDistance = targetUsesVia && !busIsOnOutwardComponentEdge(bus) ? directionalPadSize >= directionalPitch ? directionalPadSize / 2 + viaDiameter / 2 + clearance + 0.001 : directionalPitch * 0.5 : !targetUsesVia ? directionalPadSize / 2 + traceWidth / 2 + clearance + 0.001 : Math.max(directionalPitch * 0.5, directionalPadSize / 2 + (targetUsesVia ? viaDiameter : traceWidth) / 2 + clearance + 0.001);
const viaAxis = getAxis(sourcePoint, bus.direction) + directionSign(bus.direction) * initialEscapeDistance;
const viaPerpendicularAxis = getPerpendicularAxis2(sourcePoint, bus.direction) + viaHandedness * perpendicularPitch * 0.5;
return makePoint(viaAxis, viaPerpendicularAxis, bus.direction);
}
function buildPlan2(params) {
const {
preparedConnection,
bus,
targetLayer,
track,
exitAxis,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
viaHandedness,
interstitialEscape,
spreadLaneIndex,
cornerExitLaneOffset,
cornerLocalChannelLaneOffset,
cornerBoundaryChannelLaneOffset,
clearance,
terminateAtVia,
allowBlindAndBuriedVias,
initialViaPoint,
sourceEscapePath,
cornerBandTargetTrackOffset
} = params;
const sourcePoint = {
x: preparedConnection.sourcePoint.x,
y: preparedConnection.sourcePoint.y
};
const escapeLayer = targetLayer;
const windingCrossoverLayer = busUsesCoordinatedWindingChannel(bus) ? getWindingCrossoverLayer({
bus,
escapeLayer
}) : undefined;
const usesLayeredWindingChannel = Boolean(windingCrossoverLayer);
const sign = directionSign(bus.direction);
const directionalPitch = getDirectionalPitch(bus);
const requestedViaPoint = sourceEscapePath?.at(-1);
const viaPoint = requestedViaPoint !== undefined ? { x: requestedViaPoint.x, y: requestedViaPoint.y } : initialViaPoint === undefined ? getInitialViaPoint({
preparedConnection,
bus,
targetLayer,
traceWidth,
viaDiameter,
clearance,
viaHandedness
}) : { x: initialViaPoint.x, y: initialViaPoint.y };
const resolvedSourceEscapePath = sourceEscapePath ? sourceEscapePath.map((point) => ({ x: point.x, y: point.y })) : [sourcePoint, viaPoint];
if (resolvedSourceEscapePath.length < 2 || distance(resolvedSourceEscapePath[0], sourcePoint) > 0.000000001 || distance(resolvedSourceEscapePath.at(-1), viaPoint) > 0.000000001) {
throw new Error(`FanoutSolver: source escape path for "${preparedConnection.connection.name}" must run from its source point to its via`);
}
const viaAxis = getAxis(viaPoint, bus.direction);
const viaPerpendicularAxis = getPerpendicularAxis2(viaPoint, bus.direction);
const spreadLaneDistance = viaDiameter / 2 + traceWidth / 2 + clearance + 0.001 + spreadLaneIndex * (traceWidth + clearance);
const useNestedSpread = interstitialEscape && directionalPitch >= spreadLaneDistance + viaDiameter / 2 + clearance;
const spreadPoint = useNestedSpread ? makePoint(viaAxis + sign * spreadLaneDistance, viaPerpendicularAxis, bus.direction) : viaPoint;
const targetLayerDoglegAxis = useNestedSpread ? getAxis(spreadPoint, bus.direction) : viaAxis + sign * Math.abs(track - viaPerpendicularAxis);
const doglegPoint = makePoint(targetLayerDoglegAxis, track, bus.direction);
const cornerSide = getCornerSide(bus);
const boundaryDirection = bus.exitEdge ? getDirectionForExitEdge(bus.exitEdge) : bus.direction;
const boundarySign = directionSign(boundaryDirection);
const boundaryExitAxis = getExitAxis(bus, boundaryDirection);
const boundaryTargetTrack = cornerSide && bus.exitEdge ? getCornerTargetTrack({
bus,
connection: preparedConnection,
cornerExitLaneOffset,
traceWidth,
viaDiameter,
clearance,
layerNames,
targetLayer,
cornerBandTargetTrackOffset
}) : usesLayeredWindingChannel ? getBoundaryTargetTrack({
bus,
connection: preparedConnection,
boundaryDirection
}) : track;
const connectionRank = getConnectionRank(bus, preparedConnection);
const localCornerSide = getLocalCornerSide(bus);
const localChannelLaneIndex = localCornerSide === "maximum" ? cornerLocalChannelLaneOffset + connectionRank : cornerLocalChannelLaneOffset + bus.connections.length - 1 - connectionRank;
const globalBoundarySlot = cornerBoundaryChannelLaneOffset + connectionRank;
const boundaryBandConnectionCount = Math.max(bus.connections.length, bus.cornerBandConnectionCount ?? bus.connections.length);
const boundaryChannelLaneIndex = boundarySign > 0 ? globalBoundarySlot : boundaryBandConnectionCount - 1 - globalBoundarySlot;
const channelPitch = usesLayeredWindingChannel ? viaDiameter / 2 + traceWidth / 2 + clearance : traceWidth + clearance;
const channelInset = (laneIndex) => viaDiameter / 2 + traceWidth / 2 + clearance + laneIndex * channelPitch;
const localChannelAxis = getExitAxis(bus, bus.direction) - sign * channelInset(localChannelLaneIndex);
const boundaryChannelAxis = boundaryExitAxis - boundarySign * channelInset(boundaryChannelLaneIndex);
const localChannelSourcePoint = makePoint(localChannelAxis, track, bus.direction);
const localChannelTargetPoint = makePoint(boundaryChannelAxis, localChannelAxis, boundaryDirection);
const boundaryChannelTargetPoint = makePoint(boundaryChannelAxis, boundaryTargetTrack, boundaryDirection);
const windingInputTransitionPoint = isHorizontal(bus.direction) !== isHorizontal(boundaryDirection) ? localChannelTargetPoint : makePoint(boundaryChannelAxis, track, boundaryDirection);
const exitPoint = terminateAtVia ? viaPoint : cornerSide || usesLayeredWindingChannel ? makePoint(boundaryExitAxis, boundaryTargetTrack, boundaryDirection) : makePoint(exitAxis, track, bus.direction);
const segments = [];
const route = [];
route.push({
route_type: "wire",
x: sourcePoint.x,
y: sourcePoint.y,
width: traceWidth,
layer: preparedConnection.sourceLayer,
start_pcb_port_id: preparedConnection.sourcePoint.pcb_port_id
});
for (let pointIndex = 1;pointIndex < resolvedSourceEscapePath.length; pointIndex++) {
const previousPoint = resolvedSourceEscapePath[pointIndex - 1];
const point = resolvedSourceEscapePath[pointIndex];
appendSegment(segments, previousPoint, point, traceWidth, preparedConnection.sourceLayer);
if (distance(previousPoint, point) <= 0.000000001)
continue;
route.push({
route_type: "wire",
x: point.x,
y: point.y,
width: traceWidth,
layer: preparedConnection.sourceLayer
});
}
let via;
if (targetLayer !== preparedConnection.sourceLayer) {
const spanLayers = getViaSpanLayers({
fromLayer: preparedConnection.sourceLayer,
toLayer: targetLayer,
layerNames,
allowBlindAndBuriedVias
});
via = {
center: viaPoint,
diameter: viaDiameter,
holeDiameter: viaHoleDiameter,
fromLayer: preparedConnection.sourceLayer,
toLayer: targetLayer,
spanLayers
};
route.push({
route_type: "via",
x: viaPoint.x,
y: viaPoint.y,
from_layer: preparedConnection.sourceLayer,
to_layer: targetLayer,
via_diameter: viaDiameter,
via_hole_diameter: viaHoleDiameter
});
route.push({
route_type: "wire",
x: viaPoint.x,
y: viaPoint.y,
width: traceWidth,
layer: targetLayer
});
}
const appendLayerPath = (points, layer) => {
for (let index = 1;index < points.length; index++) {
const previousPoint = points[index - 1];
const nextPoint = points[index];
appendSegment(segments, previousPoint, nextPoint, traceWidth, layer);
route.push({
route_type: "wire",
x: nextPoint.x,
y: nextPoint.y,
width: traceWidth,
layer
});
}
};
const additionalVias = [];
if (usesLayeredWindingChannel && windingCrossoverLayer) {
const escapePoints = chamferOrthogonalPolyline2([
viaPoint,
...useNestedSpread ? [spreadPoint] : [],
doglegPoint,
...isHorizontal(bus.direction) !== isHorizontal(boundaryDirection) ? [localChannelSourcePoint] : [],
windingInputTransitionPoint
], Math.max(traceWidth + clearance, traceWidth * 2));
appendLayerPath(escapePoints, escapeLayer);
const appendTransitionVia = (center, fromLayer, toLayer) => {
if (fromLayer === toLayer)
return;
additionalVias.push({
center,
diameter: viaDiameter,
holeDiameter: viaHoleDiameter,
fromLayer,
toLayer,
spanLayers: getViaSpanLayers({
fromLayer,
toLayer,
layerNames,
allowBlindAndBuriedVias
})
});
route.push({
route_type: "via",
x: center.x,
y: center.y,
from_layer: fromLayer,
to_layer: toLayer,
via_diameter: viaDiameter,
via_hole_diameter: viaHoleDiameter
});
route.push({
route_type: "wire",
x: center.x,
y: center.y,
width: traceWidth,
layer: toLayer
});
};
appendTransitionVia(windingInputTransitionPoint, escapeLayer, windingCrossoverLayer);
appendLayerPath([windingInputTransitionPoint, boundaryChannelTargetPoint], windingCrossoverLayer);
appendTransitionVia(boundaryChannelTargetPoint, windingCrossoverLayer, escapeLayer);
appendLayerPath([boundaryChannelTargetPoint, exitPoint], escapeLayer);
} else {
const targetLayerPoints = terminateAtVia ? [viaPoint] : cornerSide ? chamferOrthogonalPolyline2([
viaPoint,
...useNestedSpread ? [spreadPoint] : [],
doglegPoint,
localChannelSourcePoint,
...isHorizontal(bus.direction) !== isHorizontal(boundaryDirection) ? [localChannelTargetPoint] : [],
boundaryChannelTargetPoint,
exitPoint
], Math.max(traceWidth + clearance, traceWidth * 2)) : useNestedSpread ? chamferOrthogonalPolyline2([viaPoint, spreadPoint, doglegPoint, exitPoint], Math.max(traceWidth + clearance, traceWidth * 2)) : [viaPoint, doglegPoint, exitPoint];
appendLayerPath(targetLayerPoints, escapeLayer);
}
const outputIds = createFanoutOutputIds({
connectionName: preparedConnection.connection.name,
sourcePointIndex: preparedConnection.sourcePointIndex
});
return {
busId: bus.busId,
connectionName: preparedConnection.connection.name,
connectionIndex: preparedConnection.connectionIndex,
sourcePointIndex: preparedConnection.sourcePointIndex,
sourcePoint: preparedConnection.sourcePoint,
sourceObstacle: preparedConnection.sourceObstacle,
sourceLayer: preparedConnection.sourceLayer,
targetPoint: preparedConnection.targetPoint,
targetLayer,
termination: bus.termination,
direction: bus.direction,
...bus.exitEdge ? { exitEdge: bus.exitEdge } : {},
...cornerSide ? { cornerBandSide: cornerSide } : {},
exitPoint,
trace: {
type: "pcb_trace",
pcb_trace_id: outputIds.traceId,
connection_name: preparedConnection.connection.name,
connectsTo: [
...preparedConnection.sourcePoint.pointId ? [preparedConnection.sourcePoint.pointId] : [],
...preparedConnection.sourcePoint.pcb_port_id ? [preparedConnection.sourcePoint.pcb_port_id] : [],
outputIds.boundaryExitPointId
],
route
},
segments,
via,
...additionalVias.length > 0 ? { additionalVias } : {},
length: segments.reduce((total, segment) => total + distance(segment.start, segment.end), 0)
};
}
function getPointLayer2(point) {
const layer = "layer" in point ? point.layer : point.layers[0];
if (!layer) {
throw new Error("FanoutSolver: plane endpoint has no copper layer");
}
return layer;
}
function getPlaneEndpointViaCandidates(params) {
const { preparedConnection, bus, viaDiameter, clearance } = params;
const { sourcePoint, targetPoint } = preparedConnection;
const nearbyEndpointLimit = Math.max(bus.pitchX, bus.pitchY) * 0.5;
if (distance(sourcePoint, targetPoint) <= 0.000001 || distance(sourcePoint, targetPoint) > nearbyEndpointLimit) {
return [];
}
const preferred = (() => {
switch (bus.direction) {
case "left":
return { x: -1, y: 0 };
case "right":
return { x: 1, y: 0 };
case "up":
return { x: 0, y: 1 };
case "down":
return { x: 0, y: -1 };
}
})();
const diagonal = Math.SQRT1_2;
const directions = [
preferred,
...[
{ x: diagonal, y: diagonal },
{ x: diagonal, y: -diagonal },
{ x: -diagonal, y: diagonal },
{ x: -diagonal, y: -diagonal },
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
].toSorted((first, second) => second.x * preferred.x + second.y * preferred.y - (first.x * preferred.x + first.y * preferred.y))
];
const minimumRadius = Math.max(viaDiameter, viaDiameter / 2 + clearance);
const radii = [
minimumRadius,
Math.max(minimumRadius, Math.min(bus.pitchX, bus.pitchY) * 0.5),
Math.max(minimumRadius, Math.min(bus.pitchX, bus.pitchY) * 0.625),
Math.max(minimumRadius, Math.min(bus.pitchX, bus.pitchY) * 0.75)
];
const candidates = [];
for (const origin of [sourcePoint, targetPoint]) {
for (const radius of radii) {
for (const direction of directions) {
const candidate = {
x: origin.x + direction.x * radius,
y: origin.y + direction.y * radius
};
if (distance(candidate, sourcePoint) <= 0.000001 || distance(candidate, targetPoint) <= 0.000001 || candidates.some((existing) => distance(existing, candidate) <= 0.000001)) {
continue;
}
candidates.push(candidate);
}
}
}
return candidates;
}
function addPlaneEndpointTerminal(params) {
const {
plan,
preparedConnection,
planeLayer,
viaPoint,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
allowBlindAndBuriedVias
} = params;
const targetPoint = {
x: preparedConnection.targetPoint.x,
y: preparedConnection.targetPoint.y
};
const targetEndpointLayer = getPointLayer2(preparedConnection.targetPoint);
const spanLayers = getViaSpanLayers({
fromLayer: planeLayer,
toLayer: targetEndpointLayer,
layerNames,
allowBlindAndBuriedVias
});
if (!spanLayers.includes(planeLayer)) {
throw new Error(`FanoutSolver: via for "${preparedConnection.connection.name}" does not cross plane ${planeLayer}`);
}
const planeEndpointSegments = [
{
start: viaPoint,
end: targetPoint,
width: traceWidth,
layer: targetEndpointLayer
}
];
const via = {
center: viaPoint,
diameter: viaDiameter,
holeDiameter: viaHoleDiameter,
fromLayer: planeLayer,
toLayer: targetEndpointLayer,
spanLayers
};
const outputIds = createFanoutOutputIds({
connectionName: preparedConnection.connection.name,
sourcePointIndex: preparedConnection.sourcePointIndex
});
const planeEndpointTrace = {
type: "pcb_trace",
pcb_trace_id: outputIds.planeEndpointTraceId,
connection_name: preparedConnection.connection.name,
connectsTo: [
...preparedConnection.targetPoint.pointId ? [preparedConnection.targetPoint.pointId] : [],
...preparedConnection.targetPoint.pcb_port_id ? [preparedConnection.targetPoint.pcb_port_id] : [],
outputIds.planeEndpointPointId
],
route: [
{
route_type: "wire",
...viaPoint,
width: traceWidth,
layer: planeLayer
},
{
route_type: "via",
...viaPoint,
from_layer: planeLayer,
to_layer: targetEndpointLayer,
via_diameter: viaDiameter,
via_hole_diameter: viaHoleDiameter
},
{
route_type: "wire",
...viaPoint,
width: traceWidth,
layer: targetEndpointLayer
},
{
route_type: "wire",
...targetPoint,
width: traceWidth,
layer: targetEndpointLayer
}
]
};
return {
...plan,
planeEndpointTrace,
planeEndpointSegments,
planeEndpointVia: via,
length: plan.length + planeEndpointSegments.reduce((total, segment) => total + distance(segment.start, segment.end), 0)
};
}
function segmentIsClearOfObstacles(params) {
const {
segment,
plan,
segmentIndex,
srj,
allowSameNetMerges,
obstacles,
clearance
} = params;
for (const obstacle of obstacles) {
if (!obstacle.layers.includes(segment.layer))
continue;
if (obstacle.connectedTo.includes(plan.connectionName))
continue;
if (allowSameNetMerges && obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)) {
continue;
}
if (segmentIsLegalTerminalBodyEscape({
inputSrj: srj,
segment,
bodyObstacle: obstacle,
connectionName: plan.connectionName
})) {
continue;
}
if (segmentIndex >= 0 && segmentIndex < (plan.sourceEscapeSegmentCount ?? 1) && obstacle === plan.sourceObstacle && segment.layer === plan.sourceLayer) {
continue;
}
if (distanceSegmentToObstacle(segment, obstacle) < segment.width / 2 + clearance - 0.000000001) {
return false;
}
}
return true;
}
function getPlanSegments(plan) {
return [...plan.segments, ...plan.planeEndpointSegments ?? []];
}
function getPlanVias2(plan) {
return [
plan.via,
...plan.additionalVias ?? [],
plan.planeEndpointVia
].filter((via) => Boolean(via));
}
function viaFitsInsidePlanSourcePad(plan, via) {
return distance(via.center, plan.sourcePoint) <= 0.000000001 && circleFitsInsideObstacle({
center: via.center,
diameter: via.diameter,
obstacle: plan.sourceObstacle
});
}
function planIsStaticallyClear(params) {
const {
plan,
srj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
} = params;
const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary);
if (!pointIsInsideBounds(plan.exitPoint, routableBounds) || getPlanSegments(plan).some((segment) => !pointIsInsideBounds(segment.start, routableBounds) || !pointIsInsideBounds(segment.end, routableBounds))) {
return false;
}
const segments = getPlanSegments(plan);
for (let index = 0;index < segments.length; index++) {
if (!segmentIsClearOfObstacles({
segment: segments[index],
plan,
segmentIndex: index < plan.segments.length ? index : -1,
srj,
allowSameNetMerges,
obstacles: srj.obstacles,
clearance
})) {
return false;
}
}
for (const via of getPlanVias2(plan)) {
for (const obstacle of srj.obstacles) {
if (allowsViaInPad(srj) && obstacle === plan.sourceObstacle && viaFitsInsidePlanSourcePad(plan, via)) {
continue;
}
if (!obstacle.layers.some((layer) => via.spanLayers.includes(layer))) {
continue;
}
if (allowSameNetMerges && obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)) {
continue;
}
if (distancePointToObstacle(via.center, obstacle) < via.diameter / 2 + clearance - 0.000000001) {
return false;
}
}
}
for (const traceCopper of getAllRoutedTraceCopper(srj, allowBlindAndBuriedVias)) {
if (plan.connectionName === traceCopper.connectionName || allowSameNetMerges && connectionsShareElectricalNet(srj, plan.connectionName, traceCopper.connectionName)) {
continue;
}
for (const segment of getPlanSegments(plan)) {
for (const existingSegment of traceCopper.segments) {
if (!segmentsAreClear(segment, existingSegment, clearance)) {
return false;
}
}
for (const existingVia of traceCopper.vias) {
if (existingVia.spanLayers.includes(segment.layer) && distancePointToSegment(existingVia.center, segment.start, segment.end) < existingVia.diameter / 2 + segment.width / 2 + clearance - 0.000000001) {
return false;
}
}
}
for (const via of getPlanVias2(plan)) {
for (const existingSegment of traceCopper.segments) {
if (via.spanLayers.includes(existingSegment.layer) && distancePointToSegment(via.center, existingSegment.start, existingSegment.end) < via.diameter / 2 + existingSegment.width / 2 + clearance - 0.000000001) {
return false;
}
}
for (const existingVia of traceCopper.vias) {
if (via.spanLayers.some((layer) => existingVia.spanLayers.includes(layer)) && distance(via.center, existingVia.center) < (via.diameter + existingVia.diameter) / 2 + clearance - 0.000000001) {
return false;
}
}
}
}
return true;
}
function planIsClearOfPlans(params) {
const {
plan,
otherPlans,
srj,
allowSameNetMerges,
clearance,
blockingBusCounts
} = params;
for (const otherPlan of otherPlans) {
if (allowSameNetMerges && connectionsShareElectricalNet(srj, plan.connectionName, otherPlan.connectionName)) {
continue;
}
const plansShareSourcePort = plan.sourcePoint.pcb_port_id && plan.sourcePoint.pcb_port_id === otherPlan.sourcePoint.pcb_port_id || plan.sourcePoint.pointId && plan.sourcePoint.pointId === otherPlan.sourcePoint.pointId;
if (plansShareSourcePort)
continue;
const recordBlocker = () => {
if (otherPlan.busId === plan.busId)
return;
blockingBusCounts?.set(otherPlan.busId, (blockingBusCounts.get(otherPlan.busId) ?? 0) + 1);
};
const planSegments = getPlanSegments(plan);
const otherSegments = getPlanSegments(otherPlan);
const planVias = getPlanVias2(plan);
const otherVias = getPlanVias2(otherPlan);
for (const segment of planSegments) {
for (const otherSegment of otherSegments) {
if (!segmentsAreClear(segment, otherSegment, clearance)) {
recordBlocker();
return false;
}
}
for (const otherVia of otherVias) {
if (otherVia.spanLayers.includes(segment.layer) && distancePointToSegment(otherVia.center, segment.start, segment.end) < otherVia.diameter / 2 + segment.width / 2 + clearance - 0.000000001) {
recordBlocker();
return false;
}
}
}
for (const planVia of planVias) {
for (const otherSegment of otherSegments) {
if (planVia.spanLayers.includes(otherSegment.layer) && distancePointToSegment(planVia.center, otherSegment.start, otherSegment.end) < planVia.diameter / 2 + otherSegment.width / 2 + clearance - 0.000000001) {
recordBlocker();
return false;
}
}
for (const otherVia of otherVias) {
if (planVia.spanLayers.some((layer) => otherVia.spanLayers.includes(layer)) && distance(planVia.center, otherVia.center) < (planVia.diameter + otherVia.diameter) / 2 + clearance - 0.000000001) {
recordBlocker();
return false;
}
}
}
}
return true;
}
function fanoutPlansAreMutuallyClear(params) {
const { plans, srj, clearance, allowSameNetMerges = false } = params;
return plans.every((plan, index) => planIsClearOfPlans({
plan,
otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
srj,
allowSameNetMerges,
clearance
}));
}
function planIsClear(params) {
const {
plan,
otherPlans,
staticClearanceCache,
blockingBusCounts,
cacheKey,
srj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
} = params;
let staticallyClear = staticClearanceCache?.get(cacheKey);
if (staticallyClear === undefined) {
staticallyClear = planIsStaticallyClear({
plan,
srj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
});
staticClearanceCache?.set(cacheKey, staticallyClear);
}
return staticallyClear && planIsClearOfPlans({
plan,
otherPlans,
srj,
allowSameNetMerges,
clearance,
blockingBusCounts
});
}
function fanoutPlansAreClear(params) {
const {
plans,
srj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias = true,
allowSameNetMerges = false
} = params;
for (let index = 0;index < plans.length; index++) {
const plan = plans[index];
if (!planIsStaticallyClear({
plan,
srj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
})) {
return false;
}
const otherPlans = plans.filter((_, otherIndex) => otherIndex !== index);
if (!planIsClearOfPlans({
plan,
otherPlans,
srj,
allowSameNetMerges,
clearance
})) {
return false;
}
}
return true;
}
function routePlaneTerminatedBus(params) {
const {
srj,
bus,
targetLayer,
acceptedPlans,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
staticClearanceCache,
blockingBusCounts,
allowBlindAndBuriedVias = true,
allowSameNetMerges = false,
fixedViaPointsByConnectionIndex,
planeCandidateSkipCount = 0
} = params;
const sourceObstacle = bus.connections[0]?.sourceObstacle;
if (!sourceObstacle || bus.termination.type !== "plane")
return null;
const sourceLayer = bus.connections[0].sourceLayer;
if (targetLayer === sourceLayer)
return null;
let remainingPlaneCandidatesToSkip = planeCandidateSkipCount;
const selectClearPlanePlan = (plans, isClear) => {
for (const [index, plan] of plans.entries()) {
if (!isClear(plan, index))
continue;
if (remainingPlaneCandidatesToSkip > 0) {
remainingPlaneCandidatesToSkip--;
continue;
}
if (params.collectAlternative && !params.collectAlternative(plan))
continue;
return plan;
}
return;
};
if (fixedViaPointsByConnectionIndex) {
const fixedPlans = [];
for (const preparedConnection of bus.connections) {
const fixedViaPoint = fixedViaPointsByConnectionIndex.get(preparedConnection.connectionIndex);
if (!fixedViaPoint)
return null;
const sourcePoint = {
x: preparedConnection.sourcePoint.x,
y: preparedConnection.sourcePoint.y
};
const basePlan = buildPlan2({
preparedConnection,
bus,
targetLayer,
track: getPerpendicularAxis2(sourcePoint, bus.direction),
exitAxis: getExitAxis(bus),
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
viaHandedness: 0,
interstitialEscape: false,
spreadLaneIndex: 0,
cornerExitLaneOffset: 0,
cornerLocalChannelLaneOffset: 0,
cornerBoundaryChannelLaneOffset: 0,
clearance,
terminateAtVia: true,
allowBlindAndBuriedVias,
initialViaPoint: fixedViaPoint,
sourceEscapePath: [sourcePoint, fixedViaPoint]
});
const endpointViaCandidates = getPlaneEndpointViaCandidates({
preparedConnection,
bus,
viaDiameter,
clearance
});
const plansToTry = [
...endpointViaCandidates.map((viaPoint) => addPlaneEndpointTerminal({
plan: basePlan,
preparedConnection,
planeLayer: targetLayer,
viaPoint,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
allowBlindAndBuriedVias
})),
basePlan
];
const clearPlan = selectClearPlanePlan(plansToTry, (candidatePlan, candidateIndex) => planIsClear({
plan: candidatePlan,
otherPlans: [...acceptedPlans, ...fixedPlans],
staticClearanceCache,
blockingBusCounts,
cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${fixedViaPoint.x}:${fixedViaPoint.y}:${candidateIndex}`,
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
}));
if (!clearPlan)
return null;
fixedPlans.push(clearPlan);
}
return fixedPlans;
}
if (allowsViaInPad(srj) && bus.connections.length === 1 && circleFitsInsideObstacle({
center: bus.connections[0].sourcePoint,
diameter: viaDiameter,
obstacle: sourceObstacle
})) {
const preparedConnection = bus.connections[0];
const viaInPadPlan = buildPlan2({
preparedConnection,
bus,
targetLayer,
track: getPerpendicularAxis2(preparedConnection.sourcePoint, bus.direction),
exitAxis: getExitAxis(bus),
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
viaHandedness: 0,
interstitialEscape: false,
spreadLaneIndex: 0,
cornerExitLaneOffset: 0,
cornerLocalChannelLaneOffset: 0,
cornerBoundaryChannelLaneOffset: 0,
clearance,
terminateAtVia: true,
allowBlindAndBuriedVias,
initialViaPoint: preparedConnection.sourcePoint
});
const endpointViaCandidates = getPlaneEndpointViaCandidates({
preparedConnection,
bus,
viaDiameter,
clearance
});
const plansToTry = [
...endpointViaCandidates.map((viaPoint) => addPlaneEndpointTerminal({
plan: viaInPadPlan,
preparedConnection,
planeLayer: targetLayer,
viaPoint,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
allowBlindAndBuriedVias
})),
viaInPadPlan
];
const clearPlan = selectClearPlanePlan(plansToTry, (candidatePlan, candidateIndex) => planIsClear({
plan: candidatePlan,
otherPlans: acceptedPlans,
staticClearanceCache,
blockingBusCounts,
cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
}));
if (clearPlan)
return [clearPlan];
}
const candidateDirections = [
bus.direction,
...["left", "right", "up", "down"].filter((direction) => direction !== bus.direction)
];
for (const direction of candidateDirections) {
const directionalBus = direction === bus.direction ? bus : { ...bus, direction };
const directionalPadSize = isHorizontal(direction) ? sourceObstacle.width : sourceObstacle.height;
const pairChannelFitsVia = getDirectionalPitch(directionalBus) / 2 - directionalPadSize / 2 >= viaDiameter / 2 + clearance - 0.000000001;
const viaHandednesses = pairChannelFitsVia ? [0] : [1, -1];
for (const viaHandedness of viaHandednesses) {
for (const connectionOrder of getConnectionOrders(directionalBus)) {
const candidatePlans = [];
let orderIsClear = true;
for (const preparedConnection of connectionOrder) {
const sourceTrack = getPerpendicularAxis2(preparedConnection.sourcePoint, direction);
const adjacentViaPoint = getInitialViaPoint({
preparedConnection,
bus: directionalBus,
targetLayer,
traceWidth,
viaDiameter,
clearance,
viaHandedness
});
const directionPitch = getDirectionalPitch(directionalBus);
const sign = directionSign(direction);
const boundaryAxis = getExitAxis(directionalBus, direction);
const availableTravel = sign * (boundaryAxis - getAxis(adjacentViaPoint, direction)) - (viaDiameter / 2 + clearance);
const maximumEscapeSteps = Math.max(0, Math.floor(availableTravel / directionPitch));
const sourcePoint = {
x: preparedConnection.sourcePoint.x,
y: preparedConnection.sourcePoint.y
};
const adjacentAxis = getAxis(adjacentViaPoint, direction);
const adjacentPerpendicularAxis = getPerpendicularAxis2(adjacentViaPoint, direction);
const perpendicularPitch = getPerpendicularPitch(directionalBus);
const sourceEscapePaths = [];
const maximumCandidatePaths = 128;
for (let totalSteps = 0;totalSteps <= maximumEscapeSteps && sourceEscapePaths.length < maximumCandidatePaths; totalSteps++) {
const straightViaPoint = makePoint(adjacentAxis + sign * totalSteps * directionPitch, adjacentPerpendicularAxis, direction);
if (totalSteps === 0) {
sourceEscapePaths.push([sourcePoint, adjacentViaPoint]);
} else {
for (const connector of getStraightOr45ConnectorVariants(sourcePoint, straightViaPoint)) {
sourceEscapePaths.push(connector);
if (sourceEscapePaths.length >= maximumCandidatePaths)
break;
}
if (sourceEscapePaths.length < maximumCandidatePaths) {
sourceEscapePaths.push([
sourcePoint,
adjacentViaPoint,
straightViaPoint
]);
}
}
for (let lateralSteps = 1;lateralSteps <= Math.min(3, totalSteps - 1) && sourceEscapePaths.length < maximumCandidatePaths; lateralSteps++) {
const outwardSteps = totalSteps - lateralSteps;
if (outwardSteps < 1 || outwardSteps > maximumEscapeSteps) {
continue;
}
for (const lateralSign of [-1, 1]) {
const lateralAxis = adjacentPerpendicularAxis + lateralSign * lateralSteps * perpendicularPitch;
const lateralPoint = makePoint(adjacentAxis, lateralAxis, direction);
const outwardPoint = makePoint(adjacentAxis + sign * outwardSteps * directionPitch, adjacentPerpendicularAxis, direction);
const detourViaPoint = makePoint(adjacentAxis + sign * outwardSteps * directionPitch, lateralAxis, direction);
const chamfer = Math.min(directionPitch, perpendicularPitch) * 0.2;
for (const connector of getStraightOr45ConnectorVariants(sourcePoint, detourViaPoint)) {
sourceEscapePaths.push(connector);
if (sourceEscapePaths.length >= maximumCandidatePaths)
break;
}
if (sourceEscapePaths.length >= maximumCandidatePaths)
break;
sourceEscapePaths.push(chamferOrthogonalCorners([
sourcePoint,
adjacentViaPoint,
lateralPoint,
detourViaPoint
], chamfer), chamferOrthogonalCorners([
sourcePoint,
adjacentViaPoint,
outwardPoint,
detourViaPoint
], chamfer));
if (sourceEscapePaths.length >= maximumCandidatePaths)
break;
}
}
}
let plan;
for (const [
pathIndex,
sourceEscapePath
] of sourceEscapePaths.entries()) {
const basePlan = buildPlan2({
preparedConnection,
bus: directionalBus,
targetLayer,
track: sourceTrack,
exitAxis: getExitAxis(directionalBus),
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
viaHandedness,
interstitialEscape: !pairChannelFitsVia,
spreadLaneIndex: 0,
cornerExitLaneOffset: 0,
cornerLocalChannelLaneOffset: 0,
cornerBoundaryChannelLaneOffset: 0,
clearance,
terminateAtVia: true,
allowBlindAndBuriedVias,
sourceEscapePath
});
const endpointViaCandidates = getPlaneEndpointViaCandidates({
preparedConnection,
bus: directionalBus,
viaDiameter,
clearance
});
const plansToTry = [
...endpointViaCandidates.map((viaPoint) => addPlaneEndpointTerminal({
plan: basePlan,
preparedConnection,
planeLayer: targetLayer,
viaPoint,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
allowBlindAndBuriedVias
})),
basePlan
];
plan = selectClearPlanePlan(plansToTry, (candidatePlan, candidateIndex) => planIsClear({
plan: candidatePlan,
otherPlans: [...acceptedPlans, ...candidatePlans],
staticClearanceCache,
blockingBusCounts,
cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
}));
if (plan)
break;
}
if (!plan) {
orderIsClear = false;
break;
}
candidatePlans.push(plan);
}
if (orderIsClear)
return candidatePlans;
}
}
}
return null;
}
function* routeBusAlternativesSteps(params, maxAlternatives = 1, includeVisualization = false) {
const {
srj,
bus,
targetLayer,
acceptedPlans,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
compactBusTracks,
staticClearanceCache,
blockingBusCounts,
allowBlindAndBuriedVias = true,
allowSameNetMerges = false,
rejectedViaMinimalCandidates,
stopAfterFirstRejectedViaMinimalCandidate = false,
fixedViaPointsByConnectionIndex,
reservedVias = [],
viaMinimalOnly = false,
allowBoundarySideViaFallback = false,
preferCornerBoundaryVia = false,
adaptiveWindingRouteOrder = false,
alignWindingGridToPads = false,
fixedViaFallbackRouteOrderAttempts = 24,
cornerBandTargetTrackOffset
} = params;
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
throw new Error(`FanoutSolver: maxAlternatives must be a positive integer, received ${maxAlternatives}`);
}
if (fixedViaPointsByConnectionIndex && bus.connections.some((connection) => !fixedViaPointsByConnectionIndex.has(connection.connectionIndex))) {
return [];
}
if (bus.termination.type === "plane") {
const alternatives2 = [];
if (bus.connections.length === 1 && maxAlternatives > 1) {
routePlaneTerminatedBus({
...params,
planeCandidateSkipCount: 0,
collectAlternative: (plan) => {
alternatives2.push([plan]);
return alternatives2.length >= maxAlternatives;
}
});
return alternatives2;
}
for (let planeCandidateSkipCount = 0;planeCandidateSkipCount < maxAlternatives; planeCandidateSkipCount++) {
const plan = routePlaneTerminatedBus({
...params,
planeCandidateSkipCount
});
if (!plan)
break;
alternatives2.push(plan);
}
return alternatives2;
}
const exitAxis = getExitAxis(bus);
const sourceObstacle = bus.connections[0]?.sourceObstacle;
if (!sourceObstacle)
return [[]];
const directionalPadSize = isHorizontal(bus.direction) ? sourceObstacle.width : sourceObstacle.height;
const sourceLayer = bus.connections[0].sourceLayer;
const targetUsesVia = targetLayer !== sourceLayer;
const outwardEdgeBus = busIsOnOutwardComponentEdge(bus);
const pairChannelFitsVia = getDirectionalPitch(bus) / 2 - directionalPadSize / 2 >= viaDiameter / 2 + clearance - 0.000000001;
const interstitialEscape = targetUsesVia && !outwardEdgeBus && !pairChannelFitsVia;
const availableViaHandednesses = targetUsesVia ? pairChannelFitsVia || outwardEdgeBus ? [0] : allowBlindAndBuriedVias ? [1, -1] : [-1, 1] : [0];
const viaHandednesses = (() => {
if (allowBlindAndBuriedVias)
return availableViaHandednesses;
if (availableViaHandednesses.length !== 2 || !availableViaHandednesses.includes(-1) || !availableViaHandednesses.includes(1)) {
return availableViaHandednesses;
}
const meanSourceTrack = bus.connections.reduce((sum, connection) => sum + getPerpendicularAxis2(connection.sourcePoint, bus.direction), 0) / bus.connections.length;
const meanTargetTrack = bus.connections.reduce((sum, connection) => sum + getPerpendicularAxis2(connection.exitTargetPoint ?? connection.targetPoint, bus.direction), 0) / bus.connections.length;
if (meanTargetTrack > meanSourceTrack + 0.000000001)
return [-1, 1];
if (meanTargetTrack < meanSourceTrack - 0.000000001)
return [1, -1];
return availableViaHandednesses;
})();
const alternatives = [];
const seenAlternativeKeys = new Set;
const cornerLaneOffsets = getCornerLaneOffsets(bus, acceptedPlans);
const addAlternative = (plans) => {
const key = plans.map((plan) => `${plan.connectionIndex}:${plan.targetLayer}:${plan.exitPoint.x}:${plan.exitPoint.y}:${plan.segments.map((segment) => `${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`).join(";")}`).join("|");
if (seenAlternativeKeys.has(key))
return;
seenAlternativeKeys.add(key);
alternatives.push(plans);
};
if (busUsesCoordinatedWindingChannel(bus) && bus.exitEdge) {
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge);
const boundaryExitAxis = getExitAxis(bus, boundaryDirection);
const cornerSide = getCornerSide(bus);
const canUseViaInPadTerminals = allowsViaInPad(srj) && bus.connections.every((preparedConnection) => circleFitsInsideObstacle({
center: preparedConnection.sourcePoint,
diameter: viaDiameter,
obstacle: preparedConnection.sourceObstacle
}));
const maximumThroughAllRouteOrderAttempts = 24;
const windingTargetOrderCount = cornerSide ? getWindingTargetOrders({
bus,
boundaryDirection,
layerNames,
targetLayer
}).orders.length : 1;
const uniformDogboneTerminalPatterns = viaHandednesses.map((viaHandedness) => ({
label: `uniform-${viaHandedness}`,
useViaInPad: false,
getViaHandedness: () => viaHandedness,
maximumRouteOrderAttempts: allowBlindAndBuriedVias ? undefined : maximumThroughAllRouteOrderAttempts
}));
const connectionsBySourceTrack = bus.connections.toSorted((first, second) => getPerpendicularAxis2(first.sourcePoint, bus.direction) - getPerpendicularAxis2(second.sourcePoint, bus.direction) || getAxis(first.sourcePoint, bus.direction) - getAxis(second.sourcePoint, bus.direction) || first.connectionIndex - second.connectionIndex);
const sourceTrackRankByConnectionIndex = new Map(connectionsBySourceTrack.map((connection, rank) => [
connection.connectionIndex,
rank
]));
const getTowardMedianHandedness = (rank) => rank < connectionsBySourceTrack.length / 2 ? 1 : -1;
const middleRank = Math.floor(connectionsBySourceTrack.length / 2);
const singleFlipRanks = [
0,
1,
middleRank,
middleRank + 1,
...connectionsBySourceTrack.map((_, rank) => rank)
].filter((rank, index, ranks) => rank >= 0 && rank < connectionsBySourceTrack.length && ranks.indexOf(rank) === index);
const towardMedianFlipRankSets = [
[middleRank],
[middleRank + 1],
[0],
[1],
[0, middleRank + 1],
[1, middleRank],
[0, middleRank],
[1, middleRank + 1],
[0, 1],
[middleRank, middleRank + 1],
...singleFlipRanks.map((rank) => [rank])
].map((ranks) => ranks.filter((rank) => rank >= 0 && rank < connectionsBySourceTrack.length).toSorted((first, second) => first - second)).filter((ranks, index, rankSets) => ranks.length > 0 && rankSets.findIndex((candidate) => candidate.join(",") === ranks.join(",")) === index);
const mixedDogboneTerminalPatterns = !allowBlindAndBuriedVias && (reservedVias.length > 0 || acceptedPlans.some((plan) => plan.termination.type === "plane")) && viaHandednesses.includes(-1) && viaHandednesses.includes(1) ? [
{
label: "toward-source-median",
useViaInPad: false,
maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
getViaHandedness: (connection) => getTowardMedianHandedness(sourceTrackRankByConnectionIndex.get(connection.connectionIndex) ?? 0)
},
...towardMedianFlipRankSets.slice(0, 12).map((flippedRanks) => ({
label: `toward-source-median-with-ranks-${flippedRanks.join("-")}-flipped`,
useViaInPad: false,
maximumRouteOrderAttempts: 6,
getViaHandedness: (connection) => {
const rank = sourceTrackRankByConnectionIndex.get(connection.connectionIndex) ?? 0;
const towardMedian = getTowardMedianHandedness(rank);
return flippedRanks.includes(rank) ? -towardMedian : towardMedian;
}
})),
{
label: "alternating-source-grid-a",
useViaInPad: false,
maximumRouteOrderAttempts: 3,
getViaHandedness: (connection) => (sourceTrackRankByConnectionIndex.get(connection.connectionIndex) ?? 0) % 2 === 0 ? -1 : 1
},
{
label: "alternating-source-grid-b",
useViaInPad: false,
maximumRouteOrderAttempts: 3,
getViaHandedness: (connection) => (sourceTrackRankByConnectionIndex.get(connection.connectionIndex) ?? 0) % 2 === 0 ? 1 : -1
},
{
label: "away-from-source-median",
useViaInPad: false,
maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
getViaHandedness: (connection) => (sourceTrackRankByConnectionIndex.get(connection.connectionIndex) ?? 0) < connectionsBySourceTrack.length / 2 ? -1 : 1
}
] : [];
const viaInPadTerminalPattern = {
label: "via-in-pad",
useViaInPad: true,
getViaHandedness: () => 0
};
const coordinatedViaPoints = fixedViaPointsByConnectionIndex ?? (!allowBlindAndBuriedVias && bus.connections.length >= 8 && reservedVias.length === 0 ? matchComponentDogboneViaSites([bus], {
viaDiameter,
viaHoleDiameter,
traceWidth,
clearance,
additionalObstacles: srj.obstacles,
blockingSegments: acceptedPlans.flatMap((plan) => getPlanSegments(plan).map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
}))),
blockingVias: acceptedPlans.flatMap((plan) => getPlanVias2(plan).map((via) => ({
connectionIndex: plan.connectionIndex,
...via.center,
center: via.center,
diameter: via.diameter,
spanLayers: via.spanLayers
})))
}) : null);
const fixedViaTerminalPatterns = coordinatedViaPoints ? [
...Array.from({ length: windingTargetOrderCount }, (_, windingOrderIndex) => ({
label: `component-matched-vias-winding-${windingOrderIndex}`,
useViaInPad: false,
getViaHandedness: () => 0,
getViaPoint: (connection) => coordinatedViaPoints.get(connection.connectionIndex),
maximumRouteOrderAttempts: 1,
windingOrderIndex,
preferTargetDirectedLaneBias: true
})),
{
label: "component-matched-vias-fallback",
useViaInPad: false,
getViaHandedness: () => 0,
getViaPoint: (connection) => coordinatedViaPoints.get(connection.connectionIndex),
maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
windingOrderIndex: 0,
preferTargetDirectedLaneBias: true
}
] : [];
const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some((plan) => plan.termination.type === "plane");
const acceptedBoundaryPlansExist = acceptedPlans.some((plan) => plan.termination.type === "boundary");
const dogboneTerminalPatterns = acceptedBoundaryPlansExist && !allowBlindAndBuriedVias ? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns] : [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns];
const unreservedTerminalPatterns = canUseViaInPadTerminals ? planeTerminationsAlreadyOccupyTheFanout ? [viaInPadTerminalPattern, ...dogboneTerminalPatterns] : [...dogboneTerminalPatterns, viaInPadTerminalPattern] : dogboneTerminalPatterns;
const terminalPatterns = fixedViaPointsByConnectionIndex ? fixedViaTerminalPatterns : [...fixedViaTerminalPatterns, ...unreservedTerminalPatterns];
if (!fixedViaPointsByConnectionIndex && !allowBlindAndBuriedVias && bus.connections.length > 1 && bus.connections.every((connection) => connection.sourceLayer !== targetLayer) && viaHandednesses.includes(-1) && viaHandednesses.includes(1)) {
const variants = getDogboneSideVariants(bus.connections, bus.direction);
for (const handedness of viaHandednesses) {
for (const flippedIndices of variants) {
terminalPatterns.push({
label: `local-dogbone-repair-${handedness}-${flippedIndices.join("-")}`,
useViaInPad: false,
getViaHandedness: () => handedness,
maximumRouteOrderAttempts: 6,
localDogboneRepair: true,
getViaPoint: (connection) => {
const point = getInitialViaPoint({
preparedConnection: connection,
bus,
targetLayer,
traceWidth,
viaDiameter,
clearance,
viaHandedness: handedness
});
if (!flippedIndices.includes(connection.connectionIndex))
return point;
return bus.direction === "up" || bus.direction === "down" ? { x: point.x, y: 2 * connection.sourcePoint.y - point.y } : { x: 2 * connection.sourcePoint.x - point.x, y: point.y };
}
});
}
}
}
const seenTerminalSignatures = new Set;
for (const terminalPattern of terminalPatterns) {
const terminals = bus.connections.map((preparedConnection) => {
const viaHandedness = terminalPattern.getViaHandedness(preparedConnection);
const boundaryTrack = cornerSide ? getCornerTargetTrack({
bus,
connection: preparedConnection,
cornerExitLaneOffset: cornerLaneOffsets.exit,
traceWidth,
viaDiameter,
clearance,
layerNames,
targetLayer,
windingOrderIndex: terminalPattern.windingOrderIndex,
cornerBandTargetTrackOffset
}) : getBoundaryTargetTrack({
bus,
connection: preparedConnection,
boundaryDirection
});
return {
connection: preparedConnection,
viaPoint: terminalPattern.getViaPoint ? terminalPattern.getViaPoint(preparedConnection) : terminalPattern.useViaInPad ? {
x: preparedConnection.sourcePoint.x,
y: preparedConnection.sourcePoint.y
} : getInitialViaPoint({
preparedConnection,
bus,
targetLayer,
traceWidth,
viaDiameter,
clearance,
viaHandedness
}),
exitPoint: makePoint(boundaryExitAxis, boundaryTrack, boundaryDirection)
};
});
const alignGridToPads = alignWindingGridToPads || Boolean(terminalPattern.getViaPoint && !fixedViaPointsByConnectionIndex);
if (terminalPattern.localDogboneRepair && !matchComponentDogboneViaSites([bus], {
viaDiameter,
viaHoleDiameter,
traceWidth,
clearance,
additionalObstacles: srj.obstacles,
fixedViaPointsByConnectionIndex: new Map(terminals.map((terminal) => [
terminal.connection.connectionIndex,
terminal.viaPoint
])),
blockingSegments: acceptedPlans.flatMap((plan) => getPlanSegments(plan).map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
}))),
blockingVias: [
...acceptedPlans.flatMap((plan) => getPlanVias2(plan).map((via) => ({
connectionIndex: plan.connectionIndex,
center: via.center,
diameter: via.diameter,
spanLayers: via.spanLayers
}))),
...reservedVias.map(({ via }) => ({ connectionIndex: -1, ...via }))
]
})) {
continue;
}
const gridStepDivisor = terminalPattern.getViaPoint && Math.min(bus.pitchX, bus.pitchY) - 2 * (viaDiameter / 2 + traceWidth / 2 + clearance) < traceWidth + clearance ? 2 : 1;
const terminalSignature = `${terminals.map((terminal) => `${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`).join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}`;
if (seenTerminalSignatures.has(terminalSignature))
continue;
seenTerminalSignatures.add(terminalSignature);
const windingSteps = routeViaMinimalWindingAlternativesSteps({
srj,
bus,
targetLayer,
terminals,
acceptedPlans,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges,
maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
adaptiveRouteOrder: adaptiveWindingRouteOrder,
alignGridToPads,
includeReverseTargetRotation: terminalPattern.localDogboneRepair,
reservedVias,
gridStepDivisor,
preferTargetDirectedLaneBias: terminalPattern.preferTargetDirectedLaneBias
}, fixedViaPointsByConnectionIndex && viaMinimalOnly ? Math.min(2, Math.max(1, maxAlternatives - alternatives.length)) : terminalPattern.maximumRouteOrderAttempts === undefined ? Math.min(2, maxAlternatives - alternatives.length) : 2, includeVisualization);
let windingResult = windingSteps.next();
while (!windingResult.done) {
yield {
phase: "via-minimal-winding",
busId: bus.busId,
targetLayer,
winding: windingResult.value
};
windingResult = windingSteps.next();
}
const viaMinimalAlternatives = windingResult.value;
for (const viaMinimalPlans of viaMinimalAlternatives) {
const combinedPlansAreClear = fanoutPlansAreClear({
plans: [...acceptedPlans, ...viaMinimalPlans],
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
});
if (!combinedPlansAreClear) {
const candidateIsInternallyClear = fanoutPlansAreClear({
plans: viaMinimalPlans,
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
});
if (candidateIsInternallyClear && rejectedViaMinimalCandidates) {
rejectedViaMinimalCandidates.push(viaMinimalPlans);
if (stopAfterFirstRejectedViaMinimalCandidate) {
return alternatives;
}
}
continue;
}
addAlternative(viaMinimalPlans);
if (alternatives.length >= maxAlternatives)
return alternatives;
}
}
}
if (alternatives.length === 0 && allowBoundarySideViaFallback && fixedViaPointsByConnectionIndex && bus.connections.length <= 2 && bus.exitEdge && bus.connections.every((connection) => connection.sourceLayer === bus.connections[0].sourceLayer && connection.sourceLayer !== targetLayer)) {
const sourceLayer2 = bus.connections[0].sourceLayer;
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge);
const boundaryExitAxis = getExitAxis(bus, boundaryDirection);
const finalTracks = bus.connections.map((preparedConnection) => preferCornerBoundaryVia && getCornerSide(bus) ? getCornerTargetTrack({
bus,
connection: preparedConnection,
cornerExitLaneOffset: cornerLaneOffsets.exit,
traceWidth,
viaDiameter,
clearance,
layerNames,
targetLayer,
cornerBandTargetTrackOffset
}) : getBoundaryTargetTrack({
bus,
connection: preparedConnection,
boundaryDirection
}));
const finalExitPoints = finalTracks.map((track) => makePoint(boundaryExitAxis, track, boundaryDirection));
const sourceLayerSrj = {
...srj,
obstacles: srj.obstacles.map((obstacle) => {
const connection = bus.connections.find((connection2) => obstacle === connection2.sourceObstacle || distance(obstacle.center, connection2.sourcePoint) <= 0.0000001 && obstacle.layers.includes(connection2.sourceLayer));
return connection ? {
...obstacle,
connectedTo: [
...obstacle.connectedTo,
connection.connection.name
]
} : obstacle;
})
};
const insetStep = Math.max(viaDiameter / 2 + clearance, Math.min(bus.pitchX, bus.pitchY) / 2);
const cornerSide = preferCornerBoundaryVia ? getCornerSide(bus) : undefined;
const boundaryViaCandidates = [1, 2, 3, 4, 5].flatMap((multiple) => {
const inset = multiple * insetStep;
const straight = finalTracks.map((finalTrack) => makePoint(boundaryExitAxis - directionSign(boundaryDirection) * inset, finalTrack, boundaryDirection));
if (!cornerSide)
return [straight];
return [
finalTracks.map((finalTrack) => makePoint(boundaryExitAxis - directionSign(boundaryDirection) * inset, finalTrack + (cornerSide === "maximum" ? inset : -inset), boundaryDirection)),
straight
];
});
const sourceCenter = {
x: bus.connections.reduce((sum, connection) => sum + connection.sourcePoint.x, 0) / bus.connections.length,
y: bus.connections.reduce((sum, connection) => sum + connection.sourcePoint.y, 0) / bus.connections.length
};
const directions = [
{ x: -1, y: 0, distance: sourceCenter.x - Math.min(...bus.xCoordinates) },
{ x: 1, y: 0, distance: Math.max(...bus.xCoordinates) - sourceCenter.x },
{ x: 0, y: -1, distance: sourceCenter.y - Math.min(...bus.yCoordinates) },
{ x: 0, y: 1, distance: Math.max(...bus.yCoordinates) - sourceCenter.y }
].toSorted((first, second) => first.distance - second.distance);
const displacedViaCandidates = bus.connections.length === 2 ? directions.flatMap((direction) => [3].map((multiple) => bus.connections.map((connection) => {
const original = fixedViaPointsByConnectionIndex.get(connection.connectionIndex);
return {
x: original.x + direction.x * multiple * bus.pitchX,
y: original.y + direction.y * multiple * bus.pitchY
};
}))) : [];
const viaCandidates = [
...displacedViaCandidates.map((points) => ({
points,
boundarySide: false
})),
...boundaryViaCandidates.map((points) => ({
points,
boundarySide: true
}))
];
for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
const boundaryVias = bus.connections.map((connection, index) => ({
connectionName: connection.connection.name,
via: {
center: boundaryViaPoints[index],
diameter: viaDiameter,
spanLayers: getViaSpanLayers({
fromLayer: sourceLayer2,
toLayer: targetLayer,
layerNames,
allowBlindAndBuriedVias
})
}
}));
if (boundaryVias.some((candidate, index) => boundaryVias.some((other, otherIndex) => index !== otherIndex && (distance(candidate.via.center, other.via.center) < viaDiameter + clearance - 0.000000001 || boundarySide && distancePointToSegment(candidate.via.center, other.via.center, finalExitPoints[otherIndex]) < viaDiameter / 2 + traceWidth / 2 + clearance - 0.000000001))))
continue;
if (boundaryVias.some(({ via }) => srj.obstacles.some((obstacle) => obstacle.layers.some((layer) => via.spanLayers.includes(layer)) && distancePointToObstacle(via.center, obstacle) < via.diameter / 2 + clearance - 0.000000001)))
continue;
const sourceLayerSteps = routeViaMinimalWindingAlternativesSteps({
srj: sourceLayerSrj,
bus,
targetLayer: sourceLayer2,
terminals: bus.connections.map((preparedConnection, index) => ({
connection: preparedConnection,
viaPoint: preparedConnection.sourcePoint,
exitPoint: boundaryViaPoints[index]
})),
acceptedPlans,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges,
maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 6,
reservedVias: bus.connections.length > 1 ? [...reservedVias, ...boundaryVias] : reservedVias,
gridStepDivisor: 2,
allowSourceLayerRouting: true,
alignGridToPads: true
}, 1, includeVisualization);
let sourceLayerResult = sourceLayerSteps.next();
while (!sourceLayerResult.done) {
yield {
phase: "via-minimal-winding",
busId: bus.busId,
targetLayer: sourceLayer2,
winding: sourceLayerResult.value
};
sourceLayerResult = sourceLayerSteps.next();
}
const sourceLayerPlans = sourceLayerResult.value[0];
if (!sourceLayerPlans || sourceLayerPlans.length !== bus.connections.length)
continue;
let targetLayerPlans;
if (!boundarySide) {
const targetSteps = routeViaMinimalWindingAlternativesSteps({
srj,
bus,
targetLayer,
terminals: bus.connections.map((connection, index) => ({
connection,
viaPoint: boundaryViaPoints[index],
exitPoint: finalExitPoints[index]
})),
acceptedPlans,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges,
maximumRouteOrderAttempts: 6,
reservedVias,
gridStepDivisor: 2,
alignGridToPads: true
}, 1, includeVisualization);
let targetResult = targetSteps.next();
while (!targetResult.done) {
yield {
phase: "via-minimal-winding",
busId: bus.busId,
targetLayer,
winding: targetResult.value
};
targetResult = targetSteps.next();
}
targetLayerPlans = targetResult.value[0];
if (!targetLayerPlans)
continue;
}
const plans = sourceLayerPlans.map((sourceLayerPlan, index) => {
const boundaryViaPoint = boundaryViaPoints[index];
const finalExitPoint = finalExitPoints[index];
const targetSegment = {
start: boundaryViaPoint,
end: finalExitPoint,
width: traceWidth,
layer: targetLayer
};
const targetSegments = targetLayerPlans ? targetLayerPlans[index].segments.filter((segment) => segment.layer === targetLayer) : [targetSegment];
const via = {
center: boundaryViaPoint,
diameter: viaDiameter,
holeDiameter: viaHoleDiameter,
fromLayer: sourceLayer2,
toLayer: targetLayer,
spanLayers: getViaSpanLayers({
fromLayer: sourceLayer2,
toLayer: targetLayer,
layerNames,
allowBlindAndBuriedVias
})
};
const sourceRoute = sourceLayerPlan.trace.route.filter((item) => item.route_type === "wire");
return {
...sourceLayerPlan,
...preferCornerBoundaryVia || bus.connections.length > 1 ? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length } : {},
targetLayer,
exitPoint: finalExitPoint,
via,
segments: [...sourceLayerPlan.segments, ...targetSegments],
length: sourceLayerPlan.length + targetSegments.reduce((sum, segment) => sum + distance(segment.start, segment.end), 0),
trace: {
...sourceLayerPlan.trace,
route: [
...sourceRoute,
{
route_type: "via",
...boundaryViaPoint,
from_layer: sourceLayer2,
to_layer: targetLayer,
via_diameter: viaDiameter,
via_hole_diameter: viaHoleDiameter
},
{
route_type: "wire",
...boundaryViaPoint,
width: traceWidth,
layer: targetLayer
},
...targetSegments.map((segment) => ({
route_type: "wire",
...segment.end,
width: traceWidth,
layer: targetLayer
}))
]
}
};
});
const boundaryViaPlansAreClear = fanoutPlansAreClear({
plans: [...acceptedPlans, ...plans],
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
});
const reservedViasAreClear = plans.every((plan) => reservedVias.every((reserved) => {
const via = plan.via;
if (reserved.connectionName === plan.connectionName || allowSameNetMerges && connectionsShareElectricalNet(srj, reserved.connectionName, plan.connectionName))
return true;
if (via.spanLayers.some((layer) => reserved.via.spanLayers.includes(layer)) && distance(via.center, reserved.via.center) < (via.diameter + reserved.via.diameter) / 2 + clearance - 0.000000001)
return false;
return plan.segments.every((segment) => !reserved.via.spanLayers.includes(segment.layer) || distancePointToSegment(reserved.via.center, segment.start, segment.end) >= reserved.via.diameter / 2 + traceWidth / 2 + clearance - 0.000000001);
}));
if (boundaryViaPlansAreClear && reservedViasAreClear) {
addAlternative(plans);
return alternatives;
}
}
}
if (viaMinimalOnly)
return alternatives;
const searchConnectionOrder = (connectionOrder, viaHandedness, connectionIndex, candidatePlans) => {
if (alternatives.length >= maxAlternatives)
return;
if (connectionIndex >= connectionOrder.length) {
addAlternative(candidatePlans);
return;
}
const preparedConnection = connectionOrder[connectionIndex];
const connectionRank = getConnectionRank(bus, preparedConnection);
const preferredTracks = [
getPreferredTrack({
bus,
connection: preparedConnection,
traceWidth
}),
getLegacyPreferredTrack({
bus,
connection: preparedConnection,
targetUsesVia,
interstitialEscape,
compactBusTracks,
traceWidth,
viaDiameter,
clearance
})
].filter((track, index, tracks) => tracks.findIndex((candidate) => Math.abs(candidate - track) < 0.000000001) === index);
const trackCandidates = preferredTracks.flatMap((preferredTrack) => getTrackCandidates({
bus,
connection: preparedConnection,
preferredTrack,
traceWidth,
clearance
})).filter((track, index, tracks) => tracks.findIndex((candidate) => Math.abs(candidate.value - track.value) < 0.000000001) === index);
for (let trackIndex = 0;trackIndex < trackCandidates.length; trackIndex++) {
const track = trackCandidates[trackIndex];
const plan = buildPlan2({
preparedConnection,
bus,
targetLayer,
track: track.value,
exitAxis,
layerNames,
traceWidth,
viaDiameter,
viaHoleDiameter,
viaHandedness,
interstitialEscape,
spreadLaneIndex: Math.min(connectionRank, bus.connections.length - connectionRank - 1),
cornerExitLaneOffset: cornerLaneOffsets.exit,
cornerLocalChannelLaneOffset: cornerLaneOffsets.localChannel,
cornerBoundaryChannelLaneOffset: cornerLaneOffsets.boundaryChannel,
clearance,
terminateAtVia: false,
allowBlindAndBuriedVias,
cornerBandTargetTrackOffset
});
if (!planIsClear({
plan,
otherPlans: [...acceptedPlans, ...candidatePlans],
staticClearanceCache,
blockingBusCounts,
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}:${cornerBandTargetTrackOffset ?? 0}`,
srj,
sharedBoundary: bus.sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
})) {
continue;
}
searchConnectionOrder(connectionOrder, viaHandedness, connectionIndex + 1, [...candidatePlans, plan]);
if (alternatives.length >= maxAlternatives)
return;
if (maxAlternatives === 1)
return;
}
};
for (const viaHandedness of viaHandednesses) {
for (const connectionOrder of getConnectionOrders(bus)) {
searchConnectionOrder(connectionOrder, viaHandedness, 0, []);
if (alternatives.length >= maxAlternatives)
return alternatives;
}
}
return alternatives;
}
function routeBusAlternatives(params, maxAlternatives = 1) {
const steps = routeBusAlternativesSteps(params, maxAlternatives);
let result = steps.next();
while (!result.done)
result = steps.next();
return result.value;
}
function routeBus(params) {
return routeBusAlternatives(params, 1)[0] ?? null;
}
// node_modules/@tscircuit/fanout-solver/lib/match-bus-lengths.ts
var EPSILON8 = 0.000001;
function pointsMatch2(first, second) {
return distance(first, second) <= EPSILON8;
}
function getPlanVias3(plan) {
return [plan.via, ...plan.additionalVias ?? []].filter((via) => via !== undefined);
}
function rebuildTraceRoute(plan, segments) {
const firstSegment = segments[0];
if (!firstSegment)
return null;
const firstOriginalWire = plan.trace.route.find((point) => point.route_type === "wire");
const lastOriginalWire = plan.trace.route.findLast((point) => point.route_type === "wire");
const getWireMetadata = (wire) => {
if (wire?.route_type !== "wire")
return {};
const metadata = { ...wire };
delete metadata.route_type;
delete metadata.x;
delete metadata.y;
delete metadata.width;
delete metadata.layer;
return metadata;
};
const vias = getPlanVias3(plan);
const startsWithSourceVia = pointsMatch2(firstSegment.start, plan.sourcePoint) && firstSegment.layer !== plan.sourceLayer && vias.some((via) => pointsMatch2(via.center, firstSegment.start) && via.spanLayers.includes(plan.sourceLayer) && via.spanLayers.includes(firstSegment.layer));
const initialLayer = startsWithSourceVia ? plan.sourceLayer : firstSegment.layer;
const route = [
{
...getWireMetadata(firstOriginalWire),
route_type: "wire",
...firstSegment.start,
width: firstSegment.width,
layer: initialLayer
}
];
let currentPoint = firstSegment.start;
let currentLayer = initialLayer;
for (const [segmentIndex, segment] of segments.entries()) {
if (!pointsMatch2(currentPoint, segment.start))
return null;
if (currentLayer !== segment.layer) {
const transitionVia = vias.find((via) => pointsMatch2(via.center, segment.start) && via.spanLayers.includes(currentLayer) && via.spanLayers.includes(segment.layer));
if (!transitionVia)
return null;
route.push({
route_type: "via",
...segment.start,
from_layer: currentLayer,
to_layer: segment.layer,
via_diameter: transitionVia.diameter,
via_hole_diameter: transitionVia.holeDiameter
});
route.push({
route_type: "wire",
...segment.start,
width: segment.width,
layer: segment.layer
});
currentLayer = segment.layer;
}
route.push({
...segmentIndex === segments.length - 1 ? getWireMetadata(lastOriginalWire) : {},
route_type: "wire",
...segment.end,
width: segment.width,
layer: segment.layer
});
currentPoint = segment.end;
}
return route;
}
function createPlanWithSegments(plan, segments) {
const route = rebuildTraceRoute(plan, segments);
if (!route)
return null;
const length = [...segments, ...plan.planeEndpointSegments ?? []].reduce((total, segment) => total + distance(segment.start, segment.end), 0);
return {
...plan,
trace: { ...plan.trace, route },
segments,
length
};
}
function pointIsOutsideDenseBounds(point, bounds, margin) {
return point.x < bounds.minX - margin || point.x > bounds.maxX + margin || point.y < bounds.minY - margin || point.y > bounds.maxY + margin;
}
function splitSegmentAtDenseBounds(params) {
const { segment, bounds, margin } = params;
const expandedBounds = {
minX: bounds.minX - margin,
maxX: bounds.maxX + margin,
minY: bounds.minY - margin,
maxY: bounds.maxY + margin
};
const deltaX = segment.end.x - segment.start.x;
const deltaY = segment.end.y - segment.start.y;
const splitParameters = [0, 1];
const addSplitParameter = (parameter) => {
if (parameter <= EPSILON8 || parameter >= 1 - EPSILON8)
return;
const point = {
x: segment.start.x + deltaX * parameter,
y: segment.start.y + deltaY * parameter
};
if (point.x < expandedBounds.minX - EPSILON8 || point.x > expandedBounds.maxX + EPSILON8 || point.y < expandedBounds.minY - EPSILON8 || point.y > expandedBounds.maxY + EPSILON8) {
return;
}
splitParameters.push(parameter);
};
if (Math.abs(deltaX) > EPSILON8) {
addSplitParameter((expandedBounds.minX - segment.start.x) / deltaX);
addSplitParameter((expandedBounds.maxX - segment.start.x) / deltaX);
}
if (Math.abs(deltaY) > EPSILON8) {
addSplitParameter((expandedBounds.minY - segment.start.y) / deltaY);
addSplitParameter((expandedBounds.maxY - segment.start.y) / deltaY);
}
const parameters = splitParameters.toSorted((first, second) => first - second).filter((parameter, index, values) => index === 0 || Math.abs(parameter - values[index - 1]) > EPSILON8);
return parameters.slice(1).map((endParameter, index) => {
const startParameter = parameters[index];
return {
...segment,
start: {
x: segment.start.x + deltaX * startParameter,
y: segment.start.y + deltaY * startParameter
},
end: {
x: segment.start.x + deltaX * endParameter,
y: segment.start.y + deltaY * endParameter
}
};
});
}
function getDenseCopperBounds(bus) {
return bus.componentObstacles.reduce((bounds, obstacle) => ({
minX: Math.min(bounds.minX, obstacle.center.x - obstacle.width / 2),
maxX: Math.max(bounds.maxX, obstacle.center.x + obstacle.width / 2),
minY: Math.min(bounds.minY, obstacle.center.y - obstacle.height / 2),
maxY: Math.max(bounds.maxY, obstacle.center.y + obstacle.height / 2)
}), {
minX: Number.POSITIVE_INFINITY,
maxX: Number.NEGATIVE_INFINITY,
minY: Number.POSITIVE_INFINITY,
maxY: Number.NEGATIVE_INFINITY
});
}
function hasNonAdjacentSelfIntersection(segments) {
for (let firstIndex = 0;firstIndex < segments.length; firstIndex++) {
const first = segments[firstIndex];
for (let secondIndex = firstIndex + 2;secondIndex < segments.length; secondIndex++) {
const second = segments[secondIndex];
if (first.layer !== second.layer)
continue;
if (secondIndex === firstIndex + 2 && pointsMatch2(first.end, second.start)) {
continue;
}
if (distanceSegmentToSegment(first.start, first.end, second.start, second.end) <= EPSILON8) {
return true;
}
}
}
return false;
}
function replacementCopperIsSelfClear(params) {
const {
plan,
segments,
replacementStartIndex,
replacementSegmentCount,
clearance
} = params;
const replacementEndIndex = replacementStartIndex + replacementSegmentCount - 1;
const vias = getPlanVias3(plan);
const getConnectedPathDistance = (firstIndex, secondIndex) => {
if (firstIndex === secondIndex)
return 0;
const startIndex = Math.min(firstIndex, secondIndex);
const endIndex = Math.max(firstIndex, secondIndex);
const startSegment = segments[startIndex];
const endSegment = segments[endIndex];
if (startSegment.layer !== endSegment.layer) {
return Number.POSITIVE_INFINITY;
}
let currentPoint = startSegment.end;
let pathDistance = 0;
for (let index = startIndex + 1;index < endIndex; index++) {
const segment = segments[index];
if (segment.layer !== startSegment.layer || !pointsMatch2(currentPoint, segment.start)) {
return Number.POSITIVE_INFINITY;
}
pathDistance += distance(segment.start, segment.end);
currentPoint = segment.end;
}
return pointsMatch2(currentPoint, segments[endIndex].start) ? pathDistance : Number.POSITIVE_INFINITY;
};
for (let replacementIndex = replacementStartIndex;replacementIndex <= replacementEndIndex; replacementIndex++) {
const replacement = segments[replacementIndex];
for (const [otherIndex, other] of segments.entries()) {
const requiredCenterlineClearance = replacement.width / 2 + other.width / 2 + clearance;
if (getConnectedPathDistance(replacementIndex, otherIndex) <= requiredCenterlineClearance + EPSILON8) {
continue;
}
if (!segmentsAreClear(replacement, other, clearance))
return false;
}
for (const via of vias) {
if (!via.spanLayers.includes(replacement.layer))
continue;
if (pointsMatch2(via.center, replacement.start) || pointsMatch2(via.center, replacement.end)) {
continue;
}
if (distancePointToSegment(via.center, replacement.start, replacement.end) < via.diameter / 2 + replacement.width / 2 + clearance - EPSILON8) {
return false;
}
}
}
return true;
}
function pointIsInsideBounds2(point, bounds) {
return point.x >= bounds.minX - EPSILON8 && point.x <= bounds.maxX + EPSILON8 && point.y >= bounds.minY - EPSILON8 && point.y <= bounds.maxY + EPSILON8;
}
function addScaled(point, vector, scale3) {
return {
x: point.x + vector.x * scale3,
y: point.y + vector.y * scale3
};
}
function createMeanderPoints(params) {
const {
segment,
toothCount,
targetAddedLength,
pitch,
placementFraction,
normalSign
} = params;
const dx = segment.end.x - segment.start.x;
const dy = segment.end.y - segment.start.y;
const segmentLength = Math.hypot(dx, dy);
if (segmentLength <= EPSILON8)
return null;
const isAxisAligned = Math.abs(dx) <= EPSILON8 || Math.abs(dy) <= EPSILON8;
const isFortyFiveDegree = Math.abs(Math.abs(dx) - Math.abs(dy)) <= EPSILON8;
if (!isAxisAligned && !isFortyFiveDegree)
return null;
const tangent = { x: dx / segmentLength, y: dy / segmentLength };
const normal = {
x: -tangent.y * normalSign,
y: tangent.x * normalSign
};
const chamfer = Math.min(pitch / 2, targetAddedLength / (8 * toothCount * (Math.SQRT2 - 1)));
const plateau = pitch;
const toothSpan = chamfer * 4 + plateau;
const toothGap = pitch;
const occupiedLength = toothCount * toothSpan + Math.max(0, toothCount - 1) * toothGap;
const minimumLead = pitch / 4;
if (occupiedLength + minimumLead * 2 > segmentLength + EPSILON8) {
return null;
}
const minimumAddedLengthPerTooth = 4 * chamfer * (Math.SQRT2 - 1);
if (targetAddedLength + EPSILON8 < minimumAddedLengthPerTooth * toothCount) {
return null;
}
const verticalRun = (targetAddedLength / toothCount - minimumAddedLengthPerTooth) / 2;
const movableLead = segmentLength - occupiedLength - minimumLead * 2;
const leadingLength = minimumLead + Math.max(0, movableLead) * placementFraction;
let cursor = addScaled(segment.start, tangent, leadingLength);
const points = [{ ...segment.start }, { ...cursor }];
for (let toothIndex = 0;toothIndex < toothCount; toothIndex++) {
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, chamfer);
points.push(cursor);
cursor = addScaled(cursor, normal, verticalRun);
points.push(cursor);
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, chamfer);
points.push(cursor);
cursor = addScaled(cursor, tangent, plateau);
points.push(cursor);
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, -chamfer);
points.push(cursor);
cursor = addScaled(cursor, normal, -verticalRun);
points.push(cursor);
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, -chamfer);
points.push(cursor);
if (toothIndex < toothCount - 1) {
cursor = addScaled(cursor, tangent, toothGap);
points.push(cursor);
}
}
points.push({ ...segment.end });
return points.filter((point, index) => index === 0 || !pointsMatch2(point, points[index - 1]));
}
function createTunedPlanCandidates(params) {
const {
plan,
bus,
targetAddedLength,
clearance,
sharedBoundary,
allowInsideDenseBounds = false,
denseBoundarySplitApplied = false
} = params;
const candidates = [];
const denseCopperBounds = getDenseCopperBounds(bus);
const denseMargin = plan.segments[0]?.width ? plan.segments[0].width / 2 + clearance : clearance;
const eligibleSegments = plan.segments.map((segment, segmentIndex) => ({ segment, segmentIndex })).filter(({ segment }) => segment.layer === plan.targetLayer).toSorted((first, second) => distance(second.segment.start, second.segment.end) - distance(first.segment.start, first.segment.end));
for (const { segment, segmentIndex } of eligibleSegments) {
const pitch = segment.width + clearance;
const segmentLength = distance(segment.start, segment.end);
const maximumToothCount = Math.min(12, Math.max(0, Math.floor((segmentLength / pitch + 0.5) / 4)));
for (let toothCount = 1;toothCount <= maximumToothCount; toothCount++) {
for (const placementFraction of [0.5, 0, 1, 0.25, 0.75]) {
for (const normalSign of [1, -1]) {
const points = createMeanderPoints({
segment,
toothCount,
targetAddedLength,
pitch,
placementFraction,
normalSign
});
if (!points)
continue;
if (points.some((point) => !pointIsInsideBounds2(point, sharedBoundary))) {
continue;
}
if (!allowInsideDenseBounds && points.slice(1, -1).some((point) => !pointIsOutsideDenseBounds(point, denseCopperBounds, denseMargin))) {
continue;
}
const replacementSegments = points.slice(1).map((end, index) => ({
start: points[index],
end,
width: segment.width,
layer: segment.layer
}));
const segments = [
...plan.segments.slice(0, segmentIndex),
...replacementSegments,
...plan.segments.slice(segmentIndex + 1)
];
if (hasNonAdjacentSelfIntersection(segments))
continue;
if (!replacementCopperIsSelfClear({
plan,
segments,
replacementStartIndex: segmentIndex,
replacementSegmentCount: replacementSegments.length,
clearance
})) {
continue;
}
const candidate = createPlanWithSegments(plan, segments);
if (candidate)
candidates.push(candidate);
}
}
}
}
if (denseBoundarySplitApplied)
return candidates;
const splitSegments = plan.segments.flatMap((segment) => splitSegmentAtDenseBounds({
segment,
bounds: denseCopperBounds,
margin: denseMargin
}));
const splitPlan = createPlanWithSegments(plan, splitSegments);
if (!splitPlan)
return candidates;
const splitCandidates = createTunedPlanCandidates({
...params,
plan: splitPlan,
denseBoundarySplitApplied: true
});
return [...candidates, ...splitCandidates];
}
function getBusSkew(plans) {
const lengths = plans.map((plan) => plan.length);
return Math.max(...lengths) - Math.min(...lengths);
}
function* createSpreadLaneCandidates(plan, clearance) {
for (const { segment, index } of plan.segments.map((segment2, index2) => ({ segment: segment2, index: index2 })).filter(({ segment: segment2 }) => segment2.layer === plan.targetLayer).toSorted((a, b) => distance(b.segment.start, b.segment.end) - distance(a.segment.start, a.segment.end))) {
const length = distance(segment.start, segment.end);
if (length <= EPSILON8)
continue;
const dx = Math.abs(segment.end.x - segment.start.x);
const dy = Math.abs(segment.end.y - segment.start.y);
if (dx > EPSILON8 && dy > EPSILON8 && Math.abs(dx - dy) > EPSILON8)
continue;
const tangent = {
x: (segment.end.x - segment.start.x) / length,
y: (segment.end.y - segment.start.y) / length
};
const pitch = segment.width + clearance;
for (const multiple of [2, 3, 4, 6]) {
const offset = pitch * multiple;
if (length < 2 * offset + pitch)
continue;
for (const sign of [1, -1]) {
const normal = { x: -tangent.y * sign, y: tangent.x * sign };
const points = [
segment.start,
addScaled(addScaled(segment.start, tangent, offset), normal, offset),
addScaled(addScaled(segment.end, tangent, -offset), normal, offset),
segment.end
];
const replacement = points.slice(1).map((end, i) => ({
...segment,
start: points[i],
end
}));
const segments = [
...plan.segments.slice(0, index),
...replacement,
...plan.segments.slice(index + 1)
];
if (hasNonAdjacentSelfIntersection(segments))
continue;
if (!replacementCopperIsSelfClear({
plan,
segments,
replacementStartIndex: index,
replacementSegmentCount: replacement.length,
clearance
}))
continue;
const candidate = createPlanWithSegments(plan, segments);
if (candidate)
yield candidate;
}
}
}
}
function matchBusPlanLengths(params) {
const {
preparedBuses,
inputSrj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias = true,
allowSameNetMerges = false,
allowMatchingInsideDenseBounds = false,
candidatePlansAreFeasible
} = params;
let matchedPlans = [...params.plans];
const constrainedBuses = preparedBuses.filter((bus) => bus.maxLengthSkew !== undefined && bus.connections.length > 1);
if (constrainedBuses.length === 0)
return { plans: matchedPlans };
for (const bus of constrainedBuses) {
if (bus.termination.type !== "boundary") {
return { plans: null, failedBus: bus };
}
const maximumIterations = bus.connections.length * 2;
for (let iteration = 0;iteration < maximumIterations; iteration++) {
const busPlans = matchedPlans.filter((plan) => plan.busId === bus.busId);
if (busPlans.length !== bus.connections.length) {
return { plans: null, failedBus: bus };
}
const maxLengthSkew = bus.maxLengthSkew;
const skew2 = getBusSkew(busPlans);
if (skew2 <= maxLengthSkew + EPSILON8)
break;
const shortest = busPlans.toSorted((first, second) => first.length - second.length || first.connectionName.localeCompare(second.connectionName))[0];
const longestLength = Math.max(...busPlans.map((plan) => plan.length));
const deficit = longestLength - shortest.length;
const minimumRequiredAddition = Math.max(EPSILON8, deficit - maxLengthSkew + EPSILON8);
const targetAddedLengths = [
minimumRequiredAddition,
minimumRequiredAddition + maxLengthSkew * 0.1,
deficit - maxLengthSkew * 0.5,
deficit - maxLengthSkew * 0.75,
deficit,
deficit + maxLengthSkew
].filter((value, index, values) => value > EPSILON8 && values.findIndex((candidate) => Math.abs(candidate - value) < EPSILON8) === index).toSorted((first, second) => first - second);
let acceptedPlans = null;
const acceptCandidate = (candidate) => {
const nextPlans = matchedPlans.map((plan) => plan === shortest ? candidate : plan);
const nextBusPlans = nextPlans.filter((plan) => plan.busId === bus.busId);
if (getBusSkew(nextBusPlans) > skew2 + EPSILON8)
return null;
if (!fanoutPlansAreClear({
plans: nextPlans,
srj: inputSrj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
})) {
return null;
}
if (candidatePlansAreFeasible && !candidatePlansAreFeasible(nextPlans)) {
return null;
}
return nextPlans;
};
const findMultiSpanCandidate = (targetAddedLength) => {
const maximumSearchStates = 320;
const maximumCandidatesPerState = 48;
let searchedStateCount = 0;
const sampleCandidates = (candidates) => {
if (candidates.length <= maximumCandidatesPerState) {
return [...candidates];
}
return Array.from({ length: maximumCandidatesPerState }, (_, sampleIndex) => candidates[Math.floor(sampleIndex * candidates.length / maximumCandidatesPerState)]);
};
const search = (currentPlan, stagesRemaining) => {
const addedLength = currentPlan.length - shortest.length;
const remainingAddition = targetAddedLength - addedLength;
if (remainingAddition <= EPSILON8) {
return acceptCandidate(currentPlan);
}
if (stagesRemaining <= 0)
return null;
const stageAddedLength = remainingAddition / stagesRemaining;
const candidates = sampleCandidates(createTunedPlanCandidates({
plan: currentPlan,
bus,
targetAddedLength: stageAddedLength,
clearance,
sharedBoundary: bus.sharedBoundary,
allowInsideDenseBounds: allowMatchingInsideDenseBounds
}));
for (const candidate of candidates) {
searchedStateCount++;
if (searchedStateCount > maximumSearchStates)
return null;
if (!acceptCandidate(candidate))
continue;
const result = search(candidate, stagesRemaining - 1);
if (result)
return result;
}
return null;
};
for (let stageCount = 2;stageCount <= 8; stageCount++) {
const result = search(shortest, stageCount);
if (result)
return result;
if (searchedStateCount > maximumSearchStates)
break;
}
return null;
};
for (const targetAddedLength of targetAddedLengths) {
const candidates = createTunedPlanCandidates({
plan: shortest,
bus,
targetAddedLength,
clearance,
sharedBoundary: bus.sharedBoundary,
allowInsideDenseBounds: allowMatchingInsideDenseBounds
});
for (const candidate of candidates) {
acceptedPlans = acceptCandidate(candidate);
if (!acceptedPlans)
continue;
break;
}
if (!acceptedPlans && Math.abs(targetAddedLength - minimumRequiredAddition) <= EPSILON8) {
acceptedPlans = findMultiSpanCandidate(targetAddedLength);
}
if (acceptedPlans)
break;
}
if (!acceptedPlans && params.allowPairLaneSpreading && bus.connections.length === 2) {
const longer = busPlans.find((plan) => plan !== shortest);
let attempts = 0;
for (const candidate of createSpreadLaneCandidates(longer, clearance)) {
const nextPlans = matchedPlans.map((plan) => plan === longer ? candidate : plan);
if (!fanoutPlansAreClear({
plans: nextPlans,
srj: inputSrj,
sharedBoundary,
clearance,
allowBlindAndBuriedVias,
allowSameNetMerges
}))
continue;
if (candidatePlansAreFeasible && !candidatePlansAreFeasible(nextPlans))
continue;
const result = matchBusPlanLengths({
...params,
plans: nextPlans,
preparedBuses: [bus],
allowPairLaneSpreading: false
});
if (result.plans) {
acceptedPlans = result.plans;
break;
}
if (++attempts >= 4)
break;
}
}
if (!acceptedPlans)
return { plans: null, failedBus: bus };
matchedPlans = acceptedPlans;
}
const matchedBusPlans = matchedPlans.filter((plan) => plan.busId === bus.busId);
if (getBusSkew(matchedBusPlans) > bus.maxLengthSkew + EPSILON8) {
return { plans: null, failedBus: bus };
}
}
return { plans: matchedPlans };
}
// node_modules/@tscircuit/fanout-solver/lib/prepare-buses.ts
var FANOUT_BORDER_TARGETS = new Set([
"left",
"right",
"top",
"bottom",
"top-left",
"top-right",
"bottom-left",
"bottom-right"
]);
var FANOUT_EDGES = new Set(["left", "right", "top", "bottom"]);
var AVAILABLE_BOUNDARY_REGIONS = {
top_left: {
direction: "up",
preferredExit: "top-left",
exitEdge: "top"
},
top_middle: {
direction: "up",
preferredExit: "top",
exitEdge: "top"
},
top_right: {
direction: "up",
preferredExit: "top-right",
exitEdge: "top"
},
right_top: {
direction: "right",
preferredExit: "top-right",
exitEdge: "right"
},
right_middle: {
direction: "right",
preferredExit: "right",
exitEdge: "right"
},
right_bottom: {
direction: "right",
preferredExit: "bottom-right",
exitEdge: "right"
},
bottom_right: {
direction: "down",
preferredExit: "bottom-right",
exitEdge: "bottom"
},
bottom_middle: {
direction: "down",
preferredExit: "bottom",
exitEdge: "bottom"
},
bottom_left: {
direction: "down",
preferredExit: "bottom-left",
exitEdge: "bottom"
},
left_bottom: {
direction: "left",
preferredExit: "bottom-left",
exitEdge: "left"
},
left_middle: {
direction: "left",
preferredExit: "left",
exitEdge: "left"
},
left_top: {
direction: "left",
preferredExit: "top-left",
exitEdge: "left"
},
top: {
direction: "up",
preferredExit: "top",
exitEdge: "top"
},
right: {
direction: "right",
preferredExit: "right",
exitEdge: "right"
},
bottom: {
direction: "down",
preferredExit: "bottom",
exitEdge: "bottom"
},
left: {
direction: "left",
preferredExit: "left",
exitEdge: "left"
}
};
function uniqueSorted(values) {
const sortedValues = [...values].sort((a, b) => a - b);
const result = [];
for (const value of sortedValues) {
if (result.length === 0 || Math.abs(result[result.length - 1] - value) > 0.000001) {
result.push(value);
}
}
return result;
}
function getPitch(coordinates) {
let pitch = Number.POSITIVE_INFINITY;
for (let index = 1;index < coordinates.length; index++) {
const difference = coordinates[index] - coordinates[index - 1];
if (difference > 0.000001)
pitch = Math.min(pitch, difference);
}
return pitch;
}
function getAlignedPitch(obstacles, axis) {
const perpendicularAxis = axis === "x" ? "y" : "x";
let pitch = Number.POSITIVE_INFINITY;
for (let firstIndex = 0;firstIndex < obstacles.length; firstIndex++) {
const first = obstacles[firstIndex];
for (let secondIndex = firstIndex + 1;secondIndex < obstacles.length; secondIndex++) {
const second = obstacles[secondIndex];
if (Math.abs(first.center[perpendicularAxis] - second.center[perpendicularAxis]) > 0.000001) {
continue;
}
const separation = Math.abs(first.center[axis] - second.center[axis]);
if (separation > 0.000001)
pitch = Math.min(pitch, separation);
}
}
return pitch;
}
function getComponentBounds(obstacles) {
return {
minX: Math.min(...obstacles.map((obstacle) => obstacle.center.x - obstacle.width / 2)),
maxX: Math.max(...obstacles.map((obstacle) => obstacle.center.x + obstacle.width / 2)),
minY: Math.min(...obstacles.map((obstacle) => obstacle.center.y - obstacle.height / 2)),
maxY: Math.max(...obstacles.map((obstacle) => obstacle.center.y + obstacle.height / 2))
};
}
function resolveComponentBounds(grid, options) {
const requestedBounds = options.componentBounds?.[grid.componentId];
if (!requestedBounds) {
const inferredMarginX = grid.pitchX * 2.25;
const inferredMarginY = grid.pitchY * 2.25;
return {
minX: grid.bounds.minX - inferredMarginX,
maxX: grid.bounds.maxX + inferredMarginX,
minY: grid.bounds.minY - inferredMarginY,
maxY: grid.bounds.maxY + inferredMarginY
};
}
const values = [
requestedBounds.minX,
requestedBounds.maxX,
requestedBounds.minY,
requestedBounds.maxY
];
if (values.some((value) => !Number.isFinite(value)) || requestedBounds.minX >= requestedBounds.maxX || requestedBounds.minY >= requestedBounds.maxY) {
throw new Error(`FanoutSolver: componentBounds for "${grid.componentId}" must contain finite, increasing bounds`);
}
if (requestedBounds.minX > grid.bounds.minX + 0.000001 || requestedBounds.maxX < grid.bounds.maxX - 0.000001 || requestedBounds.minY > grid.bounds.minY + 0.000001 || requestedBounds.maxY < grid.bounds.maxY - 0.000001) {
throw new Error(`FanoutSolver: componentBounds for "${grid.componentId}" must contain every component pad`);
}
return { ...requestedBounds };
}
function validateSharedBoundary(boundary, componentGrids) {
const values = [boundary.minX, boundary.maxX, boundary.minY, boundary.maxY];
if (values.some((value) => !Number.isFinite(value)) || boundary.minX >= boundary.maxX || boundary.minY >= boundary.maxY) {
throw new Error("FanoutSolver: sharedBoundary must contain finite, increasing bounds");
}
for (const grid of componentGrids) {
if (boundary.minX > grid.bounds.minX + 0.000001 || boundary.maxX < grid.bounds.maxX - 0.000001 || boundary.minY > grid.bounds.minY + 0.000001 || boundary.maxY < grid.bounds.maxY - 0.000001) {
throw new Error(`FanoutSolver: sharedBoundary must contain every pad of component "${grid.componentId}"`);
}
}
return { ...boundary };
}
function resolveSharedBoundary(componentGrids, options) {
if (options.sharedBoundary) {
return validateSharedBoundary(options.sharedBoundary, componentGrids);
}
const componentBounds = componentGrids.map((grid) => resolveComponentBounds(grid, options));
const maximumPitch = Math.max(...componentGrids.flatMap((grid) => [grid.pitchX, grid.pitchY]));
const inferredMargin = maximumPitch * 2.25;
return validateSharedBoundary({
minX: Math.min(...componentBounds.map((bounds) => bounds.minX)) - inferredMargin,
maxX: Math.max(...componentBounds.map((bounds) => bounds.maxX)) + inferredMargin,
minY: Math.min(...componentBounds.map((bounds) => bounds.minY)) - inferredMargin,
maxY: Math.max(...componentBounds.map((bounds) => bounds.maxY)) + inferredMargin
}, componentGrids);
}
function findComponentGrids(obstacles) {
const obstaclesByComponent = new Map;
for (const obstacle of obstacles) {
if (!obstacle.componentId || obstacle.isCopperPour)
continue;
const componentObstacles = obstaclesByComponent.get(obstacle.componentId) ?? [];
componentObstacles.push(obstacle);
obstaclesByComponent.set(obstacle.componentId, componentObstacles);
}
const grids = [];
for (const [componentId, componentObstacles] of obstaclesByComponent) {
const xCoordinates = uniqueSorted(componentObstacles.map((obstacle) => obstacle.center.x));
const yCoordinates = uniqueSorted(componentObstacles.map((obstacle) => obstacle.center.y));
const alignedPitchX = getAlignedPitch(componentObstacles, "x");
const alignedPitchY = getAlignedPitch(componentObstacles, "y");
const coordinatePitchX = getPitch(xCoordinates);
const coordinatePitchY = getPitch(yCoordinates);
const fallbackPitch = Math.min(...[
alignedPitchX,
alignedPitchY,
coordinatePitchX,
coordinatePitchY
].filter(Number.isFinite));
const padSizeFallback = Math.max(...componentObstacles.flatMap((obstacle) => [
obstacle.width,
obstacle.height
]));
const resolvedFallback = Number.isFinite(fallbackPitch) ? fallbackPitch : padSizeFallback;
const pitchX = Number.isFinite(alignedPitchX) ? alignedPitchX : Number.isFinite(coordinatePitchX) ? coordinatePitchX : resolvedFallback;
const pitchY = Number.isFinite(alignedPitchY) ? alignedPitchY : Number.isFinite(coordinatePitchY) ? coordinatePitchY : resolvedFallback;
grids.push({
componentId,
obstacles: componentObstacles,
xCoordinates,
yCoordinates,
pitchX,
pitchY,
bounds: getComponentBounds(componentObstacles)
});
}
return grids;
}
function getPointLayers2(point) {
return "layer" in point ? [point.layer] : point.layers;
}
function findPointObstacleMatches(params) {
const { point, connection, componentGrids } = params;
const pointLayers = getPointLayers2(point);
const matches = [];
for (const grid of componentGrids) {
const candidateObstacles = grid.obstacles.filter((obstacle) => obstacle.layers.some((layer) => pointLayers.includes(layer))).filter((obstacle) => pointIsInsideObstacle(point, obstacle, 0.00001)).sort((a, b) => {
const aDirect = a.connectedTo.includes(connection.name) || a.connectedTo.includes(point.pointId ?? "") || a.connectedTo.includes(point.pcb_port_id ?? "");
const bDirect = b.connectedTo.includes(connection.name) || b.connectedTo.includes(point.pointId ?? "") || b.connectedTo.includes(point.pcb_port_id ?? "");
if (aDirect !== bDirect)
return aDirect ? -1 : 1;
return a.width * a.height - b.width * b.height;
});
if (candidateObstacles[0]) {
matches.push({ grid, obstacle: candidateObstacles[0] });
}
}
return matches;
}
function inferBusId(connection) {
for (const point of connection.pointsToConnect) {
if ("layers" in point && point.busId)
return point.busId;
}
const nameMatch = /^BUS[_:-]([^_:-]+)(?:[_:-]\d+)?$/i.exec(connection.name);
return nameMatch?.[1] ?? null;
}
function resolvePreferredExit(busId, value) {
if (value === undefined)
return;
if (!FANOUT_BORDER_TARGETS.has(value)) {
throw new Error(`FanoutSolver: bus "${busId}" has invalid preferredExit "${value}"`);
}
return value;
}
function resolveExitEdge(busId, value) {
if (value === undefined)
return;
if (!FANOUT_EDGES.has(value)) {
throw new Error(`FanoutSolver: bus "${busId}" has invalid exitEdge "${value}"`);
}
return value;
}
function resolveExitPosition(busId, value) {
if (value === undefined)
return;
try {
return getFanoutExitPositionConfig(value);
} catch {
throw new Error(`FanoutSolver: bus "${busId}" has invalid exitPosition "${value}"`);
}
}
function assertExitPositionFieldMatches(params) {
const { busId, exitPosition, fieldName, expected, actual, sourceName } = params;
if (actual === undefined || actual === expected)
return;
throw new Error(`FanoutSolver: bus "${busId}" exitPosition "${exitPosition}" conflicts with ${sourceName} ${fieldName} "${actual}"`);
}
function resolveBusExitFields(params) {
const { busId, requestedBus, options } = params;
const exitPosition = requestedBus.exitPosition;
const exitPositionConfig = resolveExitPosition(busId, exitPosition);
const busPreferredExit = resolvePreferredExit(busId, requestedBus.preferredExit);
const optionPreferredExit = resolvePreferredExit(busId, options.busExitPreferences?.[busId]);
const busExitEdge = resolveExitEdge(busId, requestedBus.exitEdge);
if (exitPositionConfig && exitPosition) {
for (const [actual, sourceName] of [
[requestedBus.direction, "bus"],
[options.busDirections?.[busId], "busDirections"]
]) {
assertExitPositionFieldMatches({
busId,
exitPosition,
fieldName: "direction",
expected: exitPositionConfig.direction,
actual,
sourceName
});
}
for (const [actual, sourceName] of [
[busPreferredExit, "bus"],
[optionPreferredExit, "busExitPreferences"]
]) {
assertExitPositionFieldMatches({
busId,
exitPosition,
fieldName: "preferredExit",
expected: exitPositionConfig.preferredExit,
actual,
sourceName
});
}
assertExitPositionFieldMatches({
busId,
exitPosition,
fieldName: "exitEdge",
expected: exitPositionConfig.exitEdge,
actual: busExitEdge,
sourceName: "bus"
});
return {
exitPosition,
...exitPositionConfig.direction ? { direction: exitPositionConfig.direction } : {},
...exitPositionConfig.preferredExit ? { preferredExit: exitPositionConfig.preferredExit } : {},
...exitPositionConfig.exitEdge ? { exitEdge: exitPositionConfig.exitEdge } : {}
};
}
const direction = options.busDirections?.[busId] ?? requestedBus.direction ?? options.defaultDirection;
const preferredExit = resolvePreferredExit(busId, optionPreferredExit ?? busPreferredExit ?? options.defaultPreferredExit);
return {
...direction ? { direction } : {},
...preferredExit ? { preferredExit } : {},
...busExitEdge ? { exitEdge: busExitEdge } : {}
};
}
function resolveAllowedLayers(busId, allowedLayers) {
if (allowedLayers === undefined)
return;
if (allowedLayers.length === 0) {
throw new Error(`FanoutSolver: bus "${busId}" must allow at least one layer`);
}
for (const layer of allowedLayers) {
if (typeof layer !== "string" || layer.length === 0) {
throw new Error(`FanoutSolver: bus "${busId}" has an invalid allowed layer`);
}
}
return [...new Set(allowedLayers)];
}
function resolveMaxLengthSkew(busId, value) {
if (value === undefined)
return;
if (!Number.isFinite(value) || value < 0) {
throw new Error(`FanoutSolver: bus "${busId}" maxLengthSkew must be a finite non-negative number`);
}
return value;
}
function resolveAvailableBoundaryRegions(value) {
if (value === undefined)
return;
if (value.length === 0) {
throw new Error("FanoutSolver: availableCornersAndSides must contain at least one boundary region");
}
const regions = [];
const seen = new Set;
for (const input of value) {
const region = AVAILABLE_BOUNDARY_REGIONS[input];
if (!region) {
throw new Error(`FanoutSolver: invalid availableCornersAndSides value "${input}"`);
}
const key = `${region.exitEdge}:${region.direction}:${region.preferredExit}`;
if (seen.has(key))
continue;
seen.add(key);
regions.push(region);
}
return regions;
}
function resolveTermination(busId, value) {
if (value === undefined || value.type === "boundary") {
return { type: "boundary" };
}
if (value.type !== "plane" || typeof value.layer !== "string" || value.layer.length === 0) {
throw new Error(`FanoutSolver: bus "${busId}" has an invalid termination target`);
}
return { type: "plane", layer: value.layer };
}
function resolveBusSpecs(srj, options) {
const requestedBuses = options.buses ?? srj.buses;
const specsById = new Map;
const claimedConnectionNames = new Set;
const knownConnectionNames = new Set(srj.connections.map((connection) => connection.name));
for (const requestedBus of requestedBuses ?? []) {
if (specsById.has(requestedBus.busId)) {
throw new Error(`FanoutSolver: duplicate bus id "${requestedBus.busId}"`);
}
for (const connectionName of requestedBus.connectionNames) {
if (!knownConnectionNames.has(connectionName)) {
throw new Error(`FanoutSolver: bus "${requestedBus.busId}" references unknown connection "${connectionName}"`);
}
if (claimedConnectionNames.has(connectionName)) {
throw new Error(`FanoutSolver: connection "${connectionName}" belongs to more than one bus`);
}
claimedConnectionNames.add(connectionName);
}
const termination = resolveTermination(requestedBus.busId, requestedBus.termination);
const resolvedExitFields = resolveBusExitFields({
busId: requestedBus.busId,
requestedBus,
options
});
const allowedLayers = resolveAllowedLayers(requestedBus.busId, requestedBus.allowedLayers);
const maxLengthSkew = resolveMaxLengthSkew(requestedBus.busId, requestedBus.maxLengthSkew);
if (termination.type === "plane" && maxLengthSkew !== undefined) {
throw new Error(`FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot specify maxLengthSkew`);
}
if (termination.type === "plane" && resolvedExitFields.preferredExit !== undefined) {
throw new Error(`FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot also specify preferredExit`);
}
specsById.set(requestedBus.busId, {
...requestedBus,
sourceComponentId: requestedBus.sourceComponentId ?? options.sourceComponentId,
...resolvedExitFields,
...allowedLayers === undefined ? {} : { allowedLayers },
...maxLengthSkew === undefined ? {} : { maxLengthSkew },
termination
});
}
for (const connection of srj.connections) {
if (claimedConnectionNames.has(connection.name))
continue;
const inferredBusId = inferBusId(connection);
if (inferredBusId) {
const existing = specsById.get(inferredBusId);
if (existing) {
specsById.set(inferredBusId, {
...existing,
connectionNames: [...existing.connectionNames, connection.name]
});
} else {
specsById.set(inferredBusId, {
busId: inferredBusId,
connectionNames: [connection.name],
direction: options.busDirections?.[inferredBusId] ?? options.defaultDirection,
sourceComponentId: options.sourceComponentId,
preferredExit: resolvePreferredExit(inferredBusId, options.busExitPreferences?.[inferredBusId] ?? options.defaultPreferredExit),
termination: { type: "boundary" }
});
}
} else {
const singletonBusId = `connection:${connection.name}`;
specsById.set(singletonBusId, {
busId: singletonBusId,
connectionNames: [connection.name],
sourceComponentId: options.sourceComponentId,
direction: options.busDirections?.[singletonBusId] ?? options.defaultDirection,
preferredExit: resolvePreferredExit(singletonBusId, options.busExitPreferences?.[singletonBusId] ?? options.defaultPreferredExit),
termination: { type: "boundary" }
});
}
}
return [...specsById.values()];
}
function chooseSourceGrid(params) {
const { busSpec, connections, componentGrids } = params;
const matchCountByComponent = new Map;
for (const connection of connections) {
const matchedComponents = new Set;
for (const point of connection.pointsToConnect) {
for (const match of findPointObstacleMatches({
point,
connection,
componentGrids
})) {
matchedComponents.add(match.grid.componentId);
}
}
for (const componentId of matchedComponents) {
matchCountByComponent.set(componentId, (matchCountByComponent.get(componentId) ?? 0) + 1);
}
}
const selectedGrid = [...componentGrids].sort((a, b) => {
const countDifference = (matchCountByComponent.get(b.componentId) ?? 0) - (matchCountByComponent.get(a.componentId) ?? 0);
if (countDifference !== 0)
return countDifference;
return b.obstacles.length - a.obstacles.length;
})[0];
const requestedGrid = busSpec.sourceComponentId ? componentGrids.find((grid) => grid.componentId === busSpec.sourceComponentId) : undefined;
if (busSpec.sourceComponentId && !requestedGrid) {
throw new Error(`FanoutSolver: source component "${busSpec.sourceComponentId}" for bus "${busSpec.busId}" was not found`);
}
const sourceGrid = requestedGrid ?? selectedGrid;
const sourceMatchCount = sourceGrid ? matchCountByComponent.get(sourceGrid.componentId) ?? 0 : 0;
if (!sourceGrid || sourceMatchCount !== connections.length) {
throw new Error(busSpec.sourceComponentId ? `FanoutSolver: source component "${busSpec.sourceComponentId}" is not an endpoint on every connection in bus "${busSpec.busId}"` : `FanoutSolver: bus "${busSpec.busId}" does not have one component endpoint on every connection`);
}
return sourceGrid;
}
function chooseTargetPoint(sourcePoint, connection, sourcePointIndex, termination) {
const targetCandidates = connection.pointsToConnect.filter((_, pointIndex) => pointIndex !== sourcePointIndex);
const targetPoint = targetCandidates.sort((a, b) => distance(sourcePoint, b) - distance(sourcePoint, a))[0];
if (!targetPoint && termination.type === "plane") {
return sourcePoint;
}
if (!targetPoint) {
throw new Error(`FanoutSolver: connection "${connection.name}" has no target beyond its BGA pad`);
}
return targetPoint;
}
function prepareConnection(params) {
const {
connection,
connectionIndex,
sourceGrid,
componentGrids,
termination,
exitTargetPoint
} = params;
for (let sourcePointIndex = 0;sourcePointIndex < connection.pointsToConnect.length; sourcePointIndex++) {
const sourcePoint = connection.pointsToConnect[sourcePointIndex];
const sourceMatch = findPointObstacleMatches({
point: sourcePoint,
connection,
componentGrids
}).find((match) => match.grid.componentId === sourceGrid.componentId);
if (!sourceMatch)
continue;
const sourceLayer = getPointLayers2(sourcePoint).find((layer) => sourceMatch.obstacle.layers.includes(layer));
if (!sourceLayer) {
throw new Error(`FanoutSolver: connection "${connection.name}" has no source layer shared with its BGA pad`);
}
const targetPoint = chooseTargetPoint(sourcePoint, connection, sourcePointIndex, termination);
return {
connection,
connectionIndex,
sourcePoint,
sourcePointIndex,
sourceLayer,
sourceObstacle: sourceMatch.obstacle,
targetPoint,
exitTargetPoint: exitTargetPoint ?? {
x: targetPoint.x,
y: targetPoint.y
},
hasExplicitLayeredExitTarget: exitTargetPoint?.layer !== undefined
};
}
throw new Error(`FanoutSolver: connection "${connection.name}" does not touch component "${sourceGrid.componentId}"`);
}
function inferDirection(busId, connections) {
let dx = 0;
let dy = 0;
for (const preparedConnection of connections) {
const exitTargetPoint = preparedConnection.exitTargetPoint ?? preparedConnection.targetPoint;
dx += exitTargetPoint.x - preparedConnection.sourcePoint.x;
dy += exitTargetPoint.y - preparedConnection.sourcePoint.y;
}
if (Math.abs(dx) < 0.000000001 && Math.abs(dy) < 0.000000001) {
throw new Error(`FanoutSolver: cannot infer an escape direction for bus "${busId}"`);
}
if (Math.abs(dx) >= Math.abs(dy))
return dx >= 0 ? "right" : "left";
return dy >= 0 ? "up" : "down";
}
function getDirectionsForBorderTarget(target) {
switch (target) {
case "left":
return ["left"];
case "right":
return ["right"];
case "top":
return ["up"];
case "bottom":
return ["down"];
case "top-left":
return ["up", "left"];
case "top-right":
return ["up", "right"];
case "bottom-left":
return ["down", "left"];
case "bottom-right":
return ["down", "right"];
}
}
function getAverageSourcePoint(connections) {
return {
x: connections.reduce((sum, connection) => sum + connection.sourcePoint.x, 0) / connections.length,
y: connections.reduce((sum, connection) => sum + connection.sourcePoint.y, 0) / connections.length
};
}
function getDistanceToBoundary(source, direction, boundary) {
switch (direction) {
case "left":
return source.x - boundary.minX;
case "right":
return boundary.maxX - source.x;
case "up":
return boundary.maxY - source.y;
case "down":
return source.y - boundary.minY;
}
}
function getRegionAnchor(region, boundary) {
if (region.direction === "up" || region.direction === "down") {
if (region.preferredExit.endsWith("left"))
return boundary.minX;
if (region.preferredExit.endsWith("right"))
return boundary.maxX;
return (boundary.minX + boundary.maxX) / 2;
}
if (region.preferredExit.startsWith("top"))
return boundary.maxY;
if (region.preferredExit.startsWith("bottom"))
return boundary.minY;
return (boundary.minY + boundary.maxY) / 2;
}
function getRegionSourceCoordinate(source, direction) {
return direction === "up" || direction === "down" ? source.x : source.y;
}
function tryInferDirection(busId, connections) {
try {
return inferDirection(busId, connections);
} catch {
return;
}
}
function resolveAvailableBusExit(params) {
const {
busId,
explicitDirection,
preferredExit,
connections,
sharedBoundary,
availableRegions
} = params;
const compatibleRegions = availableRegions.filter((region) => (explicitDirection === undefined || region.direction === explicitDirection) && (preferredExit === undefined || region.preferredExit === preferredExit));
if (compatibleRegions.length === 0) {
throw new Error(`FanoutSolver: bus "${busId}" cannot use its requested exit with availableCornersAndSides`);
}
const inferredDirection = explicitDirection ? undefined : tryInferDirection(busId, connections);
const preferredDirectionRegions = inferredDirection ? compatibleRegions.filter((region) => region.direction === inferredDirection) : [];
const candidates = preferredDirectionRegions.length > 0 ? preferredDirectionRegions : compatibleRegions;
const averageSource = getAverageSourcePoint(connections);
return [...candidates].toSorted((first, second) => getDistanceToBoundary(averageSource, first.direction, sharedBoundary) - getDistanceToBoundary(averageSource, second.direction, sharedBoundary) || Math.abs(getRegionSourceCoordinate(averageSource, first.direction) - getRegionAnchor(first, sharedBoundary)) - Math.abs(getRegionSourceCoordinate(averageSource, second.direction) - getRegionAnchor(second, sharedBoundary)) || first.preferredExit.localeCompare(second.preferredExit))[0];
}
function validateExplicitExitAvailability(params) {
const { busId, exitEdge, preferredExit, availableRegions } = params;
const requestedBandSide = getCornerBandSide(exitEdge, preferredExit);
const hasCompatibleRegion = availableRegions.some((region) => region.exitEdge === exitEdge && getCornerBandSide(region.exitEdge, region.preferredExit) === requestedBandSide);
if (!hasCompatibleRegion) {
throw new Error(`FanoutSolver: bus "${busId}" cannot use its requested exit with availableCornersAndSides`);
}
}
function resolveBusDirection(params) {
const {
busId,
explicitDirection,
preferredExit,
connections,
sharedBoundary,
availableRegions
} = params;
if (availableRegions) {
return resolveAvailableBusExit({
busId,
explicitDirection,
preferredExit,
connections,
sharedBoundary,
availableRegions
});
}
if (!preferredExit) {
return {
direction: explicitDirection ?? inferDirection(busId, connections)
};
}
const compatibleDirections = getDirectionsForBorderTarget(preferredExit);
if (explicitDirection) {
if (!compatibleDirections.includes(explicitDirection)) {
throw new Error(`FanoutSolver: bus "${busId}" direction "${explicitDirection}" is incompatible with preferredExit "${preferredExit}"`);
}
return { direction: explicitDirection, preferredExit };
}
if (compatibleDirections.length === 1) {
return { direction: compatibleDirections[0], preferredExit };
}
let inferredDirection;
try {
inferredDirection = inferDirection(busId, connections);
} catch {
inferredDirection = undefined;
}
if (inferredDirection && compatibleDirections.includes(inferredDirection)) {
return { direction: inferredDirection, preferredExit };
}
const averageSource = getAverageSourcePoint(connections);
return {
direction: compatibleDirections.toSorted((first, second) => getDistanceToBoundary(averageSource, first, sharedBoundary) - getDistanceToBoundary(averageSource, second, sharedBoundary) || first.localeCompare(second))[0],
preferredExit
};
}
function prepareFanoutBuses(srj, options) {
const componentGrids = findComponentGrids(srj.obstacles);
if (componentGrids.length === 0 && srj.connections.length > 0) {
throw new Error("FanoutSolver: no componentId-tagged pad footprint was found");
}
const connectionIndexByName = new Map(srj.connections.map((connection, index) => [connection.name, index]));
const resolvedBusInputs = resolveBusSpecs(srj, options).map((busSpec) => {
for (const [connectionName, point] of Object.entries(busSpec.connectionExitTargets ?? {})) {
if (!busSpec.connectionNames.includes(connectionName)) {
throw new Error(`FanoutSolver: connectionExitTargets contains connection "${connectionName}" outside bus "${busSpec.busId}"`);
}
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`FanoutSolver: connectionExitTargets for connection "${connectionName}" must contain finite x and y coordinates`);
}
}
const connections = busSpec.connectionNames.map((connectionName) => {
const connectionIndex = connectionIndexByName.get(connectionName);
if (connectionIndex === undefined) {
throw new Error(`FanoutSolver: connection "${connectionName}" is missing from the input`);
}
return srj.connections[connectionIndex];
});
const sourceGrid = chooseSourceGrid({
busSpec,
connections,
componentGrids
});
const preparedConnections = connections.map((connection) => prepareConnection({
connection,
connectionIndex: connectionIndexByName.get(connection.name),
sourceGrid,
componentGrids,
termination: busSpec.termination ?? { type: "boundary" },
exitTargetPoint: busSpec.connectionExitTargets?.[connection.name]
}));
return { busSpec, sourceGrid, preparedConnections };
});
const sourceGrids = [
...new Map(resolvedBusInputs.map(({ sourceGrid }) => [
sourceGrid.componentId,
sourceGrid
])).values()
];
const sharedBoundary = resolveSharedBoundary(sourceGrids, options);
const availableRegions = resolveAvailableBoundaryRegions(options.availableCornersAndSides);
const buses = [];
for (const {
busSpec,
sourceGrid,
preparedConnections
} of resolvedBusInputs) {
if (busSpec.exitEdge && !busSpec.preferredExit) {
throw new Error(`FanoutSolver: bus "${busSpec.busId}" exitEdge requires preferredExit`);
}
if (busSpec.exitEdge && busSpec.preferredExit && !borderTargetIncludesEdge(busSpec.preferredExit, busSpec.exitEdge)) {
throw new Error(`FanoutSolver: bus "${busSpec.busId}" exitEdge "${busSpec.exitEdge}" is incompatible with preferredExit "${busSpec.preferredExit}"`);
}
const resolvedExit = resolveBusDirection({
busId: busSpec.busId,
explicitDirection: busSpec.direction ?? options.busDirections?.[busSpec.busId],
preferredExit: busSpec.preferredExit,
connections: preparedConnections,
sharedBoundary,
availableRegions: busSpec.termination?.type === "plane" || busSpec.exitEdge ? undefined : availableRegions
});
if (busSpec.termination?.type !== "plane" && busSpec.exitEdge && resolvedExit.preferredExit && availableRegions) {
validateExplicitExitAvailability({
busId: busSpec.busId,
exitEdge: busSpec.exitEdge,
preferredExit: resolvedExit.preferredExit,
availableRegions
});
}
buses.push({
busId: busSpec.busId,
...busSpec.maxLengthSkew === undefined ? {} : { maxLengthSkew: busSpec.maxLengthSkew },
direction: resolvedExit.direction,
preferredExit: resolvedExit.preferredExit,
...busSpec.exitEdge ? { exitEdge: busSpec.exitEdge } : {},
cornerBandConnectionCount: 0,
allowedLayers: busSpec.allowedLayers,
termination: busSpec.termination ?? { type: "boundary" },
connections: preparedConnections,
componentId: sourceGrid.componentId,
componentObstacles: sourceGrid.obstacles,
componentBounds: resolveComponentBounds(sourceGrid, options),
sharedBoundary,
xCoordinates: [...sourceGrid.xCoordinates],
yCoordinates: [...sourceGrid.yCoordinates],
pitchX: sourceGrid.pitchX,
pitchY: sourceGrid.pitchY
});
}
const cornerBandConnectionCounts = new Map;
for (const bus of buses) {
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit);
if (!bus.exitEdge || !side)
continue;
const key = `${bus.exitEdge}:${side}`;
cornerBandConnectionCounts.set(key, (cornerBandConnectionCounts.get(key) ?? 0) + bus.connections.length);
}
for (const bus of buses) {
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit);
if (!bus.exitEdge || !side)
continue;
bus.cornerBandConnectionCount = cornerBandConnectionCounts.get(`${bus.exitEdge}:${side}`) ?? bus.connections.length;
}
return buses;
}
// node_modules/@tscircuit/fanout-solver/lib/route-single-layer-adaptive-exits.ts
var EPSILON9 = 0.000000001;
var OBSTACLE_BIN_SIZE = 1;
var FANOUT_FLOW_DEBUG_ENABLED = globalThis.process?.env?.FANOUT_FLOW_DEBUG === "1";
function visualizeFlowProgress(params) {
const { boundary, sourcePoints, traceWidth, update } = params;
const width = boundary.maxX - boundary.minX;
const height = boundary.maxY - boundary.minY;
const annotationSize = Math.max(Math.min(width, height) * 0.02, 0.2);
const gridSampleStride = update.grid ? Math.max(1, Math.ceil(update.grid.points.length / 1200)) : 1;
const sampledGridPoints = update.grid ? update.grid.points.flatMap((point, index) => index % gridSampleStride === 0 ? [{ point, index }] : []) : [];
const processedNodeCount = update.grid?.processedNodeCount ?? update.grid?.points.length ?? 0;
return {
title: `Adaptive exits: ${update.phase}`,
rects: [
{
center: {
x: (boundary.minX + boundary.maxX) / 2,
y: (boundary.minY + boundary.maxY) / 2
},
width,
height,
fill: "rgba(0, 0, 0, 0)",
stroke: "rgba(14, 165, 233, 0.9)",
label: "adaptive flow grid boundary"
}
],
points: [
...sampledGridPoints.map(({ point, index: node }) => {
const processed = node < processedNodeCount;
return {
...point,
color: !processed ? "rgba(148, 163, 184, 0.18)" : update.grid.obstacleFreeNodes[node] ? "rgba(34, 197, 94, 0.38)" : "rgba(100, 116, 139, 0.28)",
label: processed ? update.grid.obstacleFreeNodes[node] ? "available flow node" : "blocked flow node" : "unprocessed flow node"
};
}),
...sourcePoints.map((point) => ({
...point,
color: "#f97316",
label: "adaptive route source"
})),
...(update.candidatePoints ?? []).map((point) => ({
...point,
color: "#06b6d4",
label: "selected terminal node"
}))
],
lines: (update.segments ?? []).map((segment) => ({
points: [segment.start, segment.end],
strokeColor: "rgba(250, 204, 21, 0.9)",
strokeWidth: Math.max(traceWidth, annotationSize * 0.4),
label: "current adaptive route"
})),
texts: [
{
x: boundary.minX,
y: boundary.maxY + annotationSize * 2,
text: `${update.phase}${update.direction ? ` · ${update.direction}` : ""}${update.processed !== undefined && update.total !== undefined ? ` · ${update.processed}/${update.total}` : ""}`,
color: "#0f172a",
fontSize: annotationSize * 1.5,
anchorSide: "bottom_left"
}
]
};
}
function getNetKey(connection) {
const simpleRouteConnection = connection.connection;
const connectivityNet = connection.sourceObstacle.connectedTo.find((connectionName) => connectionName.startsWith("connectivity_net"));
return connectivityNet ?? simpleRouteConnection.netConnectionName ?? connection.connection.name.replace(/::fanout:\d+$/, "");
}
function obstacleBelongsToItem(obstacle, item) {
return obstacle === item.connection.sourceObstacle || obstacle.connectedTo.includes(item.connection.connection.name) || obstacle.connectedTo.includes(item.netKey);
}
function connectWith45DegreeSegments(start, end) {
const deltaX = end.x - start.x;
const deltaY = end.y - start.y;
const absoluteX = Math.abs(deltaX);
const absoluteY = Math.abs(deltaY);
if (absoluteX < EPSILON9 || absoluteY < EPSILON9 || Math.abs(absoluteX - absoluteY) < EPSILON9) {
return [start, end];
}
if (absoluteX > absoluteY) {
return [
start,
{
x: start.x + Math.sign(deltaX) * absoluteY,
y: end.y
},
end
];
}
return [
start,
{
x: end.x,
y: start.y + Math.sign(deltaY) * absoluteX
},
end
];
}
function enforceStraightOr45DegreeSegments(points) {
if (points.length < 2)
return points;
const normalized = [points[0]];
for (const end of points.slice(1)) {
const start = normalized.at(-1);
const deltaX = Math.abs(end.x - start.x);
const deltaY = Math.abs(end.y - start.y);
if (deltaX < EPSILON9 || deltaY < EPSILON9 || Math.abs(deltaX - deltaY) < EPSILON9) {
normalized.push(end);
continue;
}
normalized.push(...connectWith45DegreeSegments(end, start).reverse().slice(1));
}
return compressPath2(normalized);
}
function compressPath2(points) {
if (points.length < 3)
return points;
const compressed = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = compressed.at(-1);
const current = points[index];
const next = points[index + 1];
if (Math.sign(current.x - previous.x) !== Math.sign(next.x - current.x) || Math.sign(current.y - previous.y) !== Math.sign(next.y - current.y)) {
compressed.push(current);
}
}
compressed.push(points.at(-1));
return compressed;
}
function chamferOrthogonalPolyline3(points, requestedChamfer) {
if (points.length < 3)
return points;
const chamfered = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = points[index - 1];
const corner = points[index];
const next = points[index + 1];
const incomingLength = distance(previous, corner);
const outgoingLength = distance(corner, next);
if (incomingLength < EPSILON9 || outgoingLength < EPSILON9)
continue;
const incoming = {
x: (corner.x - previous.x) / incomingLength,
y: (corner.y - previous.y) / incomingLength
};
const outgoing = {
x: (next.x - corner.x) / outgoingLength,
y: (next.y - corner.y) / outgoingLength
};
if (Math.abs(incoming.x * outgoing.x + incoming.y * outgoing.y) > 0.000001) {
chamfered.push(corner);
continue;
}
const chamfer = Math.min(requestedChamfer, incomingLength / 2, outgoingLength / 2);
chamfered.push({
x: corner.x - incoming.x * chamfer,
y: corner.y - incoming.y * chamfer
});
chamfered.push({
x: corner.x + outgoing.x * chamfer,
y: corner.y + outgoing.y * chamfer
});
}
chamfered.push(points.at(-1));
return compressPath2(chamfered);
}
function getSegments2(points, traceWidth) {
const segments = [];
for (let index = 1;index < points.length; index++) {
if (distance(points[index - 1], points[index]) < EPSILON9)
continue;
segments.push({
start: points[index - 1],
end: points[index],
width: traceWidth,
layer: "top"
});
}
return segments;
}
class Dinic {
edges;
levels;
nextEdges;
constructor(nodeCount) {
this.edges = Array.from({ length: nodeCount }, () => []);
this.levels = new Int32Array(nodeCount);
this.nextEdges = new Int32Array(nodeCount);
}
addEdge(from, to, capacity, metadata = {}) {
const forward = {
to,
reverseIndex: this.edges[to].length,
capacity,
initialCapacity: capacity,
...metadata
};
const reverse = {
to: from,
reverseIndex: this.edges[from].length,
capacity: 0,
initialCapacity: 0
};
this.edges[from].push(forward);
this.edges[to].push(reverse);
}
buildLevels(source, sink) {
this.levels.fill(-1);
const queue = new Int32Array(this.edges.length);
let head = 0;
let tail = 0;
queue[tail++] = source;
this.levels[source] = 0;
while (head < tail) {
const node = queue[head++];
for (const edge of this.edges[node]) {
if (edge.capacity <= 0 || this.levels[edge.to] >= 0)
continue;
this.levels[edge.to] = this.levels[node] + 1;
queue[tail++] = edge.to;
}
}
return this.levels[sink] >= 0;
}
sendFlow(node, sink) {
if (node === sink)
return 1;
for (let edgeIndex = this.nextEdges[node];edgeIndex < this.edges[node].length; edgeIndex++, this.nextEdges[node] = edgeIndex) {
const edge = this.edges[node][edgeIndex];
if (edge.capacity <= 0 || this.levels[edge.to] !== this.levels[node] + 1) {
continue;
}
const sent = this.sendFlow(edge.to, sink);
if (sent === 0)
continue;
edge.capacity -= sent;
this.edges[edge.to][edge.reverseIndex].capacity += sent;
return sent;
}
return 0;
}
*maximumFlowSteps(source, sink, limit) {
let flow = 0;
while (flow < limit && this.buildLevels(source, sink)) {
this.nextEdges.fill(0);
let flowSinceYield = 0;
while (flow < limit) {
const sent = this.sendFlow(source, sink);
if (sent === 0)
break;
flow += sent;
flowSinceYield += sent;
if (flowSinceYield >= 4 && flow < limit) {
flowSinceYield = 0;
yield flow;
}
}
}
return flow;
}
}
function* createFlowGridSteps(params) {
const { boundary, obstacles, traceWidth, clearance, reportProgress } = params;
const step = traceWidth + clearance;
const columnCount = Math.round((boundary.maxX - boundary.minX) / step) + 1;
const rowCount = Math.round((boundary.maxY - boundary.minY) / step) + 1;
const nodeCount = columnCount * rowCount;
const points = Array.from({ length: nodeCount }, (_, node) => ({
x: boundary.minX + node % columnCount * step,
y: boundary.minY + Math.floor(node / columnCount) * step
}));
const requiredObstacleDistance = traceWidth / 2 + clearance;
const obstacleIndexesByBin = new Map;
for (let obstacleIndex = 0;obstacleIndex < obstacles.length; obstacleIndex++) {
const obstacle = obstacles[obstacleIndex];
const minBinX = Math.floor((obstacle.center.x - obstacle.width / 2 - requiredObstacleDistance) / OBSTACLE_BIN_SIZE);
const maxBinX = Math.floor((obstacle.center.x + obstacle.width / 2 + requiredObstacleDistance) / OBSTACLE_BIN_SIZE);
const minBinY = Math.floor((obstacle.center.y - obstacle.height / 2 - requiredObstacleDistance) / OBSTACLE_BIN_SIZE);
const maxBinY = Math.floor((obstacle.center.y + obstacle.height / 2 + requiredObstacleDistance) / OBSTACLE_BIN_SIZE);
for (let binX = minBinX;binX <= maxBinX; binX++) {
for (let binY = minBinY;binY <= maxBinY; binY++) {
const key = `${binX}:${binY}`;
const indexes = obstacleIndexesByBin.get(key) ?? [];
indexes.push(obstacleIndex);
obstacleIndexesByBin.set(key, indexes);
}
}
if ((obstacleIndex + 1) % 128 === 0) {
reportProgress({
phase: "index-obstacles",
processed: obstacleIndex + 1,
total: obstacles.length
});
yield;
}
}
const getNearbyObstacles = (first, second = first) => {
const minBinX = Math.floor(Math.min(first.x, second.x) / OBSTACLE_BIN_SIZE);
const maxBinX = Math.floor(Math.max(first.x, second.x) / OBSTACLE_BIN_SIZE);
const minBinY = Math.floor(Math.min(first.y, second.y) / OBSTACLE_BIN_SIZE);
const maxBinY = Math.floor(Math.max(first.y, second.y) / OBSTACLE_BIN_SIZE);
const indexes = new Set;
for (let binX = minBinX;binX <= maxBinX; binX++) {
for (let binY = minBinY;binY <= maxBinY; binY++) {
for (const obstacleIndex of obstacleIndexesByBin.get(`${binX}:${binY}`) ?? []) {
indexes.add(obstacleIndex);
}
}
}
return [...indexes].map((index) => obstacles[index]);
};
const obstacleFreeNodes = new Uint8Array(nodeCount);
for (let node = 0;node < nodeCount; node++) {
const point = points[node];
if (getNearbyObstacles(point).every((obstacle) => distancePointToObstacle(point, obstacle) >= requiredObstacleDistance - EPSILON9)) {
obstacleFreeNodes[node] = 1;
}
if ((node + 1) % 2048 === 0) {
reportProgress({
phase: "classify-flow-grid",
processed: node + 1,
total: nodeCount,
grid: {
points,
obstacleFreeNodes,
processedNodeCount: node + 1
}
});
yield;
}
}
const neighbors = Array.from({ length: nodeCount }, () => []);
for (let node = 0;node < nodeCount; node++) {
if (!obstacleFreeNodes[node])
continue;
const column = node % columnCount;
const row = Math.floor(node / columnCount);
for (const [deltaColumn, deltaRow] of [
[1, 0],
[0, 1]
]) {
const nextColumn = column + deltaColumn;
const nextRow = row + deltaRow;
if (nextColumn >= columnCount || nextRow >= rowCount)
continue;
const nextNode = nextRow * columnCount + nextColumn;
if (!obstacleFreeNodes[nextNode])
continue;
const segment = {
start: points[node],
end: points[nextNode],
width: traceWidth,
layer: "top"
};
if (getNearbyObstacles(segment.start, segment.end).some((obstacle) => distanceSegmentToObstacle(segment, obstacle) < requiredObstacleDistance - EPSILON9)) {
continue;
}
neighbors[node].push(nextNode);
neighbors[nextNode].push(node);
}
if ((node + 1) % 2048 === 0) {
reportProgress({
phase: "connect-flow-grid",
processed: node + 1,
total: nodeCount,
grid: { points, obstacleFreeNodes, processedNodeCount: nodeCount }
});
yield;
}
}
return {
boundary,
step,
columnCount,
rowCount,
nodeCount,
points,
obstacleFreeNodes,
neighbors
};
}
function* routeDirectionGroupSteps(params) {
const {
direction,
availableDirections,
items,
grid,
obstacles,
traceWidth,
clearance,
occupiedNodes,
acceptedSegments,
connectorSelectionOffset = 0,
reportProgress
} = params;
if (items.length === 0) {
return { routes: [], usedNodes: [], unmatchedItems: [] };
}
const {
boundary,
step,
columnCount,
rowCount,
nodeCount: gridNodeCount
} = grid;
const pointForNode = (node) => grid.points[node];
const requiredObstacleDistance = traceWidth / 2 + clearance;
const requiredRouteDistance = traceWidth + clearance;
const freeNodes = new Uint8Array(gridNodeCount);
for (let node = 0;node < gridNodeCount; node++) {
if (!occupiedNodes[node] && grid.obstacleFreeNodes[node]) {
freeNodes[node] = 1;
}
}
const connectorIsClear = (points, item) => {
const segments = getSegments2(points, traceWidth);
return segments.every((segment) => obstacles.every((obstacle) => obstacleBelongsToItem(obstacle, item) || distanceSegmentToObstacle(segment, obstacle) >= requiredObstacleDistance - EPSILON9)) && segments.every((segment) => acceptedSegments.every((acceptedSegment) => distanceSegmentToSegment(segment.start, segment.end, acceptedSegment.start, acceptedSegment.end) >= requiredRouteDistance - EPSILON9));
};
const equivalentItemsByKey = new Map;
for (const item of items) {
const key = `${item.source.x.toFixed(6)}:${item.source.y.toFixed(6)}:${item.netKey}`;
const equivalents = equivalentItemsByKey.get(key) ?? [];
equivalents.push(item);
equivalentItemsByKey.set(key, equivalents);
}
const offsetCandidates = Array.from({ length: 61 * 61 }, (_, index) => {
const x = index % 61 - 30;
const y = Math.floor(index / 61) - 30;
return { x, y, distanceSquared: x * x + y * y };
}).sort((first, second) => first.distanceSquared - second.distanceSquared);
const terminals = [];
let processedTerminalGroupCount = 0;
for (const equivalentItems of equivalentItemsByKey.values()) {
const item = equivalentItems[0];
const maxConnectorLength = Math.hypot(item.connection.sourceObstacle.width / 2, item.connection.sourceObstacle.height / 2) + clearance + step * 2;
const sourceColumn = Math.round((item.source.x - boundary.minX) / step);
const sourceRow = Math.round((item.source.y - boundary.minY) / step);
const candidates = [];
const candidateNodes = new Set;
for (const offset of offsetCandidates) {
const column = sourceColumn + offset.x;
const row = sourceRow + offset.y;
if (column < 0 || column >= columnCount || row < 0 || row >= rowCount) {
continue;
}
const node = row * columnCount + column;
if (!freeNodes[node] || candidateNodes.has(node))
continue;
if (distance(item.source, pointForNode(node)) > maxConnectorLength) {
continue;
}
const connectorPoints = connectWith45DegreeSegments(item.source, pointForNode(node));
if (!connectorIsClear(connectorPoints, item))
continue;
candidateNodes.add(node);
candidates.push({ node, connectorPoints });
if (candidates.length >= 12)
break;
}
if (candidates.length === 0)
return null;
terminals.push({ item, equivalentItems, candidates });
processedTerminalGroupCount++;
if (processedTerminalGroupCount % 8 === 0) {
reportProgress({
phase: "discover-terminal-connectors",
direction,
processed: processedTerminalGroupCount,
total: equivalentItemsByKey.size,
grid: {
points: grid.points,
obstacleFreeNodes: freeNodes,
processedNodeCount: gridNodeCount
},
candidatePoints: terminals.flatMap((value) => value.candidates.map((candidate) => pointForNode(candidate.node))),
segments: acceptedSegments
});
yield;
}
}
const selectedConnectorSegments = [];
let selectedConnectorCount = 0;
for (const terminal of [...terminals].sort((first, second) => first.candidates.length - second.candidates.length)) {
const candidateOffset = terminal.candidates.length === 0 ? 0 : connectorSelectionOffset % terminal.candidates.length;
const orderedCandidates = [
...terminal.candidates.slice(candidateOffset),
...terminal.candidates.slice(0, candidateOffset)
];
const candidate = orderedCandidates.find((value) => {
const segments = getSegments2(value.connectorPoints, traceWidth);
return selectedConnectorSegments.every((selected) => selected.netKey === terminal.item.netKey || segments.every((segment) => selected.segments.every((otherSegment) => distanceSegmentToSegment(segment.start, segment.end, otherSegment.start, otherSegment.end) >= requiredRouteDistance - EPSILON9)));
});
if (!candidate)
return null;
terminal.candidates = [candidate];
selectedConnectorSegments.push({
netKey: terminal.item.netKey,
segments: getSegments2(candidate.connectorPoints, traceWidth)
});
selectedConnectorCount++;
if (selectedConnectorCount % 8 === 0) {
reportProgress({
phase: "select-terminal-connectors",
direction,
processed: selectedConnectorCount,
total: terminals.length,
grid: {
points: grid.points,
obstacleFreeNodes: freeNodes,
processedNodeCount: gridNodeCount
},
candidatePoints: terminals.flatMap((value) => value.candidates.map((candidate2) => pointForNode(candidate2.node))),
segments: [
...acceptedSegments,
...selectedConnectorSegments.flatMap((selected) => selected.segments)
]
});
yield;
}
}
const terminalNodes = new Set(terminals.map((terminal) => terminal.candidates[0].node));
for (const selected of selectedConnectorSegments) {
for (const segment of selected.segments) {
const minColumn = Math.max(0, Math.floor((Math.min(segment.start.x, segment.end.x) - requiredRouteDistance - boundary.minX) / step));
const maxColumn = Math.min(columnCount - 1, Math.ceil((Math.max(segment.start.x, segment.end.x) + requiredRouteDistance - boundary.minX) / step));
const minRow = Math.max(0, Math.floor((Math.min(segment.start.y, segment.end.y) - requiredRouteDistance - boundary.minY) / step));
const maxRow = Math.min(rowCount - 1, Math.ceil((Math.max(segment.start.y, segment.end.y) + requiredRouteDistance - boundary.minY) / step));
for (let row = minRow;row <= maxRow; row++) {
for (let column = minColumn;column <= maxColumn; column++) {
const node = row * columnCount + column;
if (!freeNodes[node] || terminalNodes.has(node))
continue;
const point = pointForNode(node);
if (distanceSegmentToSegment(segment.start, segment.end, point, point) < requiredRouteDistance - EPSILON9) {
freeNodes[node] = 0;
}
}
}
}
}
const source = 0;
const terminalStart = 1;
const gridInStart = terminalStart + terminals.length;
const gridOutStart = gridInStart + gridNodeCount;
const sink = gridOutStart + gridNodeCount;
const flow = new Dinic(sink + 1);
for (let terminalIndex = 0;terminalIndex < terminals.length; terminalIndex++) {
const terminalNode = terminalStart + terminalIndex;
flow.addEdge(source, terminalNode, 1);
for (const candidate of terminals[terminalIndex].candidates) {
flow.addEdge(terminalNode, gridInStart + candidate.node, 1, {
gridNode: candidate.node
});
}
}
for (let node = 0;node < gridNodeCount; node++) {
if (!freeNodes[node])
continue;
flow.addEdge(gridInStart + node, gridOutStart + node, 1);
const column = node % columnCount;
const row = Math.floor(node / columnCount);
for (const nextNode of grid.neighbors[node]) {
if (!freeNodes[nextNode])
continue;
flow.addEdge(gridOutStart + node, gridInStart + nextNode, 1, {
gridNode: nextNode
});
}
const isTarget = direction === "any" && ((!availableDirections || availableDirections.has("left")) && column === 0 || (!availableDirections || availableDirections.has("right")) && column === columnCount - 1 || (!availableDirections || availableDirections.has("down")) && row === 0 || (!availableDirections || availableDirections.has("up")) && row === rowCount - 1) || direction === "left" && column === 0 || direction === "right" && column === columnCount - 1 || direction === "down" && row === 0 || direction === "up" && row === rowCount - 1;
if (isTarget) {
flow.addEdge(gridOutStart + node, sink, 1, { isSink: true });
}
if ((node + 1) % 2048 === 0) {
reportProgress({
phase: "build-flow-network",
direction,
processed: node + 1,
total: gridNodeCount,
grid: {
points: grid.points,
obstacleFreeNodes: freeNodes,
processedNodeCount: gridNodeCount
},
candidatePoints: terminals.map((terminal) => pointForNode(terminal.candidates[0].node)),
segments: [
...acceptedSegments,
...selectedConnectorSegments.flatMap((selected) => selected.segments)
]
});
yield;
}
}
const flowSteps = flow.maximumFlowSteps(source, sink, terminals.length);
let flowResult = flowSteps.next();
while (!flowResult.done) {
reportProgress({
phase: "augment-terminal-flow",
direction,
processed: flowResult.value,
total: terminals.length,
grid: {
points: grid.points,
obstacleFreeNodes: freeNodes,
processedNodeCount: gridNodeCount
},
candidatePoints: terminals.map((terminal) => pointForNode(terminal.candidates[0].node)),
segments: [
...acceptedSegments,
...selectedConnectorSegments.flatMap((selected) => selected.segments)
]
});
yield;
flowResult = flowSteps.next();
}
const achievedFlow = flowResult.value;
const terminalWasMatched = (terminalIndex) => {
const terminalNode = terminalStart + terminalIndex;
return flow.edges[source].some((edge) => edge.to === terminalNode && edge.initialCapacity === 1 && edge.capacity === 0);
};
if (achievedFlow !== terminals.length) {
if (FANOUT_FLOW_DEBUG_ENABLED) {
const unmatchedConnections = terminals.flatMap((terminal, index) => {
return terminalWasMatched(index) ? [] : [terminal.item.connection.connection.name];
});
console.error("single-layer flow group failed", {
direction,
achievedFlow,
requiredFlow: terminals.length,
unmatchedConnections
});
}
}
const routes = [];
const usedNodes = [];
const unmatchedItems = [];
for (let terminalIndex = 0;terminalIndex < terminals.length; terminalIndex++) {
const terminal = terminals[terminalIndex];
if (!terminalWasMatched(terminalIndex)) {
unmatchedItems.push(...terminal.equivalentItems);
continue;
}
const terminalNode = terminalStart + terminalIndex;
const candidateEdge = flow.edges[terminalNode].find((edge) => edge.initialCapacity === 1 && edge.capacity === 0 && edge.gridNode !== undefined);
if (candidateEdge?.gridNode === undefined)
return null;
const candidate = terminal.candidates.find((value) => value.node === candidateEdge.gridNode);
if (!candidate)
return null;
const gridPoints = [pointForNode(candidate.node)];
let node = candidate.node;
const routeNodes = [node];
for (let guard = 0;guard <= gridNodeCount; guard++) {
const outNode = gridOutStart + node;
const nextEdge = flow.edges[outNode].find((edge) => edge.initialCapacity === 1 && edge.capacity === 0 && (edge.gridNode !== undefined || edge.isSink));
if (!nextEdge)
return null;
if (nextEdge.isSink)
break;
if (nextEdge.gridNode === undefined)
return null;
node = nextEdge.gridNode;
routeNodes.push(node);
gridPoints.push(pointForNode(node));
}
usedNodes.push(...routeNodes);
const unchamferedPoints = compressPath2([
...candidate.connectorPoints,
...gridPoints.slice(1)
]);
const points = enforceStraightOr45DegreeSegments(chamferOrthogonalPolyline3(unchamferedPoints, step / 2));
const segments = getSegments2(points, traceWidth);
for (const item of terminal.equivalentItems) {
routes.push({ item, points, segments });
}
if ((terminalIndex + 1) % 8 === 0) {
reportProgress({
phase: "extract-flow-routes",
direction,
processed: terminalIndex + 1,
total: terminals.length,
grid: {
points: grid.points,
obstacleFreeNodes: freeNodes,
processedNodeCount: gridNodeCount
},
segments: [
...acceptedSegments,
...routes.flatMap((route) => route.segments)
]
});
yield;
}
}
return { routes, usedNodes, unmatchedItems };
}
function buildPlan3(route, traceWidth) {
const { item, points, segments } = route;
const traceRoute = points.map((point, index) => ({
route_type: "wire",
x: point.x,
y: point.y,
width: traceWidth,
layer: "top",
...index === 0 && item.connection.sourcePoint.pcb_port_id ? { start_pcb_port_id: item.connection.sourcePoint.pcb_port_id } : {}
}));
const outputIds = createFanoutOutputIds({
connectionName: item.connection.connection.name,
sourcePointIndex: item.connection.sourcePointIndex
});
return {
busId: item.bus.busId,
connectionName: item.connection.connection.name,
connectionIndex: item.connection.connectionIndex,
sourcePointIndex: item.connection.sourcePointIndex,
sourcePoint: item.connection.sourcePoint,
sourceObstacle: item.connection.sourceObstacle,
sourceLayer: item.connection.sourceLayer,
targetPoint: item.connection.targetPoint,
targetLayer: "top",
termination: item.bus.termination,
direction: item.bus.direction,
...item.bus.exitEdge ? { exitEdge: item.bus.exitEdge } : {},
exitPoint: points.at(-1),
trace: {
type: "pcb_trace",
pcb_trace_id: outputIds.traceId,
connection_name: item.connection.connection.name,
connectsTo: [
...item.connection.sourcePoint.pointId ? [item.connection.sourcePoint.pointId] : [],
...item.connection.sourcePoint.pcb_port_id ? [item.connection.sourcePoint.pcb_port_id] : [],
outputIds.boundaryExitPointId
],
route: traceRoute
},
segments,
length: segments.reduce((total, segment) => total + distance(segment.start, segment.end), 0)
};
}
function getConnectorVariants2(start, end) {
const first = connectWith45DegreeSegments(start, end);
const second = connectWith45DegreeSegments(end, start).reverse();
return first.length === second.length && first.every((point, index) => distance(point, second[index]) < EPSILON9) ? [first] : [first, second];
}
function plansHaveRequiredClearance(params) {
const { plans, items, obstacles, traceWidth, clearance } = params;
const netKeyByConnectionName = new Map(items.map((item) => [item.connection.connection.name, item.netKey]));
const uniqueSegmentsByNet = new Map;
const segmentKeysByNet = new Map;
for (const plan of plans) {
const netKey = netKeyByConnectionName.get(plan.connectionName);
const segments = uniqueSegmentsByNet.get(netKey) ?? [];
const segmentKeys = segmentKeysByNet.get(netKey) ?? new Set;
for (const segment of plan.segments) {
const endpoints = [segment.start, segment.end].map((point) => `${point.x.toFixed(6)}:${point.y.toFixed(6)}`).sort();
const key = endpoints.join(":");
if (segmentKeys.has(key))
continue;
segmentKeys.add(key);
segments.push(segment);
}
uniqueSegmentsByNet.set(netKey, segments);
segmentKeysByNet.set(netKey, segmentKeys);
}
const requiredObstacleDistance = traceWidth / 2 + clearance;
for (const [netKey, segments] of uniqueSegmentsByNet) {
for (const segment of segments) {
for (const obstacle of obstacles) {
if (obstacle.connectedTo.includes(netKey) || distanceSegmentToObstacle(segment, obstacle) >= requiredObstacleDistance - EPSILON9) {
continue;
}
if (FANOUT_FLOW_DEBUG_ENABLED) {
console.error("single-layer route violates obstacle clearance", {
netKey,
segment,
obstacleId: obstacle.obstacleId,
distance: distanceSegmentToObstacle(segment, obstacle),
requiredObstacleDistance
});
}
return false;
}
}
}
const requiredRouteDistance = traceWidth + clearance;
const entries = [...uniqueSegmentsByNet];
for (let firstIndex = 0;firstIndex < entries.length; firstIndex++) {
for (let secondIndex = firstIndex + 1;secondIndex < entries.length; secondIndex++) {
for (const first of entries[firstIndex][1]) {
for (const second of entries[secondIndex][1]) {
if (distanceSegmentToSegment(first.start, first.end, second.start, second.end) < requiredRouteDistance - EPSILON9) {
if (FANOUT_FLOW_DEBUG_ENABLED) {
console.error("single-layer routes violate copper clearance", {
firstNetKey: entries[firstIndex][0],
secondNetKey: entries[secondIndex][0],
first,
second,
distance: distanceSegmentToSegment(first.start, first.end, second.start, second.end),
requiredRouteDistance
});
}
return false;
}
}
}
}
}
return true;
}
function getDirectionForBoundaryPoint(point, boundary) {
if (Math.abs(point.x - boundary.minX) < EPSILON9)
return "left";
if (Math.abs(point.x - boundary.maxX) < EPSILON9)
return "right";
if (Math.abs(point.y - boundary.minY) < EPSILON9)
return "down";
if (Math.abs(point.y - boundary.maxY) < EPSILON9)
return "up";
return null;
}
function* routeWithAdaptiveExitsSteps(params) {
const {
items,
grid,
obstacles,
traceWidth,
clearance,
availableBoundaryRegions,
reportProgress
} = params;
const availableDirections = availableBoundaryRegions ? new Set(availableBoundaryRegions.map((region) => region.direction)) : undefined;
const mergeItems = new Set(items.filter((item) => item.connection.sourceObstacle.width > 2 && item.connection.sourceObstacle.height > 2));
let unrestricted = null;
for (let mergeRound = 0;mergeRound < 4; mergeRound++) {
const directlyRoutedItems = items.filter((item) => !mergeItems.has(item));
let bestResult = null;
for (let connectorSelectionOffset = 0;connectorSelectionOffset < 4; connectorSelectionOffset++) {
const result = yield* routeDirectionGroupSteps({
direction: "any",
availableDirections,
items: directlyRoutedItems,
grid,
obstacles,
traceWidth,
clearance,
occupiedNodes: new Uint8Array(grid.nodeCount),
acceptedSegments: [],
connectorSelectionOffset,
reportProgress
});
if (!result)
continue;
if (!bestResult || result.routes.length > bestResult.routes.length) {
bestResult = result;
}
if (result.routes.length === directlyRoutedItems.length)
break;
}
if (!bestResult)
return null;
if (bestResult.routes.length === directlyRoutedItems.length) {
unrestricted = bestResult;
break;
}
const directlyRoutedNetCounts = new Map;
for (const item of directlyRoutedItems) {
directlyRoutedNetCounts.set(item.netKey, (directlyRoutedNetCounts.get(item.netKey) ?? 0) + 1);
}
let addedMergeItem = false;
for (const item of bestResult.unmatchedItems) {
if ((directlyRoutedNetCounts.get(item.netKey) ?? 0) < 2)
continue;
mergeItems.add(item);
addedMergeItem = true;
}
if (!addedMergeItem)
return null;
}
if (!unrestricted)
return null;
const routes = [...unrestricted.routes];
const requiredObstacleDistance = traceWidth / 2 + clearance;
const requiredRouteDistance = traceWidth + clearance;
for (const mergeItem of mergeItems) {
const candidates = routes.filter((route) => route.item.netKey === mergeItem.netKey).flatMap((route) => route.points.map((point, pointIndex) => ({
route,
point,
pointIndex
}))).sort((first, second) => Number(second.route.item.bus.componentId === mergeItem.bus.componentId) - Number(first.route.item.bus.componentId === mergeItem.bus.componentId) || distance(mergeItem.source, first.point) - distance(mergeItem.source, second.point));
let mergedRoute = null;
for (const candidate of candidates) {
for (const connectorPoints of getConnectorVariants2(mergeItem.source, candidate.point)) {
const connectorSegments = getSegments2(connectorPoints, traceWidth);
const clearsObstacles = connectorSegments.every((segment) => obstacles.every((obstacle) => obstacleBelongsToItem(obstacle, mergeItem) || distanceSegmentToObstacle(segment, obstacle) >= requiredObstacleDistance - EPSILON9));
const clearsRoutes = connectorSegments.every((segment) => routes.every((route) => route.item.netKey === mergeItem.netKey || route.segments.every((otherSegment) => distanceSegmentToSegment(segment.start, segment.end, otherSegment.start, otherSegment.end) >= requiredRouteDistance - EPSILON9)));
if (!clearsObstacles || !clearsRoutes)
continue;
const points = enforceStraightOr45DegreeSegments(compressPath2([
...connectorPoints,
...candidate.route.points.slice(candidate.pointIndex + 1)
]));
mergedRoute = {
item: mergeItem,
points,
segments: getSegments2(points, traceWidth)
};
break;
}
if (mergedRoute)
break;
}
if (!mergedRoute)
return null;
routes.push(mergedRoute);
reportProgress({
phase: "merge-same-net-routes",
processed: routes.length,
total: items.length,
grid: {
points: grid.points,
obstacleFreeNodes: grid.obstacleFreeNodes,
processedNodeCount: grid.nodeCount
},
segments: routes.flatMap((route) => route.segments)
});
yield;
}
const plans = routes.map((route) => buildPlan3(route, traceWidth));
if (!plansHaveRequiredClearance({
plans,
items,
obstacles,
traceWidth,
clearance
})) {
if (FANOUT_FLOW_DEBUG_ENABLED) {
console.error("single-layer adaptive exits failed exact clearance");
}
return null;
}
for (const plan of plans) {
const direction = getDirectionForBoundaryPoint(plan.exitPoint, grid.boundary);
if (!direction || availableDirections && !availableDirections.has(direction)) {
return null;
}
const item = items.find((candidate) => candidate.connection.connection.name === plan.connectionName);
item.bus.direction = direction;
const compatibleRegions = availableBoundaryRegions?.filter((region) => region.direction === direction);
const exitCoordinate = direction === "up" || direction === "down" ? plan.exitPoint.x : plan.exitPoint.y;
item.bus.preferredExit = compatibleRegions?.toSorted((first, second) => Math.abs(exitCoordinate - getRegionAnchor(first, grid.boundary)) - Math.abs(exitCoordinate - getRegionAnchor(second, grid.boundary)))[0]?.preferredExit ?? (direction === "up" ? "top" : direction === "down" ? "bottom" : direction);
plan.direction = direction;
}
const planByConnectionName = new Map(plans.map((plan) => [plan.connectionName, plan]));
return items.map((item) => planByConnectionName.get(item.connection.connection.name));
}
function* routeSingleLayerWithAdaptiveExitsSteps(params) {
const {
srj,
buses,
traceWidth,
clearance,
availableBoundaryRegions,
onProgress
} = params;
if (buses.some((bus) => bus.connections.length !== 1))
return null;
const items = buses.flatMap((bus) => bus.connections.map((connection) => ({
bus,
connection,
source: {
x: connection.sourcePoint.x,
y: connection.sourcePoint.y
},
netKey: getNetKey(connection)
})));
const topObstacles = srj.obstacles.filter((obstacle) => obstacle.layers.includes("top"));
const boundary = buses[0]?.sharedBoundary;
if (!boundary)
return [];
const sourcePoints = items.map((item) => item.source);
const reportProgress = (update) => {
onProgress?.(visualizeFlowProgress({ boundary, sourcePoints, traceWidth, update }), {
adaptivePhase: update.phase,
...update.direction ? { adaptiveDirection: update.direction } : {},
...update.processed !== undefined ? { adaptiveWorkUnit: update.processed } : {},
...update.total !== undefined ? { adaptiveWorkUnitCount: update.total } : {}
});
};
reportProgress({
phase: "prepare-flow-grid",
processed: 0,
total: topObstacles.length
});
const grid = yield* createFlowGridSteps({
boundary,
obstacles: topObstacles,
traceWidth,
clearance,
reportProgress
});
if (FANOUT_FLOW_DEBUG_ENABLED) {
console.error("single-layer adaptive-exit grid ready", {
nodeCount: grid.nodeCount
});
}
const adaptivePlans = yield* routeWithAdaptiveExitsSteps({
items,
grid,
obstacles: topObstacles,
traceWidth,
clearance,
availableBoundaryRegions,
reportProgress
});
if (adaptivePlans)
return adaptivePlans;
return null;
}
// node_modules/@tscircuit/fanout-solver/lib/route-single-layer-push-shove.ts
function isHorizontal2(direction) {
return direction === "left" || direction === "right";
}
function directionSign2(direction) {
return direction === "right" || direction === "up" ? 1 : -1;
}
function getAxis2(point, direction) {
return isHorizontal2(direction) ? point.x : point.y;
}
function getPerpendicularAxis3(point, direction) {
return isHorizontal2(direction) ? point.y : point.x;
}
function makePoint2(axis, perpendicularAxis, direction) {
return isHorizontal2(direction) ? { x: axis, y: perpendicularAxis } : { x: perpendicularAxis, y: axis };
}
function getExitAxis2(bus) {
switch (bus.direction) {
case "right":
return bus.sharedBoundary.maxX;
case "left":
return bus.sharedBoundary.minX;
case "up":
return bus.sharedBoundary.maxY;
case "down":
return bus.sharedBoundary.minY;
}
}
function compressPath3(points) {
if (points.length < 3)
return points;
const compressed = [points[0]];
for (let index = 1;index < points.length - 1; index++) {
const previous = compressed.at(-1);
const current = points[index];
const next = points[index + 1];
const incoming = {
x: Math.sign(current.x - previous.x),
y: Math.sign(current.y - previous.y)
};
const outgoing = {
x: Math.sign(next.x - current.x),
y: Math.sign(next.y - current.y)
};
if (incoming.x !== outgoing.x || incoming.y !== outgoing.y) {
compressed.push(current);
}
}
compressed.push(points.at(-1));
return compressed;
}
function getPathSegments(points, traceWidth) {
const segments = [];
for (let index = 1;index < points.length; index++) {
if (distance(points[index - 1], points[index]) < 0.000000001)
continue;
segments.push({
start: points[index - 1],
end: points[index],
width: traceWidth,
layer: "top"
});
}
return segments;
}
function getCornerChannelPrefixes(params) {
const { srj, bus, connection, traceWidth, clearance } = params;
const direction = bus.direction;
const sign = directionSign2(direction);
const source = {
x: connection.sourcePoint.x,
y: connection.sourcePoint.y
};
const sourceAxis = getAxis2(source, direction);
const sourceTrack = getPerpendicularAxis3(source, direction);
const requiredObstacleDistance = traceWidth / 2 + clearance;
const componentExitAxis = (() => {
switch (direction) {
case "right":
return bus.componentBounds.maxX + requiredObstacleDistance;
case "left":
return bus.componentBounds.minX - requiredObstacleDistance;
case "up":
return bus.componentBounds.maxY + requiredObstacleDistance;
case "down":
return bus.componentBounds.minY - requiredObstacleDistance;
}
})();
const obstacleDistanceForSegment = (segment) => Math.min(...srj.obstacles.filter((obstacle) => obstacle !== connection.sourceObstacle && obstacle.layers.includes("top")).map((obstacle) => distanceSegmentToObstacle(segment, obstacle)));
const directSegment = {
start: source,
end: makePoint2(componentExitAxis, sourceTrack, direction),
width: traceWidth,
layer: "top"
};
if (obstacleDistanceForSegment(directSegment) >= requiredObstacleDistance - 0.000000001) {
return null;
}
const perpendicularBounds = isHorizontal2(direction) ? {
minimum: bus.componentBounds.minY,
maximum: bus.componentBounds.maxY
} : {
minimum: bus.componentBounds.minX,
maximum: bus.componentBounds.maxX
};
const directionalDistance = sign * (componentExitAxis - sourceAxis);
const targetTrack = getPerpendicularAxis3(connection.exitTargetPoint ?? connection.targetPoint, direction);
const lanePitch = traceWidth + clearance;
const exitAxis = getExitAxis2(bus);
const candidates = [-1, 1].map((perpendicularSign) => {
const perpendicularExit = perpendicularSign < 0 ? perpendicularBounds.minimum - requiredObstacleDistance : perpendicularBounds.maximum + requiredObstacleDistance;
const perpendicularDistance = perpendicularSign * (perpendicularExit - sourceTrack);
const diagonalDistance = Math.max(directionalDistance, perpendicularDistance);
const diagonalEnd = makePoint2(sourceAxis + sign * diagonalDistance, sourceTrack + perpendicularSign * diagonalDistance, direction);
if (sign * (exitAxis - getAxis2(diagonalEnd, direction)) < lanePitch - 0.000000001) {
return null;
}
const points = [source, diagonalEnd];
const withinBoundary = points.every((point) => point.x >= bus.sharedBoundary.minX - 0.000000001 && point.x <= bus.sharedBoundary.maxX + 0.000000001 && point.y >= bus.sharedBoundary.minY - 0.000000001 && point.y <= bus.sharedBoundary.maxY + 0.000000001);
if (!withinBoundary)
return null;
const obstacleDistance = Math.min(...getPathSegments(points, traceWidth).map(obstacleDistanceForSegment));
if (obstacleDistance < requiredObstacleDistance - 0.000000001)
return null;
return {
points,
obstacleDistance,
targetDistance: Math.abs(getPerpendicularAxis3(diagonalEnd, direction) - targetTrack)
};
}).filter((candidate) => candidate !== null).toSorted((first, second) => second.obstacleDistance - first.obstacleDistance || first.targetDistance - second.targetDistance);
return candidates.length > 0 ? candidates.map((candidate) => candidate.points) : null;
}
function completeCornerChannelRoute(params) {
const { item, srj, acceptedSegments, traceWidth, clearance } = params;
const prefixes = item.cornerChannelPrefixes;
if (!prefixes)
return null;
const direction = item.direction;
const sign = directionSign2(direction);
const exitAxis = getExitAxis2(item.bus);
const lanePitch = traceWidth + clearance;
const requiredObstacleDistance = traceWidth / 2 + clearance;
const boundaryMinimum = isHorizontal2(direction) ? item.bus.sharedBoundary.minY : item.bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal2(direction) ? item.bus.sharedBoundary.maxY : item.bus.sharedBoundary.maxX;
const minimumTrack = boundaryMinimum + traceWidth / 2;
const maximumTrack = boundaryMaximum - traceWidth / 2;
const targetTrack = getPerpendicularAxis3(item.connection.exitTargetPoint ?? item.connection.targetPoint, direction);
const trackStep = lanePitch / 2;
const trackCandidates = new Set([
Math.max(minimumTrack, Math.min(maximumTrack, targetTrack))
]);
for (let track = Math.ceil(minimumTrack / trackStep) * trackStep;track <= maximumTrack + 0.000000001; track += trackStep) {
trackCandidates.add(Number(track.toFixed(9)));
}
const orderedTracks = [...trackCandidates].toSorted((first, second) => Math.abs(first - targetTrack) - Math.abs(second - targetTrack));
for (const prefix of prefixes) {
const diagonalEnd = prefix.at(-1);
const diagonalTrack = getPerpendicularAxis3(diagonalEnd, direction);
for (const track of orderedTracks) {
const shift = Math.abs(track - diagonalTrack);
const straightEnd = makePoint2(getAxis2(diagonalEnd, direction) + sign * lanePitch, diagonalTrack, direction);
const doglegEnd = makePoint2(getAxis2(straightEnd, direction) + sign * shift, track, direction);
if (sign * (exitAxis - getAxis2(doglegEnd, direction)) < -0.000000001) {
continue;
}
const boundaryPoint = makePoint2(exitAxis, track, direction);
const completionPoints = [
...prefix,
straightEnd,
...shift > 0.000000001 ? [doglegEnd] : []
];
if (distance(completionPoints.at(-1), boundaryPoint) > 0.000000001) {
completionPoints.push(boundaryPoint);
}
const points = compressPath3(completionPoints);
const segments = getPathSegments(points, traceWidth);
const clearsObstacles = segments.every((segment) => srj.obstacles.every((obstacle) => obstacle === item.connection.sourceObstacle || !obstacle.layers.includes("top") || distanceSegmentToObstacle(segment, obstacle) >= requiredObstacleDistance - 0.000000001));
if (!clearsObstacles)
continue;
const clearsRoutes = segments.every((segment) => acceptedSegments.every((acceptedSegment) => distanceSegmentToSegment(segment.start, segment.end, acceptedSegment.start, acceptedSegment.end) >= traceWidth + clearance - 0.000000001));
if (!clearsRoutes)
continue;
return { item, points, track };
}
}
return null;
}
function getMaximumObstacleClearDistributionShift(params) {
const {
route,
direction,
exitAxis,
signedShift,
maximumShift,
obstacles,
requiredObstacleDistance
} = params;
if (maximumShift <= 0.000000001 || Math.abs(signedShift) <= 0.000000001)
return 0;
const perpendicularSign = Math.sign(signedShift);
const shiftIsClear = (shift) => {
if (shift <= 0.000000001)
return true;
const segment = {
start: makePoint2(exitAxis - directionSign2(direction) * shift, route.track, direction),
end: makePoint2(exitAxis, route.track + perpendicularSign * shift, direction),
width: 0,
layer: "top"
};
return obstacles.every((obstacle) => obstacle === route.item.connection.sourceObstacle || !obstacle.layers.includes("top") || distanceSegmentToObstacle(segment, obstacle) >= requiredObstacleDistance - 0.000000001);
};
if (shiftIsClear(maximumShift))
return maximumShift;
let lowerShift = 0;
let upperShift = maximumShift;
for (let iteration = 0;iteration < 32; iteration++) {
const candidateShift = (lowerShift + upperShift) / 2;
if (shiftIsClear(candidateShift))
lowerShift = candidateShift;
else
upperShift = candidateShift;
}
return lowerShift;
}
function selectOrderedTracks(params) {
const { requestedTracks, currentTracks, candidateTracks, maximumShifts } = params;
const rowCount = requestedTracks.length + 1;
const columnCount = candidateTracks.length + 1;
const costs = Array.from({ length: rowCount }, () => new Float64Array(columnCount).fill(Number.POSITIVE_INFINITY));
const tookCandidate = Array.from({ length: rowCount }, () => new Uint8Array(columnCount));
costs[0].fill(0);
for (let row2 = 1;row2 < rowCount; row2++) {
for (let column2 = 1;column2 < columnCount; column2++) {
const skippedCost = costs[row2][column2 - 1];
const requestedShift = Math.abs(requestedTracks[row2 - 1] - candidateTracks[column2 - 1]);
const currentShift = Math.abs(currentTracks[row2 - 1] - candidateTracks[column2 - 1]);
const selectedCost = currentShift > maximumShifts[row2 - 1] + 0.000000001 ? Number.POSITIVE_INFINITY : costs[row2 - 1][column2 - 1] + requestedShift;
if (selectedCost < skippedCost) {
costs[row2][column2] = selectedCost;
tookCandidate[row2][column2] = 1;
} else {
costs[row2][column2] = skippedCost;
}
}
}
if (!Number.isFinite(costs.at(-1).at(-1)))
return null;
const selectedTracks = [];
let row = requestedTracks.length;
let column = candidateTracks.length;
while (row > 0 && column > 0) {
if (tookCandidate[row][column]) {
selectedTracks.push(candidateTracks[column - 1]);
row--;
}
column--;
}
if (row > 0)
return null;
return selectedTracks.reverse();
}
function getCandidateTracks(params) {
const {
direction,
activeRoutes,
obstacles,
boundaryMinimum,
boundaryMaximum,
traceWidth,
clearance,
requestedTracks,
maximumShifts
} = params;
const lanePitch = traceWidth + clearance;
const requiredObstacleDistance = traceWidth / 2 + clearance;
let selectedTracks = null;
let selectedCost = Number.POSITIVE_INFINITY;
for (const phase of Array.from({ length: Math.round(lanePitch / traceWidth) }, (_, index) => index * traceWidth)) {
const candidates = [];
const firstTrack = Math.ceil((boundaryMinimum - phase) / lanePitch) * lanePitch + phase;
for (let track = firstTrack;track <= boundaryMaximum + 0.000000001; track += lanePitch) {
const roundedTrack = Math.round(track / traceWidth) * traceWidth;
if (obstacles.every((obstacle) => {
const obstacleTrack = getPerpendicularAxis3(obstacle.center, direction);
const obstacleSize = isHorizontal2(direction) ? obstacle.height : obstacle.width;
return Math.abs(roundedTrack - obstacleTrack) >= obstacleSize / 2 + requiredObstacleDistance - 0.000000001;
})) {
candidates.push(roundedTrack);
}
}
const tracks = selectOrderedTracks({
requestedTracks,
currentTracks: activeRoutes.map((route) => route.track),
candidateTracks: candidates,
maximumShifts
});
if (!tracks)
continue;
const cost = tracks.reduce((sum, track, index) => sum + Math.abs(track - requestedTracks[index]), 0);
if (cost < selectedCost) {
selectedCost = cost;
selectedTracks = tracks;
}
}
return selectedTracks;
}
function getCornerSide2(corner, direction) {
switch (direction) {
case "up":
if (corner === "top-left")
return "minimum";
if (corner === "top-right")
return "maximum";
return null;
case "down":
if (corner === "bottom-left")
return "minimum";
if (corner === "bottom-right")
return "maximum";
return null;
case "left":
if (corner === "bottom-left")
return "minimum";
if (corner === "top-left")
return "maximum";
return null;
case "right":
if (corner === "bottom-right")
return "minimum";
if (corner === "top-right")
return "maximum";
return null;
}
}
function getFinalTrackTargets(params) {
const {
items,
direction,
boundaryMinimum,
boundaryMaximum,
traceWidth,
clearance,
borderDistribution,
currentTrackByConnectionName
} = params;
const getCurrentTrack = (item) => currentTrackByConnectionName.get(item.connection.connection.name) ?? getPerpendicularAxis3(item.source, direction);
const orderedItems = [...items].toSorted((first, second) => getCurrentTrack(first) - getCurrentTrack(second) || first.connection.connection.name.localeCompare(second.connection.connection.name));
const lanePitch = traceWidth + clearance;
const minimumLane = boundaryMinimum + traceWidth / 2;
const maximumLane = boundaryMaximum - traceWidth / 2;
const availableSpan = maximumLane - minimumLane;
const requiredSpan = lanePitch * Math.max(orderedItems.length - 1, 0);
if (availableSpan < requiredSpan - 0.000000001)
return null;
const sourceTracks = orderedItems.map(getCurrentTrack);
let targetTracks = [...sourceTracks];
let distributedPitch = lanePitch;
const enforcedConnectionNames = new Set;
if (borderDistribution === "even" && orderedItems.length > 1) {
const sourceMinimum = sourceTracks[0];
const sourceMaximum = sourceTracks.at(-1);
const desiredPitch = Math.max(lanePitch, (sourceMaximum - sourceMinimum) / (orderedItems.length - 1));
const buildOutwardTracks = (pitch) => {
const tracks = [...sourceTracks];
const upperMiddle = Math.floor(orderedItems.length / 2);
const lowerMiddle = orderedItems.length % 2 === 0 ? upperMiddle - 1 : upperMiddle;
if (lowerMiddle !== upperMiddle) {
const middleGap = tracks[upperMiddle] - tracks[lowerMiddle];
if (middleGap < pitch) {
const shove = (pitch - middleGap) / 2;
tracks[lowerMiddle] = tracks[lowerMiddle] - shove;
tracks[upperMiddle] = tracks[upperMiddle] + shove;
}
}
for (let index = lowerMiddle - 1;index >= 0; index--) {
tracks[index] = Math.min(tracks[index], tracks[index + 1] - pitch);
}
for (let index = upperMiddle + 1;index < tracks.length; index++) {
tracks[index] = Math.max(tracks[index], tracks[index - 1] + pitch);
}
return tracks;
};
const tracksFitBoundary = (tracks) => tracks[0] >= minimumLane - 0.000000001 && tracks.at(-1) <= maximumLane + 0.000000001;
targetTracks = buildOutwardTracks(desiredPitch);
if (tracksFitBoundary(targetTracks)) {
distributedPitch = desiredPitch;
} else {
let lowerPitch = lanePitch;
let upperPitch = desiredPitch;
targetTracks = buildOutwardTracks(lowerPitch);
if (!tracksFitBoundary(targetTracks))
return null;
for (let iteration = 0;iteration < 32; iteration++) {
const candidatePitch = (lowerPitch + upperPitch) / 2;
const candidateTracks = buildOutwardTracks(candidatePitch);
if (tracksFitBoundary(candidateTracks)) {
lowerPitch = candidatePitch;
targetTracks = candidateTracks;
} else {
upperPitch = candidatePitch;
}
}
distributedPitch = lowerPitch;
}
for (const item of orderedItems) {
enforcedConnectionNames.add(item.connection.connection.name);
}
}
const minimumCornerIndexes = [];
const maximumCornerIndexes = [];
for (let index = 0;index < orderedItems.length; index++) {
const item = orderedItems[index];
const preferredExit = item.bus.preferredExit;
if (!preferredExit?.includes("-"))
continue;
const cornerSide = getCornerSide2(preferredExit, direction);
if (!cornerSide)
return null;
enforcedConnectionNames.add(item.connection.connection.name);
if (cornerSide === "minimum")
minimumCornerIndexes.push(index);
else
maximumCornerIndexes.push(index);
}
if (minimumCornerIndexes.some((index, expectedIndex) => index !== expectedIndex) || maximumCornerIndexes.some((index, offset) => index !== orderedItems.length - maximumCornerIndexes.length + offset)) {
return null;
}
const cornerInset = lanePitch * 2;
if (minimumCornerIndexes.length > 0) {
targetTracks[0] = minimumLane + cornerInset;
for (let index = 1;index < minimumCornerIndexes.length; index++) {
const preservedPitch = Math.max(distributedPitch, sourceTracks[index] - sourceTracks[index - 1]);
targetTracks[index] = targetTracks[index - 1] + preservedPitch;
}
}
if (maximumCornerIndexes.length > 0) {
const lastIndex = orderedItems.length - 1;
targetTracks[lastIndex] = maximumLane - cornerInset;
for (let index = lastIndex - 1;index >= orderedItems.length - maximumCornerIndexes.length; index--) {
const preservedPitch = Math.max(distributedPitch, sourceTracks[index + 1] - sourceTracks[index]);
targetTracks[index] = targetTracks[index + 1] - preservedPitch;
}
}
return {
byConnectionName: new Map(orderedItems.map((item, index) => [
item.connection.connection.name,
targetTracks[index]
])),
enforcedConnectionNames
};
}
function buildPlan4(path) {
const { item, points, segments } = path;
const route = points.map((point, index) => ({
route_type: "wire",
x: point.x,
y: point.y,
width: segments[0]?.width ?? 0.1,
layer: "top",
...index === 0 && item.connection.sourcePoint.pcb_port_id ? { start_pcb_port_id: item.connection.sourcePoint.pcb_port_id } : {}
}));
const outputIds = createFanoutOutputIds({
connectionName: item.connection.connection.name,
sourcePointIndex: item.connection.sourcePointIndex
});
return {
busId: item.bus.busId,
connectionName: item.connection.connection.name,
connectionIndex: item.connection.connectionIndex,
sourcePointIndex: item.connection.sourcePointIndex,
sourcePoint: item.connection.sourcePoint,
sourceObstacle: item.connection.sourceObstacle,
sourceLayer: item.connection.sourceLayer,
targetPoint: item.connection.targetPoint,
targetLayer: "top",
termination: item.bus.termination,
direction: item.direction,
...item.bus.exitEdge ? { exitEdge: item.bus.exitEdge } : {},
exitPoint: points.at(-1),
trace: {
type: "pcb_trace",
pcb_trace_id: outputIds.traceId,
connection_name: item.connection.connection.name,
connectsTo: [
...item.connection.sourcePoint.pointId ? [item.connection.sourcePoint.pointId] : [],
...item.connection.sourcePoint.pcb_port_id ? [item.connection.sourcePoint.pcb_port_id] : [],
outputIds.boundaryExitPointId
],
route
},
segments,
length: segments.reduce((total, segment) => total + distance(segment.start, segment.end), 0)
};
}
function routesAreClear(params) {
const { paths, obstacles, traceWidth, clearance } = params;
const requiredObstacleDistance = traceWidth / 2 + clearance;
const requiredCenterDistance = traceWidth + clearance;
for (const path of paths) {
if (Math.abs(getAxis2(path.points.at(-1), path.item.direction) - getExitAxis2(path.item.bus)) > 0.000001) {
return false;
}
for (const segment of path.segments) {
for (const obstacle of obstacles) {
if (obstacle === path.item.connection.sourceObstacle || !obstacle.layers.includes("top")) {
continue;
}
if (distanceSegmentToObstacle(segment, obstacle) < requiredObstacleDistance - 0.000000001) {
return false;
}
}
}
}
for (let firstIndex = 0;firstIndex < paths.length; firstIndex++) {
for (let secondIndex = firstIndex + 1;secondIndex < paths.length; secondIndex++) {
for (const firstSegment of paths[firstIndex].segments) {
for (const secondSegment of paths[secondIndex].segments) {
if (distanceSegmentToSegment(firstSegment.start, firstSegment.end, secondSegment.start, secondSegment.end) < requiredCenterDistance - 0.000000001) {
return false;
}
}
}
}
}
return true;
}
function routeSingleLayerWithPushAndShove(params) {
const { srj, buses, traceWidth, clearance, borderDistribution } = params;
const requiredObstacleDistance = traceWidth / 2 + clearance;
const recordsByConnectionName = new Map;
const enforcedFinalTargets = new Map;
const items = buses.flatMap((bus) => bus.connections.map((connection) => {
const source = {
x: connection.sourcePoint.x,
y: connection.sourcePoint.y
};
const cornerChannelPrefixes = getCornerChannelPrefixes({
srj,
bus,
connection,
traceWidth,
clearance
});
return {
bus,
connection,
direction: bus.direction,
source,
...cornerChannelPrefixes ? { cornerChannelPrefixes } : {}
};
}));
for (const direction of [
"left",
"right",
"up",
"down"
]) {
const sign = directionSign2(direction);
const directionItems = items.filter((item) => item.direction === direction && !item.cornerChannelPrefixes);
if (directionItems.length === 0)
continue;
const boundaryMinimum = isHorizontal2(direction) ? directionItems[0].bus.sharedBoundary.minY : directionItems[0].bus.sharedBoundary.minX;
const boundaryMaximum = isHorizontal2(direction) ? directionItems[0].bus.sharedBoundary.maxY : directionItems[0].bus.sharedBoundary.maxX;
const exitAxis = getExitAxis2(directionItems[0].bus);
const sourcesByEventKey = new Map;
const obstaclesByEventKey = new Map;
const axisByEventKey = new Map;
for (const item of directionItems) {
const axis = getAxis2(item.source, direction);
const key = axis.toFixed(6);
axisByEventKey.set(key, axis);
const eventSources = sourcesByEventKey.get(key) ?? [];
eventSources.push(item);
sourcesByEventKey.set(key, eventSources);
}
for (const [eventKey, eventSources] of sourcesByEventKey) {
const eventAxis = axisByEventKey.get(eventKey);
const eventComponentIds = new Set(eventSources.map((item) => item.bus.componentId));
obstaclesByEventKey.set(eventKey, srj.obstacles.filter((obstacle) => obstacle.layers.includes("top") && !!obstacle.componentId && eventComponentIds.has(obstacle.componentId) && Math.abs(getAxis2(obstacle.center, direction) - eventAxis) < 0.000001));
}
const eventAxes = [...axisByEventKey.values()].toSorted((a, b) => sign * (a - b));
const active = new Map;
for (let eventIndex = 0;eventIndex < eventAxes.length; eventIndex++) {
const eventAxis = eventAxes[eventIndex];
const eventKey = eventAxis.toFixed(6);
for (const item of sourcesByEventKey.get(eventKey) ?? []) {
const directionalPadSize = isHorizontal2(direction) ? item.connection.sourceObstacle.width : item.connection.sourceObstacle.height;
const sourceEscapeDistance = Math.max(item.connection.sourceObstacle.width, item.connection.sourceObstacle.height) < 1.5 ? directionalPadSize / 2 + requiredObstacleDistance : 0;
const sourceEscapePoint = makePoint2(eventAxis + sign * sourceEscapeDistance, getPerpendicularAxis3(item.source, direction), direction);
active.set(item.connection.connection.name, {
item,
points: distance(item.source, sourceEscapePoint) > 0.000000001 ? [item.source, sourceEscapePoint] : [item.source],
track: getPerpendicularAxis3(item.source, direction)
});
}
const nextAxis = eventAxes[eventIndex + 1] ?? exitAxis;
if (active.size === 0 || sign * (nextAxis - eventAxis) <= 0.000000001) {
continue;
}
const orderedRoutes = [...active.values()].toSorted((a, b) => a.track - b.track || a.item.connection.connection.name.localeCompare(b.item.connection.connection.name));
const lookaheadObstacles = eventAxes.slice(eventIndex + 1, eventIndex + 3).flatMap((axis) => obstaclesByEventKey.get(axis.toFixed(6)) ?? []);
const maximumShifts = orderedRoutes.map((route) => Math.abs(nextAxis - getAxis2(route.points.at(-1), direction)));
const selectedTracks = getCandidateTracks({
direction,
activeRoutes: orderedRoutes,
obstacles: lookaheadObstacles,
boundaryMinimum,
boundaryMaximum,
traceWidth,
clearance,
requestedTracks: orderedRoutes.map((route) => route.track),
maximumShifts
});
if (!selectedTracks)
return null;
for (let index = 0;index < orderedRoutes.length; index++) {
const route = orderedRoutes[index];
const selectedTrack = selectedTracks[index];
const shift = Math.abs(selectedTrack - route.track);
const routeStartAxis = getAxis2(route.points.at(-1), direction);
const diagonalEnd = makePoint2(routeStartAxis + sign * shift, selectedTrack, direction);
if (distance(route.points.at(-1), diagonalEnd) > 0.000000001) {
route.points.push(diagonalEnd);
}
const nextPoint = makePoint2(nextAxis, selectedTrack, direction);
if (distance(route.points.at(-1), nextPoint) > 0.000000001) {
route.points.push(nextPoint);
}
route.track = selectedTrack;
}
}
const finalTrackTargets = getFinalTrackTargets({
items: directionItems,
direction,
boundaryMinimum,
boundaryMaximum,
traceWidth,
clearance,
borderDistribution,
currentTrackByConnectionName: new Map([...active.values()].map((route) => [
route.item.connection.connection.name,
route.track
]))
});
if (!finalTrackTargets)
return null;
const orderedActiveRoutes = [...active.values()].toSorted((first, second) => first.track - second.track || first.item.connection.connection.name.localeCompare(second.item.connection.connection.name));
const distributionStartOffsetByConnectionName = new Map;
const lanePitch = traceWidth + clearance;
let nextMinimumStartOffset = lanePitch;
for (const route of orderedActiveRoutes) {
const connectionName = route.item.connection.connection.name;
if (!finalTrackTargets.enforcedConnectionNames.has(connectionName)) {
continue;
}
const targetTrack = finalTrackTargets.byConnectionName.get(connectionName);
const signedShift = targetTrack - route.track;
if (signedShift >= -0.000000001)
continue;
distributionStartOffsetByConnectionName.set(connectionName, nextMinimumStartOffset);
nextMinimumStartOffset += lanePitch * 2;
}
let nextMaximumStartOffset = lanePitch;
for (let index = orderedActiveRoutes.length - 1;index >= 0; index--) {
const route = orderedActiveRoutes[index];
const connectionName = route.item.connection.connection.name;
if (!finalTrackTargets.enforcedConnectionNames.has(connectionName)) {
continue;
}
const targetTrack = finalTrackTargets.byConnectionName.get(connectionName);
const signedShift = targetTrack - route.track;
if (signedShift <= 0.000000001)
continue;
distributionStartOffsetByConnectionName.set(connectionName, nextMaximumStartOffset);
nextMaximumStartOffset += lanePitch * 2;
}
for (const route of orderedActiveRoutes) {
const connectionName = route.item.connection.connection.name;
const intendedTargetTrack = finalTrackTargets.byConnectionName.get(connectionName) ?? route.track;
const startOffset = distributionStartOffsetByConnectionName.get(connectionName) ?? 0;
const previousPoint = route.points.at(-2);
const availableDepth = previousPoint ? sign * (exitAxis - getAxis2(previousPoint, direction)) : 0;
const intendedSignedShift = intendedTargetTrack - route.track;
const maximumRunwayShift = Math.min(Math.abs(intendedSignedShift), Math.max(0, availableDepth - startOffset));
const maximumShift = getMaximumObstacleClearDistributionShift({
route,
direction,
exitAxis,
signedShift: intendedSignedShift,
maximumShift: maximumRunwayShift,
obstacles: srj.obstacles,
requiredObstacleDistance
});
const adjustedShift = Math.sign(intendedSignedShift) * maximumShift;
finalTrackTargets.byConnectionName.set(connectionName, route.track + adjustedShift);
}
for (const connectionName of finalTrackTargets.enforcedConnectionNames) {
enforcedFinalTargets.set(connectionName, finalTrackTargets.byConnectionName.get(connectionName));
}
for (const route of orderedActiveRoutes) {
const connectionName = route.item.connection.connection.name;
const targetTrack = finalTrackTargets.byConnectionName.get(connectionName) ?? route.track;
const shift = Math.abs(targetTrack - route.track);
const distributionStartOffset = distributionStartOffsetByConnectionName.get(connectionName) ?? 0;
const distributionDepth = shift > 0.000000001 ? distributionStartOffset + shift : 0;
const distributionStartAxis = exitAxis - sign * distributionDepth;
const lastPoint = route.points.at(-1);
if (Math.abs(getAxis2(lastPoint, direction) - exitAxis) > 0.000001) {
return null;
}
const previousPoint = route.points.at(-2);
if (previousPoint && (Math.abs(getPerpendicularAxis3(previousPoint, direction) - route.track) > 0.000001 || sign * (distributionStartAxis - getAxis2(previousPoint, direction)) < -0.000001)) {
return null;
}
const stagingPoint = makePoint2(distributionStartAxis, route.track, direction);
if (previousPoint && distance(previousPoint, stagingPoint) < 0.000000001) {
route.points.pop();
} else {
route.points[route.points.length - 1] = stagingPoint;
}
}
for (const route of orderedActiveRoutes) {
const connectionName = route.item.connection.connection.name;
const targetTrack = finalTrackTargets.byConnectionName.get(connectionName) ?? route.track;
const shift = Math.abs(targetTrack - route.track);
const distributionStartOffset = distributionStartOffsetByConnectionName.get(connectionName) ?? 0;
if (shift > 0.000000001 && distributionStartOffset > 0.000000001) {
route.points.push(makePoint2(getAxis2(route.points.at(-1), direction) + sign * distributionStartOffset, route.track, direction));
}
if (shift > 0.000000001) {
route.points.push(makePoint2(getAxis2(route.points.at(-1), direction) + sign * shift, targetTrack, direction));
route.track = targetTrack;
}
const boundaryPoint = makePoint2(exitAxis, route.track, direction);
if (distance(route.points.at(-1), boundaryPoint) > 0.000000001) {
if (sign * (exitAxis - getAxis2(route.points.at(-1), direction)) < -0.000001) {
return null;
}
route.points.push(boundaryPoint);
}
recordsByConnectionName.set(connectionName, route);
}
}
const acceptedSegments = [...recordsByConnectionName.values()].flatMap((route) => getPathSegments(compressPath3(route.points), traceWidth));
for (const item of items) {
if (!item.cornerChannelPrefixes)
continue;
const route = completeCornerChannelRoute({
item,
srj,
acceptedSegments,
traceWidth,
clearance
});
if (!route)
return null;
recordsByConnectionName.set(item.connection.connection.name, route);
acceptedSegments.push(...getPathSegments(compressPath3(route.points), traceWidth));
}
if (recordsByConnectionName.size !== items.length)
return null;
const maximumTargetError = (traceWidth + clearance) / 2 + 0.000001;
for (const [connectionName, targetTrack] of enforcedFinalTargets) {
const route = recordsByConnectionName.get(connectionName);
if (!route || Math.abs(route.track - targetTrack) > maximumTargetError) {
return null;
}
}
const paths = items.map((item) => {
const activeRoute = recordsByConnectionName.get(item.connection.connection.name);
const points = compressPath3(activeRoute.points);
return {
item,
points,
segments: getPathSegments(points, traceWidth)
};
});
if (!routesAreClear({
paths,
obstacles: srj.obstacles,
traceWidth,
clearance
})) {
return null;
}
return paths.map(buildPlan4);
}
// node_modules/@tscircuit/fanout-solver/lib/runtime-process.ts
var getRuntimeProcess = (runtime) => runtime.process ?? { env: {} };
// node_modules/@tscircuit/fanout-solver/lib/shorten-bus-plans.ts
function shortenBusPlans(params) {
const { bus } = params;
let plans = [...params.plans];
if (bus.maxLengthSkew === undefined)
return plans;
const busPlans = plans.filter((plan) => plan.busId === bus.busId).toSorted((first, second) => second.length - first.length);
for (const plan of busPlans) {
const minimumLength = Math.min(...plans.filter((p) => p.busId === bus.busId).map((p) => p.length));
if (plan.length - minimumLength <= bus.maxLengthSkew)
continue;
if (!plan.via || plan.additionalVias?.length || plan.planeEndpointVia || plan.segments.filter((segment) => segment.layer === plan.sourceLayer).length !== 1)
continue;
const connection = bus.connections.find((c) => c.connectionIndex === plan.connectionIndex);
for (const alignGridToPads of [true, false]) {
const candidate = routeViaMinimalWinding({
...params,
bus: {
...bus,
connections: [connection],
routableEscapeLayers: [plan.targetLayer]
},
terminals: [
{ connection, viaPoint: plan.via.center, exitPoint: plan.exitPoint }
],
targetLayer: plan.targetLayer,
acceptedPlans: plans.filter((p) => p !== plan),
allowBlindAndBuriedVias: false,
gridStepDivisor: 2,
alignGridToPads,
maximumRouteOrderAttempts: 1
})?.[0];
if (!candidate || candidate.length >= plan.length - 0.000001)
continue;
const nextPlans = plans.map((p) => p === plan ? candidate : p);
if (!fanoutPlansAreClear({
...params,
plans: nextPlans,
allowBlindAndBuriedVias: false
}))
continue;
plans = nextPlans;
break;
}
}
return plans;
}
// node_modules/@tscircuit/fanout-solver/lib/validate-fanout-solution.ts
var EPSILON10 = 0.000001;
function pointsMatch3(first, second) {
return distance(first, second) <= EPSILON10;
}
function getPointLayers3(point) {
return "layer" in point ? [point.layer] : point.layers;
}
function getPlanSegments2(plan) {
return [...plan.segments, ...plan.planeEndpointSegments ?? []];
}
function getPlanVias4(plan) {
return [
plan.via,
...plan.additionalVias ?? [],
plan.planeEndpointVia
].filter((via) => Boolean(via));
}
function connectionPointsMatch(first, second) {
return pointsMatch3(first, second) && getPointLayers3(first).join("\x00") === getPointLayers3(second).join("\x00") && first.pointId === second.pointId && first.pcb_port_id === second.pcb_port_id;
}
function pointIsOnBoundary(point, boundary) {
const inside = point.x >= boundary.minX - EPSILON10 && point.x <= boundary.maxX + EPSILON10 && point.y >= boundary.minY - EPSILON10 && point.y <= boundary.maxY + EPSILON10;
const onEdge = Math.abs(point.x - boundary.minX) <= EPSILON10 || Math.abs(point.x - boundary.maxX) <= EPSILON10 || Math.abs(point.y - boundary.minY) <= EPSILON10 || Math.abs(point.y - boundary.maxY) <= EPSILON10;
return inside && onEdge;
}
function pointIsOnBoundaryEdge(point, edge, boundary) {
const inside = point.x >= boundary.minX - EPSILON10 && point.x <= boundary.maxX + EPSILON10 && point.y >= boundary.minY - EPSILON10 && point.y <= boundary.maxY + EPSILON10;
if (!inside)
return false;
switch (edge) {
case "left":
return Math.abs(point.x - boundary.minX) <= EPSILON10;
case "right":
return Math.abs(point.x - boundary.maxX) <= EPSILON10;
case "top":
return Math.abs(point.y - boundary.maxY) <= EPSILON10;
case "bottom":
return Math.abs(point.y - boundary.minY) <= EPSILON10;
}
}
function pointIsInsideBounds3(point, bounds) {
return point.x >= bounds.minX - EPSILON10 && point.x <= bounds.maxX + EPSILON10 && point.y >= bounds.minY - EPSILON10 && point.y <= bounds.maxY + EPSILON10;
}
function addIssue2(issues, code, message, plan, otherConnectionName) {
issues.push({
code,
message,
...plan ? {
connectionName: plan.connectionName,
busId: plan.busId
} : {},
...otherConnectionName ? { otherConnectionName } : {}
});
}
function extractTraceSegments(params) {
const { trace, plan, issues } = params;
const segments = [];
let previousWire;
let pendingVia;
for (const routePoint of trace.route) {
if (routePoint.route_type === "via") {
if (!previousWire || !pointsMatch3(previousWire, routePoint) || previousWire.layer !== routePoint.from_layer) {
addIssue2(issues, "disconnected-trace", `Trace ${trace.pcb_trace_id} reaches a via without a matching ${routePoint.from_layer} wire endpoint`, plan);
}
pendingVia = routePoint;
continue;
}
if (routePoint.route_type !== "wire") {
addIssue2(issues, "unsupported-route-point", `Trace ${trace.pcb_trace_id} contains unsupported ${routePoint.route_type} geometry`, plan);
continue;
}
if (pendingVia) {
if (!pointsMatch3(routePoint, pendingVia) || routePoint.layer !== pendingVia.to_layer) {
addIssue2(issues, "disconnected-trace", `Trace ${trace.pcb_trace_id} does not continue from its via on ${pendingVia.to_layer}`, plan);
}
previousWire = routePoint;
pendingVia = undefined;
continue;
}
if (previousWire) {
if (previousWire.layer !== routePoint.layer) {
addIssue2(issues, "disconnected-trace", `Trace ${trace.pcb_trace_id} changes from ${previousWire.layer} to ${routePoint.layer} without a via`, plan);
} else if (!pointsMatch3(previousWire, routePoint)) {
segments.push({
start: { x: previousWire.x, y: previousWire.y },
end: { x: routePoint.x, y: routePoint.y },
width: routePoint.width,
layer: routePoint.layer
});
}
}
previousWire = routePoint;
}
if (pendingVia) {
addIssue2(issues, "disconnected-trace", `Trace ${trace.pcb_trace_id} ends at a via without a wire on ${pendingVia.to_layer}`, plan);
}
return segments;
}
function validatePlanStructure(params) {
const { plan, preparedBus, inputSrj, outputSrj, sharedBoundary, issues } = params;
const inputConnection = inputSrj.connections[plan.connectionIndex];
if (!inputConnection || inputConnection.name !== plan.connectionName) {
addIssue2(issues, "connection-mismatch", `Plan index ${plan.connectionIndex} does not identify connection ${plan.connectionName}`, plan);
return;
}
const preparedConnection = preparedBus?.connections.find((connection) => connection.connectionIndex === plan.connectionIndex);
if (!preparedConnection || preparedConnection.sourcePointIndex !== plan.sourcePointIndex || !connectionPointsMatch(preparedConnection.sourcePoint, plan.sourcePoint) || preparedConnection.sourceObstacle.obstacleId !== plan.sourceObstacle.obstacleId) {
addIssue2(issues, "source-mismatch", `Plan ${plan.connectionName} does not start at its prepared component endpoint`, plan);
}
if (preparedBus?.termination.type !== plan.termination.type) {
addIssue2(issues, "termination-mismatch", `Plan ${plan.connectionName} does not use its bus termination`, plan);
}
const expectedCornerBandSide = getCornerBandSide(preparedBus?.exitEdge, preparedBus?.preferredExit);
if (preparedBus?.exitEdge !== plan.exitEdge || expectedCornerBandSide !== plan.cornerBandSide) {
addIssue2(issues, "output-exit-mismatch", `Plan ${plan.connectionName} does not retain its prepared boundary edge and band`, plan);
} else if (preparedBus?.exitEdge && !pointIsOnBoundaryEdge(plan.exitPoint, preparedBus.exitEdge, sharedBoundary)) {
addIssue2(issues, "output-exit-mismatch", `Plan ${plan.connectionName} does not terminate on its declared ${preparedBus.exitEdge} edge`, plan);
}
const isAllowedViaInPadPlaneTermination = inputSrj.allowViaInPad === true && plan.termination.type === "plane" && plan.via !== undefined && pointsMatch3(plan.via.center, plan.sourcePoint) && pointsMatch3(plan.exitPoint, plan.sourcePoint);
if (!isAllowedViaInPadPlaneTermination && (plan.segments.length === 0 || plan.length <= EPSILON10)) {
addIssue2(issues, "not-broken-out", `Plan ${plan.connectionName} has no non-zero escape geometry`, plan);
} else {
const routableBounds = {
minX: Math.min(inputSrj.bounds.minX, sharedBoundary.minX),
maxX: Math.max(inputSrj.bounds.maxX, sharedBoundary.maxX),
minY: Math.min(inputSrj.bounds.minY, sharedBoundary.minY),
maxY: Math.max(inputSrj.bounds.maxY, sharedBoundary.maxY)
};
if (plan.segments.some((segment) => !pointIsInsideBounds3(segment.start, routableBounds) || !pointIsInsideBounds3(segment.end, routableBounds))) {
addIssue2(issues, "outside-routing-bounds", `Plan ${plan.connectionName} leaves the routable SRJ/shared-boundary area`, plan);
}
if (plan.segments[0] && !pointsMatch3(plan.segments[0].start, plan.sourcePoint)) {
addIssue2(issues, "disconnected-trace", `Plan ${plan.connectionName} does not start at its source pad`, plan);
}
if (plan.segments.at(-1) && !pointsMatch3(plan.segments.at(-1).end, plan.exitPoint)) {
addIssue2(issues, "disconnected-trace", `Plan ${plan.connectionName} does not end at its declared exit`, plan);
}
for (let index = 1;index < plan.segments.length; index++) {
const previous = plan.segments[index - 1];
const current = plan.segments[index];
if (!pointsMatch3(previous.end, current.start)) {
addIssue2(issues, "disconnected-trace", `Plan ${plan.connectionName} has a gap between route segments`, plan);
}
const transitionVia = getPlanVias4(plan).find((via) => pointsMatch3(previous.end, via.center) && via.spanLayers.includes(previous.layer) && via.spanLayers.includes(current.layer));
if (previous.layer !== current.layer && !transitionVia) {
addIssue2(issues, "disconnected-trace", `Plan ${plan.connectionName} changes layers without a connecting via`, plan);
}
}
}
const measuredLength = [
...plan.segments,
...plan.planeEndpointSegments ?? []
].reduce((total, segment) => total + distance(segment.start, segment.end), 0);
if (Math.abs(measuredLength - plan.length) > 0.000001) {
addIssue2(issues, "plan-length-mismatch", `Plan ${plan.connectionName} declares ${plan.length.toFixed(6)}mm but contains ${measuredLength.toFixed(6)}mm of routed copper`, plan);
}
const traceSegments = extractTraceSegments({
trace: plan.trace,
plan,
issues
});
if (traceSegments.length !== plan.segments.length || traceSegments.some((segment, index) => {
const declared = plan.segments[index];
return !declared || segment.layer !== declared.layer || Math.abs(segment.width - declared.width) > EPSILON10 || !pointsMatch3(segment.start, declared.start) || !pointsMatch3(segment.end, declared.end);
})) {
addIssue2(issues, "trace-plan-mismatch", `Trace ${plan.trace.pcb_trace_id} does not encode its declared route segments`, plan);
}
if (plan.planeEndpointTrace || plan.planeEndpointSegments || plan.planeEndpointVia) {
if (!plan.planeEndpointTrace || !plan.planeEndpointSegments || !plan.planeEndpointVia) {
addIssue2(issues, "trace-plan-mismatch", `Plane endpoint geometry for ${plan.connectionName} is incomplete`, plan);
} else {
const endpointSegments = extractTraceSegments({
trace: plan.planeEndpointTrace,
plan,
issues
});
if (endpointSegments.length !== plan.planeEndpointSegments.length || endpointSegments.some((segment, index) => {
const declared = plan.planeEndpointSegments?.[index];
return !declared || segment.layer !== declared.layer || Math.abs(segment.width - declared.width) > EPSILON10 || !pointsMatch3(segment.start, declared.start) || !pointsMatch3(segment.end, declared.end);
}) || distance(plan.planeEndpointVia.center, plan.targetPoint) <= EPSILON10) {
addIssue2(issues, "trace-plan-mismatch", `Trace ${plan.planeEndpointTrace.pcb_trace_id} does not encode a valid offset plane-to-endpoint dogbone`, plan);
}
}
}
const firstRoutePoint = plan.trace.route.find((routePoint) => ("x" in routePoint) && ("y" in routePoint));
const lastRoutePoint = [...plan.trace.route].reverse().find((routePoint) => ("x" in routePoint) && ("y" in routePoint));
if (!firstRoutePoint || !lastRoutePoint || !pointsMatch3(firstRoutePoint, plan.sourcePoint) || !pointsMatch3(lastRoutePoint, plan.exitPoint)) {
addIssue2(issues, "disconnected-trace", `Trace ${plan.trace.pcb_trace_id} does not span its source and exit`, plan);
}
const outputConnection = outputSrj.connections.find((connection) => connection.name === plan.connectionName);
if (plan.termination.type === "boundary") {
if (!outputConnection) {
addIssue2(issues, "output-connection-missing", `Boundary connection ${plan.connectionName} was removed from the output`, plan);
} else {
const outputSource = outputConnection.pointsToConnect[plan.sourcePointIndex];
if (!outputSource || !pointsMatch3(outputSource, plan.exitPoint) || !("layer" in outputSource) || outputSource.layer !== plan.targetLayer) {
addIssue2(issues, "output-exit-mismatch", `Output connection ${plan.connectionName} is not attached to its fanout exit`, plan);
}
for (let index = 0;index < inputConnection.pointsToConnect.length; index++) {
if (index === plan.sourcePointIndex)
continue;
const inputPoint = inputConnection.pointsToConnect[index];
const outputPoint = outputConnection.pointsToConnect[index];
if (!inputPoint || !outputPoint || !connectionPointsMatch(inputPoint, outputPoint)) {
addIssue2(issues, "downstream-endpoint-lost", `Output connection ${plan.connectionName} did not retain downstream endpoint ${index}`, plan);
}
}
}
} else if (outputConnection) {
addIssue2(issues, "plane-connection-retained", `Plane-terminated connection ${plan.connectionName} remains in the output`, plan);
}
}
function plansHaveConnectedCopper(first, second) {
const firstSegments = getPlanSegments2(first);
const secondSegments = getPlanSegments2(second);
const firstVias = getPlanVias4(first);
const secondVias = getPlanVias4(second);
for (const firstSegment of firstSegments) {
for (const secondSegment of secondSegments) {
if (firstSegment.layer === secondSegment.layer && distanceSegmentToSegment(firstSegment.start, firstSegment.end, secondSegment.start, secondSegment.end) <= (firstSegment.width + secondSegment.width) / 2 + EPSILON10) {
return true;
}
}
for (const secondVia of secondVias) {
if (secondVia.spanLayers.includes(firstSegment.layer) && distancePointToSegment(secondVia.center, firstSegment.start, firstSegment.end) <= secondVia.diameter / 2 + firstSegment.width / 2 + EPSILON10) {
return true;
}
}
}
for (const firstVia of firstVias) {
for (const secondSegment of secondSegments) {
if (firstVia.spanLayers.includes(secondSegment.layer) && distancePointToSegment(firstVia.center, secondSegment.start, secondSegment.end) <= firstVia.diameter / 2 + secondSegment.width / 2 + EPSILON10) {
return true;
}
}
for (const secondVia of secondVias) {
if (firstVia.spanLayers.some((layer) => secondVia.spanLayers.includes(layer)) && distance(firstVia.center, secondVia.center) <= (firstVia.diameter + secondVia.diameter) / 2 + EPSILON10) {
return true;
}
}
}
return false;
}
function validateBreakoutConnectivity(params) {
const { plans, inputSrj, sharedBoundary, issues } = params;
const connectedPlans = new Set;
const neighboringPlans = new Map;
for (const plan of plans)
neighboringPlans.set(plan, []);
for (let firstIndex = 0;firstIndex < plans.length; firstIndex++) {
const first = plans[firstIndex];
if (first.termination.type === "plane" ? Boolean(first.via) : first.segments.some((segment) => pointIsOnBoundary(segment.start, sharedBoundary) || pointIsOnBoundary(segment.end, sharedBoundary))) {
connectedPlans.add(first);
}
for (let secondIndex = firstIndex + 1;secondIndex < plans.length; secondIndex++) {
const second = plans[secondIndex];
if (!connectionsShareElectricalNet(inputSrj, first.connectionName, second.connectionName) || !plansHaveConnectedCopper(first, second)) {
continue;
}
neighboringPlans.get(first).push(second);
neighboringPlans.get(second).push(first);
}
}
const queue = [...connectedPlans];
while (queue.length > 0) {
const plan = queue.shift();
for (const neighbor of neighboringPlans.get(plan) ?? []) {
if (connectedPlans.has(neighbor))
continue;
connectedPlans.add(neighbor);
queue.push(neighbor);
}
}
for (const plan of plans) {
if (connectedPlans.has(plan))
continue;
addIssue2(issues, "not-broken-out", plan.termination.type === "boundary" ? `Connection ${plan.connectionName} has no continuous same-net copper path to the shared boundary` : `Plane connection ${plan.connectionName} has no terminating via`, plan);
}
return connectedPlans;
}
function validateClearances(params) {
const { plans, inputSrj, clearance, allowBlindAndBuriedVias, issues } = params;
for (const plan of plans) {
const segments = getPlanSegments2(plan);
for (let segmentIndex = 0;segmentIndex < segments.length; segmentIndex++) {
const segment = segments[segmentIndex];
for (const obstacle of inputSrj.obstacles) {
if (!obstacle.layers.includes(segment.layer))
continue;
if (obstacleSharesElectricalNet(inputSrj, obstacle, plan.connectionName)) {
continue;
}
if (segmentIndex === 0 && obstacle.obstacleId === plan.sourceObstacle.obstacleId && segment.layer === plan.sourceLayer) {
continue;
}
if (segmentIsLegalTerminalBodyEscape({
inputSrj,
segment,
bodyObstacle: obstacle,
connectionName: plan.connectionName
})) {
continue;
}
const actual = distanceSegmentToObstacle(segment, obstacle);
const required = segment.width / 2 + clearance;
if (actual < required - 0.000000001) {
addIssue2(issues, "obstacle-clearance", `Trace ${plan.connectionName} on ${segment.layer} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId}; ${required.toFixed(4)}mm is required`, plan);
}
}
}
for (const via of getPlanVias4(plan)) {
for (const obstacle of inputSrj.obstacles) {
if (!obstacle.layers.some((layer) => via.spanLayers.includes(layer)) || obstacleSharesElectricalNet(inputSrj, obstacle, plan.connectionName)) {
continue;
}
const actual = distancePointToObstacle(via.center, obstacle);
const required = via.diameter / 2 + clearance;
if (actual < required - 0.000000001) {
addIssue2(issues, "via-obstacle-clearance", `Via ${plan.connectionName} is ${actual.toFixed(4)}mm from different-net obstacle ${obstacle.obstacleId} on its layer span; ${required.toFixed(4)}mm is required`, plan);
}
}
}
for (const traceCopper of getAllRoutedTraceCopper(inputSrj, allowBlindAndBuriedVias)) {
if (plan.connectionName === traceCopper.connectionName || connectionsShareElectricalNet(inputSrj, plan.connectionName, traceCopper.connectionName)) {
continue;
}
for (const segment of getPlanSegments2(plan)) {
for (const existingSegment of traceCopper.segments) {
if (!segmentsAreClear(segment, existingSegment, clearance)) {
addIssue2(issues, "different-net-trace-clearance", `Trace ${plan.connectionName} violates routed trace ${traceCopper.trace.pcb_trace_id} on ${segment.layer}`, plan, traceCopper.connectionName);
}
}
for (const existingVia of traceCopper.vias) {
if (existingVia.spanLayers.includes(segment.layer) && distancePointToSegment(existingVia.center, segment.start, segment.end) < existingVia.diameter / 2 + segment.width / 2 + clearance - 0.000000001) {
addIssue2(issues, "different-net-trace-via-clearance", `Trace ${plan.connectionName} violates a via in routed trace ${traceCopper.trace.pcb_trace_id} on ${segment.layer}`, plan, traceCopper.connectionName);
}
}
}
for (const via of getPlanVias4(plan)) {
for (const existingSegment of traceCopper.segments) {
if (via.spanLayers.includes(existingSegment.layer) && distancePointToSegment(via.center, existingSegment.start, existingSegment.end) < via.diameter / 2 + existingSegment.width / 2 + clearance - 0.000000001) {
addIssue2(issues, "different-net-trace-via-clearance", `Via ${plan.connectionName} violates routed trace ${traceCopper.trace.pcb_trace_id} on ${existingSegment.layer}`, plan, traceCopper.connectionName);
}
}
for (const existingVia of traceCopper.vias) {
if (via.spanLayers.some((layer) => existingVia.spanLayers.includes(layer)) && distance(via.center, existingVia.center) < (via.diameter + existingVia.diameter) / 2 + clearance - 0.000000001) {
addIssue2(issues, "different-net-via-clearance", `Via ${plan.connectionName} violates a via in routed trace ${traceCopper.trace.pcb_trace_id}`, plan, traceCopper.connectionName);
}
}
}
}
}
for (let firstIndex = 0;firstIndex < plans.length; firstIndex++) {
const first = plans[firstIndex];
for (let secondIndex = firstIndex + 1;secondIndex < plans.length; secondIndex++) {
const second = plans[secondIndex];
if (connectionsShareElectricalNet(inputSrj, first.connectionName, second.connectionName)) {
continue;
}
const firstSegments = getPlanSegments2(first);
const secondSegments = getPlanSegments2(second);
const firstVias = getPlanVias4(first);
const secondVias = getPlanVias4(second);
for (const firstSegment of firstSegments) {
for (const secondSegment of secondSegments) {
if (!segmentsAreClear(firstSegment, secondSegment, clearance)) {
addIssue2(issues, "different-net-trace-clearance", `Different-net traces ${first.connectionName} and ${second.connectionName} intersect or violate clearance on ${firstSegment.layer}`, first, second.connectionName);
}
}
for (const secondVia of secondVias) {
if (secondVia.spanLayers.includes(firstSegment.layer) && distancePointToSegment(secondVia.center, firstSegment.start, firstSegment.end) < secondVia.diameter / 2 + firstSegment.width / 2 + clearance - 0.000000001) {
addIssue2(issues, "different-net-trace-via-clearance", `Trace ${first.connectionName} violates via clearance to ${second.connectionName} on ${firstSegment.layer}`, first, second.connectionName);
}
}
}
for (const firstVia of firstVias) {
for (const secondSegment of secondSegments) {
if (firstVia.spanLayers.includes(secondSegment.layer) && distancePointToSegment(firstVia.center, secondSegment.start, secondSegment.end) < firstVia.diameter / 2 + secondSegment.width / 2 + clearance - 0.000000001) {
addIssue2(issues, "different-net-trace-via-clearance", `Via ${first.connectionName} violates trace clearance to ${second.connectionName} on ${secondSegment.layer}`, first, second.connectionName);
}
}
for (const secondVia of secondVias) {
if (firstVia.spanLayers.some((layer) => secondVia.spanLayers.includes(layer)) && distance(firstVia.center, secondVia.center) < (firstVia.diameter + secondVia.diameter) / 2 + clearance - 0.000000001) {
addIssue2(issues, "different-net-via-clearance", `Vias ${first.connectionName} and ${second.connectionName} violate clearance on an overlapping layer span`, first, second.connectionName);
}
}
}
}
}
}
function validateFanoutSolution(params) {
const {
inputSrj,
outputSrj,
plans,
preparedBuses,
sharedBoundary,
clearance,
allowBlindAndBuriedVias = true
} = params;
const issues = [];
const plansByConnection = new Map;
const preparedBusById = new Map(preparedBuses.map((bus) => [bus.busId, bus]));
for (const plan of plans) {
const connectionPlans = plansByConnection.get(plan.connectionName) ?? [];
connectionPlans.push(plan);
plansByConnection.set(plan.connectionName, connectionPlans);
}
for (const bus of preparedBuses) {
if (bus.maxLengthSkew === undefined)
continue;
const busPlans = plans.filter((plan) => plan.busId === bus.busId);
if (busPlans.length < 2)
continue;
const lengths = busPlans.map((plan) => plan.length);
const skew2 = Math.max(...lengths) - Math.min(...lengths);
if (skew2 > bus.maxLengthSkew + 0.000001) {
addIssue2(issues, "bus-length-skew", `Bus ${bus.busId} has ${skew2.toFixed(6)}mm routed-length skew; ${bus.maxLengthSkew.toFixed(6)}mm is allowed`, busPlans[0]);
}
}
for (const connection of inputSrj.connections) {
const connectionPlans = plansByConnection.get(connection.name) ?? [];
if (connectionPlans.length === 0) {
addIssue2(issues, "missing-plan", `Connection ${connection.name} has no fanout plan`);
} else if (connectionPlans.length > 1) {
addIssue2(issues, "duplicate-plan", `Connection ${connection.name} has ${connectionPlans.length} fanout plans`, connectionPlans[0]);
}
}
for (const plan of plans) {
if (!inputSrj.connections.some((connection) => connection.name === plan.connectionName)) {
addIssue2(issues, "unknown-plan", `Plan ${plan.connectionName} is not an input connection`, plan);
continue;
}
validatePlanStructure({
plan,
preparedBus: preparedBusById.get(plan.busId),
inputSrj,
outputSrj,
sharedBoundary,
issues
});
}
const connectedPlans = validateBreakoutConnectivity({
plans,
inputSrj,
sharedBoundary,
issues
});
validateClearances({
plans,
inputSrj,
clearance,
allowBlindAndBuriedVias,
issues
});
return {
valid: issues.length === 0,
checkedConnectionCount: inputSrj.connections.length,
brokenOutConnectionCount: new Set([...connectedPlans].map((plan) => plan.connectionName)).size,
issues
};
}
// node_modules/@tscircuit/fanout-solver/lib/layer-colors.ts
var COPPER_LAYER_COLORS = [
"#ef4444",
"#2563eb",
"#16a34a",
"#9333ea",
"#f59e0b",
"#0891b2",
"#db2777",
"#65a30d",
"#4f46e5",
"#ea580c"
];
function getCopperLayerColor(layerIndex) {
if (!Number.isInteger(layerIndex) || layerIndex < 0) {
throw new Error(`FanoutSolver: copper layer index must be a non-negative integer, received ${layerIndex}`);
}
return COPPER_LAYER_COLORS[layerIndex % COPPER_LAYER_COLORS.length];
}
// node_modules/@tscircuit/fanout-solver/lib/visualize-simple-route-json.ts
var LEGACY_VISUALIZATION_LAYERS = new Set([
"top",
"bottom",
"inner1",
"inner2",
"inner3",
"inner4",
"inner5",
"inner6",
"inner7",
"inner8"
]);
var JUMPER_DIMENSIONS = {
"0603": { padLength: 0.8, padWidth: 0.95 },
"1206": { padLength: 0.6, padWidth: 1.6 },
"1206x4_pair": { padLength: 0.8, padWidth: 0.5 }
};
var getLayerIndex = (layerNames, layerName) => {
const layerIndex = layerNames.indexOf(layerName);
if (layerIndex < 0) {
throw new Error(`FanoutSolver: cannot visualize unknown copper layer "${layerName}"`);
}
return layerIndex;
};
var getGraphicsLayer = (layerNames, copperLayers) => {
const zLayers = [
...new Set(copperLayers.map((layerName) => getLayerIndex(layerNames, layerName)))
].sort((first, second) => first - second);
return `z${zLayers.join(",")}`;
};
var getPointLayers4 = (point) => {
const layers = "layers" in point ? point.layers : undefined;
if (layers && layers.length > 0)
return layers;
return [point.layer];
};
var getObstacleLayerIndexes = (obstacle, layerNames) => {
if (obstacle.__zLayers && obstacle.__zLayers.length > 0) {
return [...new Set(obstacle.__zLayers)].filter((layerIndex) => Number.isInteger(layerIndex) && layerIndex >= 0 && layerIndex < layerNames.length).sort((first, second) => first - second);
}
return [
...new Set(obstacle.layers.map((layerName) => getLayerIndex(layerNames, layerName)))
].sort((first, second) => first - second);
};
var getViaLayerNames = (layerNames, fromLayer, toLayer) => {
const fromIndex = getLayerIndex(layerNames, fromLayer);
const toIndex = getLayerIndex(layerNames, toLayer);
return layerNames.slice(Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex) + 1);
};
var firstFiniteNumber = (...values) => values.find((value) => typeof value === "number" && Number.isFinite(value));
var getViaPadDiameter = (srj) => {
const holeDiameter = firstFiniteNumber(srj.min_via_hole_diameter, srj.minViaHoleDiameter);
const padDiameter = firstFiniteNumber(srj.min_via_pad_diameter, srj.minViaPadDiameter, srj.minViaDiameter);
return Math.max(padDiameter ?? srj.minViaDiameter ?? 0.3, holeDiameter ?? 0);
};
var getColorMap = (connections) => Object.fromEntries(connections.map((connection, index) => [
connection.name,
`hsl(${index * 340 / connections.length}, 100%, 50%)`
]));
var hslToRgb = (hue, saturation, lightness) => {
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
const hueSegment = (hue % 360 + 360) % 360 / 60;
const secondary = chroma * (1 - Math.abs(hueSegment % 2 - 1));
const [red, green, blue] = hueSegment < 1 ? [chroma, secondary, 0] : hueSegment < 2 ? [secondary, chroma, 0] : hueSegment < 3 ? [0, chroma, secondary] : hueSegment < 4 ? [0, secondary, chroma] : hueSegment < 5 ? [secondary, 0, chroma] : [chroma, 0, secondary];
const match = lightness - chroma / 2;
return [red, green, blue].map((channel) => Math.round((channel + match) * 255));
};
var transparentize = (color, amount) => {
const namedColors = {
blue: [0, 0, 255],
orange: [255, 165, 0],
purple: [128, 0, 128],
red: [255, 0, 0]
};
let channels = namedColors[color];
let alpha = 1;
const rgbaMatch = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/.exec(color);
if (rgbaMatch) {
channels = [
Number(rgbaMatch[1]),
Number(rgbaMatch[2]),
Number(rgbaMatch[3])
];
alpha = rgbaMatch[4] === undefined ? 1 : Number(rgbaMatch[4]);
}
const hslMatch = /^hsl\(\s*([\d.-]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*\)$/.exec(color);
if (hslMatch) {
channels = hslToRgb(Number(hslMatch[1]), Number(hslMatch[2]) / 100, Number(hslMatch[3]) / 100);
}
if (!channels)
return color;
const outputAlpha = +Math.max(0, alpha * 100 - amount * 100).toFixed(2) / 100;
if (outputAlpha >= 1) {
const hex = channels.map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("");
return hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5] ? `#${hex[0]}${hex[2]}${hex[4]}` : `#${hex}`;
}
return `rgba(${channels.join(",")},${outputAlpha})`;
};
var getUniqueValues = (values) => {
const seen = new Set;
return values.filter((value) => {
if (seen.has(value))
return false;
seen.add(value);
return true;
});
};
var createObstacleLabelFormatter = (srj) => {
const rootConnectionIndex = new Map;
const addMapping = (identifier, rootName) => {
if (!identifier)
return;
const names = rootConnectionIndex.get(identifier) ?? [];
if (!names.includes(rootName))
names.push(rootName);
rootConnectionIndex.set(identifier, names);
};
for (const connection of srj.connections) {
const rootNames = connection.__rootConnectionNames ?? [connection.name];
for (const rootName of rootNames) {
addMapping(connection.name, rootName);
addMapping(rootName, rootName);
addMapping(connection.__netConnectionName, rootName);
for (const point of connection.pointsToConnect) {
addMapping(point.pointId, rootName);
addMapping(point.pcb_port_id, rootName);
}
}
}
return (obstacle) => {
const rootNames = getUniqueValues([
...obstacle.connectedTo.flatMap((identifier) => rootConnectionIndex.get(identifier) ?? []),
...(obstacle.offBoardConnectsTo ?? []).flatMap((identifier) => rootConnectionIndex.get(identifier) ?? [])
]);
const rootLabel = rootNames.join(", ");
return obstacle.layers.map((layerName) => rootLabel ? `${layerName}
${rootLabel}` : layerName).join(`
`);
};
};
function visualizeSimpleRouteJson(srj) {
const layerNames = getCopperLayerNames(srj.layerCount);
const hasArbitraryCopperLayer = (srj.traces ?? []).some((trace) => trace.route.some((routePoint, routePointIndex) => {
const nextRoutePoint = trace.route[routePointIndex + 1];
return routePoint.route_type === "wire" && nextRoutePoint?.route_type === "wire" && nextRoutePoint.layer === routePoint.layer && !LEGACY_VISUALIZATION_LAYERS.has(routePoint.layer);
}));
const connectionNames = new Set(srj.connections.map(({ name }) => name));
const traceOnlyConnections = hasArbitraryCopperLayer ? [
...new Set((srj.traces ?? []).map(({ connection_name }) => connection_name).filter((connectionName) => connectionName && !connectionNames.has(connectionName)))
].map((name) => ({ name, pointsToConnect: [] })) : [];
const visualizedConnections = [...srj.connections, ...traceOnlyConnections];
const colorMap = getColorMap(visualizedConnections);
const formatObstacleLabel = createObstacleLabelFormatter({
...srj,
connections: visualizedConnections
});
const lines = [];
const circles = [];
const rects = [];
const points = [];
for (const connection of visualizedConnections) {
for (const point of connection.pointsToConnect) {
const pointLayers = getPointLayers4(point);
const rootNames = connection.__rootConnectionNames ?? [connection.name];
points.push({
x: point.x,
y: point.y,
color: colorMap[connection.name],
layer: getGraphicsLayer(layerNames, pointLayers),
label: [
connection.name,
rootNames.join(", "),
pointLayers.join(",")
].join(`
`)
});
}
}
for (const trace of srj.traces ?? []) {
const jumpers = trace.route.filter((routePoint) => routePoint.route_type === "jumper");
const isWireSegmentInsideJumper = (start, end) => jumpers.some((jumper) => {
const tolerance = 0.01;
return Math.abs(start.x - jumper.start.x) < tolerance && Math.abs(start.y - jumper.start.y) < tolerance && Math.abs(end.x - jumper.end.x) < tolerance && Math.abs(end.y - jumper.end.y) < tolerance || Math.abs(start.x - jumper.end.x) < tolerance && Math.abs(start.y - jumper.end.y) < tolerance && Math.abs(end.x - jumper.start.x) < tolerance && Math.abs(end.y - jumper.start.y) < tolerance;
});
for (const routePoint of trace.route) {
if (routePoint.route_type === "via") {
const viaLayers = getViaLayerNames(layerNames, routePoint.from_layer, routePoint.to_layer);
circles.push({
center: { x: routePoint.x, y: routePoint.y },
radius: (routePoint.via_diameter ?? getViaPadDiameter(srj)) / 2,
fill: hasArbitraryCopperLayer ? colorMap[trace.connection_name] : "blue",
stroke: "none",
layer: getGraphicsLayer(layerNames, viaLayers)
});
} else if (routePoint.route_type === "through_obstacle") {
lines.push({
points: [routePoint.start, routePoint.end],
strokeColor: transparentize(colorMap[trace.connection_name] ?? "purple", 0.35),
strokeWidth: routePoint.width,
strokeDash: [0.1, 0.1],
layer: getGraphicsLayer(layerNames, [
routePoint.from_layer,
routePoint.to_layer
]),
label: `${trace.connection_name} through_obstacle`
});
}
}
for (let routePointIndex = 0;routePointIndex < trace.route.length - 1; routePointIndex++) {
const routePoint = trace.route[routePointIndex];
const nextRoutePoint = trace.route[routePointIndex + 1];
if (routePoint.route_type === "jumper") {
const color = colorMap[trace.connection_name] ?? "rgba(255, 165, 0, 0.8)";
const dimensions = JUMPER_DIMENSIONS[routePoint.footprint === "1206x4_pair" ? "1206x4_pair" : "0603"];
const horizontal = Math.abs(routePoint.end.x - routePoint.start.x) > Math.abs(routePoint.end.y - routePoint.start.y);
const padWidth = horizontal ? dimensions.padLength : dimensions.padWidth;
const padHeight = horizontal ? dimensions.padWidth : dimensions.padLength;
const layer = getGraphicsLayer(layerNames, [routePoint.layer]);
for (const center of [routePoint.start, routePoint.end]) {
rects.push({
center,
width: padWidth,
height: padHeight,
fill: transparentize(color, 0.5),
stroke: "rgba(0, 0, 0, 0.5)",
layer
});
}
lines.push({
points: [routePoint.start, routePoint.end],
strokeColor: "rgba(100, 100, 100, 0.8)",
strokeWidth: dimensions.padWidth * 0.3,
layer
});
} else if (routePoint.route_type === "wire" && nextRoutePoint.route_type === "wire" && nextRoutePoint.layer === routePoint.layer && !isWireSegmentInsideJumper(routePoint, nextRoutePoint)) {
const layerIndex = getLayerIndex(layerNames, routePoint.layer);
lines.push({
points: [
{ x: routePoint.x, y: routePoint.y },
{ x: nextRoutePoint.x, y: nextRoutePoint.y }
],
layer: `z${layerIndex}`,
strokeWidth: routePoint.width,
strokeColor: getCopperLayerColor(layerIndex),
...hasArbitraryCopperLayer ? { label: trace.connection_name } : {}
});
}
}
}
for (const obstacle of srj.obstacles) {
if (obstacle.isCopperPour)
continue;
const layerIndexes = getObstacleLayerIndexes(obstacle, layerNames);
if (layerIndexes.length === 0) {
throw new Error(`FanoutSolver: cannot visualize obstacle "${obstacle.obstacleId ?? "unknown"}" without a valid layer`);
}
const onlyLayerName = layerIndexes.length === 1 ? layerNames[layerIndexes[0]] : undefined;
const fill = transparentize(onlyLayerName === "bottom" ? "blue" : "red", 0.5 ** layerIndexes.length);
const shape = obstacle.shape;
const common = {
center: obstacle.center,
fill,
layer: `z${layerIndexes.join(",")}`,
label: formatObstacleLabel(obstacle)
};
if (shape === "circle") {
circles.push({
...common,
radius: Math.min(obstacle.width, obstacle.height) / 2
});
} else {
rects.push({
...common,
width: obstacle.width,
height: obstacle.height,
ccwRotationDegrees: obstacle.ccwRotationDegrees
});
}
}
for (const jumper of srj.jumpers ?? []) {
for (const pad of jumper.pads) {
rects.push({
center: pad.center,
width: pad.width,
height: pad.height,
ccwRotationDegrees: pad.ccwRotationDegrees,
fill: "rgba(255, 165, 0, 0.3)",
stroke: "rgba(255, 165, 0, 0.8)",
layer: getGraphicsLayer(layerNames, pad.layers)
});
}
}
return { rects, circles, lines, points };
}
// node_modules/@tscircuit/fanout-solver/lib/fanout-solver.ts
var process = getRuntimeProcess(globalThis);
class FanoutWorkSolver extends BaseSolver {
solverName;
generator;
getVisualization;
getStats;
getProgress;
output;
hasOutput = false;
nextInput;
constructor(solverName, generator, getVisualization, getStats, getProgress) {
super();
this.solverName = solverName;
this.generator = generator;
this.getVisualization = getVisualization;
this.getStats = getStats;
this.getProgress = getProgress;
this.MAX_ITERATIONS = 1e6;
}
getSolverName() {
return this.solverName;
}
_step() {
if (this.activeSubSolver) {
this.activeSubSolver.step();
if (this.activeSubSolver.failed) {
this.failedSubSolvers = [
...this.failedSubSolvers ?? [],
this.activeSubSolver
];
this.error = this.activeSubSolver.error;
this.failed = true;
this.activeSubSolver = null;
return;
}
if (this.activeSubSolver.solved) {
this.nextInput = this.activeSubSolver.getOutput();
this.activeSubSolver = null;
}
this.stats = this.getStats();
return;
}
const result = this.generator.next(this.nextInput);
this.nextInput = undefined;
this.stats = this.getStats();
if (result.done) {
this.output = result.value;
this.hasOutput = true;
this.solved = true;
return;
}
const yielded = result.value;
if (yielded?.type === "subsolver" && yielded.solver instanceof BaseSolver) {
this.activeSubSolver = yielded.solver;
}
}
computeProgress() {
return this.getProgress();
}
getConstructorParams() {
return [];
}
getOutput() {
if (!this.solved || !this.hasOutput) {
throw new Error(`${this.solverName}: output requested before completion`);
}
return this.output;
}
visualize() {
return this.activeSubSolver?.visualize() ?? this.getVisualization();
}
}
function resolvePositiveNumber(label, value) {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`FanoutSolver: ${label} must be a positive number, received ${value}`);
}
return value;
}
function resolveConfig(srj, options) {
const traceWidth = resolvePositiveNumber("traceWidth", options.traceWidth ?? srj.nominalTraceWidth ?? srj.minTraceWidth);
const viaDiameter = resolvePositiveNumber("viaDiameter", options.viaDiameter ?? srj.minViaPadDiameter ?? srj.min_via_pad_diameter ?? srj.minViaDiameter ?? Math.max(traceWidth * 2, 0.3));
const viaHoleDiameter = resolvePositiveNumber("viaHoleDiameter", options.viaHoleDiameter ?? srj.minViaHoleDiameter ?? srj.min_via_hole_diameter ?? viaDiameter * 0.5);
if (viaHoleDiameter >= viaDiameter) {
throw new Error(`FanoutSolver: viaHoleDiameter ${viaHoleDiameter} must be smaller than viaDiameter ${viaDiameter}`);
}
const clearance = resolvePositiveNumber("clearance", options.clearance ?? srj.minViaEdgeToPadEdgeClearance ?? srj.minTraceToPadEdgeClearance ?? srj.defaultObstacleMargin ?? srj.minTraceWidth);
const layerNames = getCopperLayerNames(srj.layerCount);
const escapeLayers = options.escapeLayers ?? layerNames;
for (const layer of escapeLayers) {
if (!layerNames.includes(layer)) {
throw new Error(`FanoutSolver: escape layer "${layer}" is not available in a ${srj.layerCount}-layer SimpleRouteJson`);
}
}
if (new Set(escapeLayers).size !== escapeLayers.length) {
throw new Error("FanoutSolver: escapeLayers contains duplicates");
}
const borderDistribution = options.borderDistribution ?? "preserve";
if (borderDistribution !== "preserve" && borderDistribution !== "even") {
throw new Error(`FanoutSolver: borderDistribution must be "preserve" or "even", received "${borderDistribution}"`);
}
return {
traceWidth,
viaDiameter,
viaHoleDiameter,
clearance,
compactBusTracks: options.compactBusTracks ?? false,
allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
allowSameNetMerges: options.allowSameNetMerges ?? false,
densePlaneReservationBusIds: options.densePlaneReservationBusIds ?? [],
denseUnrestrictedPlaneRoutingBusIds: options.denseUnrestrictedPlaneRoutingBusIds ?? [],
singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
borderDistribution,
layerNames,
escapeLayers,
maxLayerCombinations: options.maxLayerCombinations === undefined ? 256 : resolvePositiveNumber("maxLayerCombinations", options.maxLayerCombinations),
balanceLayerLoadByConnectionCount: options.balanceLayerLoadByConnectionCount ?? false
};
}
function validateCornerBandCapacities(buses, config) {
const checkedBands = new Set;
const exitPitch = Math.max(config.traceWidth + config.clearance, config.viaDiameter + config.clearance);
const endInset = Math.max(config.viaDiameter / 2 + config.clearance, exitPitch);
for (const bus of buses) {
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit);
if (!bus.exitEdge || !side)
continue;
const bandKey = `${bus.exitEdge}:${side}`;
if (checkedBands.has(bandKey))
continue;
checkedBands.add(bandKey);
const edgeLength = bus.exitEdge === "left" || bus.exitEdge === "right" ? bus.sharedBoundary.maxY - bus.sharedBoundary.minY : bus.sharedBoundary.maxX - bus.sharedBoundary.minX;
const connectionCount = bus.cornerBandConnectionCount ?? bus.connections.length;
const halfTrackSpan = (connectionCount - 1) * exitPitch / 2;
const availableHalfTrackSpan = edgeLength / 4 - endInset;
if (halfTrackSpan > availableHalfTrackSpan + 0.000001) {
throw new Error(`FanoutSolver: ${side} band on the ${bus.exitEdge} edge cannot fit ${connectionCount} via-safe exits`);
}
}
}
function assignmentLoadPenalty(assignment, buses, weightByConnectionCount) {
const connectionCountByBusId = new Map(buses.map((bus) => [bus.busId, bus.connections.length]));
const loadByLayer = new Map;
for (const [busId, layer] of Object.entries(assignment)) {
loadByLayer.set(layer, (loadByLayer.get(layer) ?? 0) + (weightByConnectionCount ? connectionCountByBusId.get(busId) ?? 1 : 1));
}
return [...loadByLayer.values()].reduce((penalty, load) => penalty + load * load, 0);
}
function getLayerLoadPenaltyWeight(config) {
return config.balanceLayerLoadByConnectionCount ? 0.25 : 0.01;
}
function comparePlaneRoutingPriority(first, second, allowBlindAndBuriedVias) {
const planeDifference = Number(first.termination.type === "plane") - Number(second.termination.type === "plane");
return allowBlindAndBuriedVias ? -planeDifference : planeDifference;
}
function getPlanViaCount(plans) {
return plans.reduce((count, plan) => count + Number(Boolean(plan.via)) + (plan.additionalVias?.length ?? 0) + Number(Boolean(plan.planeEndpointVia)), 0);
}
function getBusDistanceToBoundary(bus) {
const averageSource = bus.connections.reduce((sum, connection) => {
const sourceAxis = bus.direction === "left" || bus.direction === "right" ? connection.sourcePoint.x : connection.sourcePoint.y;
return sum + sourceAxis;
}, 0) / bus.connections.length;
switch (bus.direction) {
case "right":
return bus.sharedBoundary.maxX - averageSource;
case "left":
return averageSource - bus.sharedBoundary.minX;
case "up":
return bus.sharedBoundary.maxY - averageSource;
case "down":
return averageSource - bus.sharedBoundary.minY;
}
}
function busUsesDestinationGuidedTracks(bus) {
const isHorizontal3 = bus.direction === "left" || bus.direction === "right";
return bus.connections.some((connection) => {
const exitTargetPoint = connection.exitTargetPoint ?? connection.targetPoint;
const sourceTrack = isHorizontal3 ? connection.sourcePoint.y : connection.sourcePoint.x;
const targetTrack = isHorizontal3 ? exitTargetPoint.y : exitTargetPoint.x;
return Math.abs(sourceTrack - targetTrack) > 0.000001;
});
}
function getCommonExplicitExitTargetLayer(bus) {
if (bus.connections.length === 0 || bus.connections.some((connection) => !connection.hasExplicitLayeredExitTarget || !connection.exitTargetPoint?.layer)) {
return;
}
const targetLayers = new Set(bus.connections.map((connection) => connection.exitTargetPoint.layer));
if (targetLayers.size !== 1)
return;
const [targetLayer] = targetLayers;
return targetLayer;
}
function busIsOnOutwardComponentEdge2(bus) {
const isHorizontal3 = bus.direction === "left" || bus.direction === "right";
const directionalCoordinates = isHorizontal3 ? bus.xCoordinates : bus.yCoordinates;
const averageSource = bus.connections.reduce((sum, connection) => sum + (isHorizontal3 ? connection.sourcePoint.x : connection.sourcePoint.y), 0) / bus.connections.length;
const outwardCoordinate = bus.direction === "right" || bus.direction === "up" ? Math.max(...directionalCoordinates) : Math.min(...directionalCoordinates);
return Math.abs(averageSource - outwardCoordinate) < 0.000001;
}
function getBusDepthInRows(bus) {
const isHorizontal3 = bus.direction === "left" || bus.direction === "right";
const directionalCoordinates = isHorizontal3 ? bus.xCoordinates : bus.yCoordinates;
const averageSource = bus.connections.reduce((sum, connection) => sum + (isHorizontal3 ? connection.sourcePoint.x : connection.sourcePoint.y), 0) / bus.connections.length;
const outwardCoordinate = bus.direction === "right" || bus.direction === "up" ? Math.max(...directionalCoordinates) : Math.min(...directionalCoordinates);
const directionalPitch = isHorizontal3 ? bus.pitchX : bus.pitchY;
return Math.round(Math.abs(averageSource - outwardCoordinate) / directionalPitch);
}
function createInitialLayerAssignment(params) {
const {
buses,
escapeLayers,
escapeLayersByBusId,
preferOrderedCoordinatedWindingLayers
} = params;
const assignment = {};
const directionsByComponent = new Map;
let nextViaLayerIndex = 0;
for (const bus of buses) {
const directions = directionsByComponent.get(bus.componentId) ?? new Set;
directions.add(bus.direction);
directionsByComponent.set(bus.componentId, directions);
}
for (const bus of buses) {
const sourceLayer = bus.connections[0]?.sourceLayer;
if (!sourceLayer) {
throw new Error(`FanoutSolver: bus "${bus.busId}" has no connections`);
}
if (bus.termination.type === "plane") {
assignment[bus.busId] = bus.termination.layer;
continue;
}
const routableEscapeLayers = escapeLayersByBusId[bus.busId] ?? escapeLayers;
const viaLayers = routableEscapeLayers.filter((layer) => layer !== sourceLayer);
const commonExitTargetLayer = getCommonExplicitExitTargetLayer(bus);
if (commonExitTargetLayer && routableEscapeLayers.includes(commonExitTargetLayer)) {
assignment[bus.busId] = commonExitTargetLayer;
} else if (!busUsesCoordinatedWinding(bus) && routableEscapeLayers.includes(sourceLayer) && (busUsesDestinationGuidedTracks(bus) || busIsOnOutwardComponentEdge2(bus))) {
assignment[bus.busId] = sourceLayer;
} else if (viaLayers.length > 0) {
if (preferOrderedCoordinatedWindingLayers && busUsesCoordinatedWinding(bus)) {
assignment[bus.busId] = viaLayers[0];
continue;
}
const componentDirections = directionsByComponent.get(bus.componentId);
const hasOpposingDirection = componentDirections.has("left") && componentDirections.has("right") || componentDirections.has("up") && componentDirections.has("down");
if (hasOpposingDirection) {
const depthInRows = getBusDepthInRows(bus);
assignment[bus.busId] = viaLayers[Math.max(depthInRows - 1, 0) % viaLayers.length];
} else {
assignment[bus.busId] = viaLayers[nextViaLayerIndex % viaLayers.length];
nextViaLayerIndex++;
}
} else {
assignment[bus.busId] = sourceLayer;
}
}
return assignment;
}
function prioritizeLayerAssignment(params) {
const { initialAssignment, generatedAssignments, maxAssignments } = params;
const initialKey = JSON.stringify(initialAssignment);
return [
initialAssignment,
...generatedAssignments.filter((assignment) => JSON.stringify(assignment) !== initialKey)
].slice(0, maxAssignments);
}
function busUsesCoordinatedWinding(bus) {
return Boolean(bus.exitEdge && bus.termination.type === "boundary" && bus.connections.length > 0 && bus.connections.every((connection) => connection.hasExplicitLayeredExitTarget === true));
}
function shouldUseJointBoundaryViaReservation(boundaryBusConnectionCounts) {
return boundaryBusConnectionCounts.length === 5 || boundaryBusConnectionCounts.length === 6 || boundaryBusConnectionCounts.length === 7 || boundaryBusConnectionCounts.length === 8 || boundaryBusConnectionCounts.length === 9 || boundaryBusConnectionCounts.length === 4 && new Set(boundaryBusConnectionCounts).size > 1;
}
function shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts) {
const boundaryBusCount = boundaryBusConnectionCounts.length;
const singletonBusCount = boundaryBusConnectionCounts.filter((count) => count === 1).length;
return (boundaryBusCount === 5 || boundaryBusCount === 6 || boundaryBusCount === 7) && singletonBusCount === 1 || boundaryBusCount === 8 && (singletonBusCount === 1 || singletonBusCount === 2) || boundaryBusCount === 9 && singletonBusCount >= 1 && singletonBusCount <= 3;
}
function getDenseSingletonDeferralCandidateCount(boundaryBusConnectionCounts) {
if (!shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)) {
return 0;
}
const singletonBusCount = boundaryBusConnectionCounts.filter((count) => count === 1).length;
return Math.max(1, singletonBusCount - 1);
}
function isDenseSingletonEmbeddedInMultiLayerWideBus(params) {
const sourcePoint = params.singletonBus.connections[0]?.sourcePoint;
if (params.singletonBus.connections.length !== 1 || !sourcePoint)
return false;
return params.wideBuses.some((wideBus) => {
const wideLayers = wideBus.routableEscapeLayers ?? wideBus.allowedLayers ?? [];
if (wideBus.connections.length < 8 || wideBus.componentId !== params.singletonBus.componentId || wideBus.exitEdge !== params.singletonBus.exitEdge || wideLayers.length < 2 || !wideLayers.includes(params.singletonTargetLayer)) {
return false;
}
const sourceXs = wideBus.connections.map((connection) => connection.sourcePoint.x);
const sourceYs = wideBus.connections.map((connection) => connection.sourcePoint.y);
return sourcePoint.x >= Math.min(...sourceXs) - 0.000000001 && sourcePoint.x <= Math.max(...sourceXs) + 0.000000001 && sourcePoint.y >= Math.min(...sourceYs) - 0.000000001 && sourcePoint.y <= Math.max(...sourceYs) + 0.000000001;
});
}
function getDenseLeadingCornerBandTargetTrackOffset(params) {
const cornerPitch = Math.max(params.traceWidth + params.clearance, params.viaDiameter + params.clearance);
return -params.leadingLaneCount * cornerPitch / 2;
}
function shouldSearchAdditionalBoundaryRouteTopologies(params) {
if (params.boundaryBusCount === 5 || params.boundaryBusCount > 9) {
return false;
}
if (params.boundaryBusCount === 6 || params.boundaryBusCount === 7 || params.boundaryBusCount === 8 || params.boundaryBusCount === 9) {
return params.connectionCount > 2 && params.rawSkew - params.maximumSkew > Math.max(1, params.maximumSkew * 0.5);
}
return params.rawSkew - params.maximumSkew > Math.max(1, params.maximumSkew * 0.25);
}
function getDenseSingletonBoundaryGeometry(bus) {
const connection = bus.connections[0];
const target = connection?.exitTargetPoint;
let targetProjection = 0;
if (connection && target) {
const deltaX = target.x - connection.sourcePoint.x;
const deltaY = target.y - connection.sourcePoint.y;
targetProjection = bus.direction === "right" ? deltaX : bus.direction === "left" ? -deltaX : bus.direction === "up" ? deltaY : -deltaY;
}
return {
isCorner: Boolean(getCornerBandSide(bus.exitEdge, bus.preferredExit)),
targetProjection
};
}
function compareDenseSingletonBoundaryDeferralPriority(first, second) {
const firstGeometry = getDenseSingletonBoundaryGeometry(first);
const secondGeometry = getDenseSingletonBoundaryGeometry(second);
return Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) || (firstGeometry.isCorner && secondGeometry.isCorner ? firstGeometry.targetProjection - secondGeometry.targetProjection : 0) || first.busId.localeCompare(second.busId);
}
function compareReleasedDenseSingletonBoundaryDeferralPriority(first, second) {
const firstGeometry = getDenseSingletonBoundaryGeometry(first);
const secondGeometry = getDenseSingletonBoundaryGeometry(second);
return Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) || (firstGeometry.isCorner && secondGeometry.isCorner ? Number(firstGeometry.targetProjection > 0) - Number(secondGeometry.targetProjection > 0) : 0) || first.busId.localeCompare(second.busId);
}
function getDenseBoundaryPairRoutingPriorityKeys(params) {
const { pairBuses } = params;
const firstBus = pairBuses[0];
const firstSourceLayer = firstBus?.connections[0]?.sourceLayer;
if (params.boundaryBusCount !== 7 && params.boundaryBusCount !== 8 && params.boundaryBusCount !== 9 || pairBuses.length !== 3 || firstBus === undefined || firstBus.exitEdge === undefined || firstBus.assignedLayer === undefined || firstSourceLayer === undefined || pairBuses.some((bus) => bus.connections.length !== 2 || bus.componentId !== firstBus.componentId || bus.exitEdge !== firstBus.exitEdge || bus.assignedLayer !== firstBus.assignedLayer || bus.connections.some((connection) => {
const exitTarget = connection.exitTargetPoint;
return connection.sourceLayer !== firstSourceLayer || exitTarget?.layer !== firstBus.assignedLayer || !Number.isFinite(connection.sourcePoint.x) || !Number.isFinite(connection.sourcePoint.y) || !Number.isFinite(exitTarget?.x) || !Number.isFinite(exitTarget?.y);
}))) {
return null;
}
const getMaximumSourceToExitTargetDistance = (bus) => Math.max(...bus.connections.map((connection) => {
const exitTarget = connection.exitTargetPoint;
return Math.hypot(exitTarget.x - connection.sourcePoint.x, exitTarget.y - connection.sourcePoint.y);
}));
return pairBuses.map((bus) => Number(getMaximumSourceToExitTargetDistance(bus).toFixed(9)));
}
function getCandidateEscapeLayersForBus(params) {
const { bus, srj, config, staticClearanceCache } = params;
const busAllowedLayers = bus.allowedLayers;
const allowedEscapeLayers = busAllowedLayers === undefined ? config.escapeLayers : config.escapeLayers.filter((layer) => busAllowedLayers.includes(layer));
if (busUsesCoordinatedWinding(bus))
return allowedEscapeLayers;
const individuallyRoutableLayers = allowedEscapeLayers.filter((targetLayer) => routeBus({
srj,
bus,
targetLayer,
acceptedPlans: [],
layerNames: config.layerNames,
traceWidth: config.traceWidth,
viaDiameter: config.viaDiameter,
viaHoleDiameter: config.viaHoleDiameter,
clearance: config.clearance,
compactBusTracks: config.compactBusTracks,
allowBlindAndBuriedVias: config.allowBlindAndBuriedVias,
allowSameNetMerges: config.allowSameNetMerges,
staticClearanceCache
}) !== null);
const candidateLayers = individuallyRoutableLayers.length > 0 ? individuallyRoutableLayers : allowedEscapeLayers;
return candidateLayers;
}
class FanoutSolver extends BaseSolver {
inputSrj;
options;
preparedBuses;
attempts = [];
layerAssignments = [];
config;
routingSrj;
escapeLayersByBusId = {};
boundaryBuses;
fixedPlaneAssignments;
evaluatedAssignmentKeys = new Set;
queuedAssignmentKeys = new Set;
assignmentRepairDepthByKey = new Map;
pendingRepairAssignments = [];
routeStaticClearanceCache = new Map;
routingPrefixCache = new Map;
groupedBeamEvaluated = false;
routingInitialized = false;
nextCandidateLayerBusIndex = 0;
nextAssignmentIndex = 0;
nextGeneratedAssignmentIndex = 0;
activeOperation = null;
inProgressPlans = [];
activeRoutingVisualization = null;
activeAdaptiveVisualization = null;
bestAttempt = null;
lengthMatchingFailure = null;
endpointCompletion = null;
constructor(inputSrj, options = {}) {
super();
this.inputSrj = inputSrj;
this.options = options;
this.routingSrj = {
...inputSrj,
obstacles: [...inputSrj.obstacles]
};
this.config = resolveConfig(inputSrj, options);
this.preparedBuses = prepareFanoutBuses(this.routingSrj, options);
validateCornerBandCapacities(this.preparedBuses, this.config);
for (const bus of this.preparedBuses) {
for (const connection of bus.connections) {
if (!connection.hasExplicitLayeredExitTarget)
continue;
const targetLayer = connection.exitTargetPoint?.layer;
if (typeof targetLayer !== "string" || targetLayer.length === 0 || !this.config.layerNames.includes(targetLayer)) {
throw new Error(`FanoutSolver: connection exit target for "${connection.connection.name}" uses unavailable layer "${String(targetLayer)}"`);
}
}
for (const allowedLayer of bus.allowedLayers ?? []) {
if (!this.config.layerNames.includes(allowedLayer)) {
throw new Error(`FanoutSolver: bus "${bus.busId}" allows unavailable layer "${allowedLayer}"`);
}
}
if (bus.termination.type === "boundary" && bus.allowedLayers !== undefined && !bus.allowedLayers.some((layer) => this.config.escapeLayers.includes(layer))) {
throw new Error(`FanoutSolver: bus "${bus.busId}" has no allowed layer in escapeLayers`);
}
bus.routableEscapeLayers = this.config.escapeLayers.filter((layer) => bus.allowedLayers?.includes(layer) ?? true);
if (bus.termination.type !== "plane")
continue;
const planeLayer = bus.termination.layer;
if (!this.config.layerNames.includes(planeLayer)) {
throw new Error(`FanoutSolver: plane-terminated bus "${bus.busId}" targets unavailable layer "${planeLayer}"`);
}
if (bus.allowedLayers !== undefined && !bus.allowedLayers.includes(planeLayer)) {
throw new Error(`FanoutSolver: plane-terminated bus "${bus.busId}" targets disallowed layer "${planeLayer}"`);
}
if (bus.connections.some((connection) => connection.sourceLayer === planeLayer)) {
throw new Error(`FanoutSolver: plane-terminated bus "${bus.busId}" must target a layer below its source pad`);
}
}
this.boundaryBuses = this.preparedBuses.filter((bus) => bus.termination.type === "boundary");
this.fixedPlaneAssignments = Object.fromEntries(this.preparedBuses.flatMap((bus) => bus.termination.type === "plane" ? [[bus.busId, bus.termination.layer]] : []));
const workUnitsPerAssignment = this.preparedBuses.length * 3 + 8;
const estimatedWorkUnitCount = this.boundaryBuses.length + 1 + this.config.maxLayerCombinations * workUnitsPerAssignment + this.preparedBuses.length * 2 + 20;
this.MAX_ITERATIONS = Math.max(1e4, estimatedWorkUnitCount);
}
getSolverName() {
return "FanoutSolver";
}
stepRoutingInitialization() {
const bus = this.boundaryBuses[this.nextCandidateLayerBusIndex];
if (bus) {
this.escapeLayersByBusId[bus.busId] = getCandidateEscapeLayersForBus({
bus,
srj: this.routingSrj,
config: this.config,
staticClearanceCache: this.routeStaticClearanceCache
});
this.nextCandidateLayerBusIndex++;
this.stats = {
phase: "discover-candidate-layers",
bus: bus.busId,
busIndex: this.nextCandidateLayerBusIndex,
busCount: this.boundaryBuses.length
};
return;
}
const generatedAssignments = generateLayerAssignments({
busIds: this.boundaryBuses.map((candidate) => candidate.busId),
layers: this.config.escapeLayers,
layersByBusId: this.escapeLayersByBusId,
maxAssignments: this.config.maxLayerCombinations
}).map((assignment) => ({
...assignment,
...this.fixedPlaneAssignments
}));
this.layerAssignments.push(...prioritizeLayerAssignment({
initialAssignment: createInitialLayerAssignment({
buses: this.preparedBuses,
escapeLayers: this.config.escapeLayers,
escapeLayersByBusId: this.escapeLayersByBusId,
preferOrderedCoordinatedWindingLayers: this.config.densePlaneReservationBusIds.length > 0 || this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 || shouldUseAdaptiveDensePlaneRouting(this.preparedBuses, this.config.allowBlindAndBuriedVias)
}),
generatedAssignments,
maxAssignments: this.config.maxLayerCombinations
}));
this.routingInitialized = true;
this.stats = {
phase: "prepare-layer-assignments",
assignmentCount: this.layerAssignments.length
};
}
*initializeRoutingSteps() {
while (!this.routingInitialized) {
this.stepRoutingInitialization();
if (!this.routingInitialized)
yield;
}
}
setInProgressPlans(params) {
this.inProgressPlans = [...params.plans];
this.stats = {
...this.stats,
phase: params.phase,
...params.strategy ? { routingStrategy: params.strategy } : {},
...params.unitIndex !== undefined ? { workUnit: params.unitIndex } : {},
...params.unitCount !== undefined ? { workUnitCount: params.unitCount } : {},
...params.busId ? { bus: params.busId } : {},
routedConnections: `${params.plans.length}/${this.inputSrj.connections.length}`
};
}
visualizeCurrentState() {
const visualizedSrj = this.endpointCompletion?.simpleRouteJson ?? (!this.solved && !this.failed && this.inProgressPlans.length > 0 ? buildOutputSimpleRouteJson({
inputSrj: this.inputSrj,
plans: this.inProgressPlans,
layerNames: this.config.layerNames
}) : undefined) ?? this.bestAttempt?.outputSrj ?? this.inputSrj;
return visualizeSimpleRouteJson(visualizedSrj);
}
visualizeWorkState(solverName) {
const base = this.visualizeCurrentState();
const boundary = this.preparedBuses[0]?.sharedBoundary ?? this.inputSrj.bounds;
const activeBusId = typeof this.stats.bus === "string" ? this.stats.bus : undefined;
const activeBus = activeBusId ? this.preparedBuses.find((bus) => bus.busId === activeBusId) : undefined;
const width = boundary.maxX - boundary.minX;
const height = boundary.maxY - boundary.minY;
const annotationSize = Math.max(Math.min(width, height) * 0.025, 0.25);
const phase = typeof this.stats.phase === "string" ? this.stats.phase : "starting";
const detail = [
typeof this.stats.routeConnection === "string" ? `connection ${this.stats.routeConnection}` : undefined,
typeof this.stats.searchBatch === "number" ? `batch ${this.stats.searchBatch}` : undefined,
typeof this.stats.expandedStates === "number" ? `${this.stats.expandedStates.toLocaleString()} states` : undefined
].filter(Boolean).join(" · ");
const title = `${solverName}: ${phase}`;
return {
...mergeGraphics(base, {
rects: [
{
center: {
x: (boundary.minX + boundary.maxX) / 2,
y: (boundary.minY + boundary.maxY) / 2
},
width,
height,
fill: "rgba(0, 0, 0, 0)",
stroke: "rgba(14, 165, 233, 0.8)",
label: `${solverName} working boundary`
}
],
circles: (activeBus?.connections ?? []).map((connection) => ({
center: connection.sourcePoint,
radius: Math.max(annotationSize, Math.min(connection.sourceObstacle.width, connection.sourceObstacle.height) * 0.6),
fill: "rgba(250, 204, 21, 0.25)",
stroke: "#f59e0b",
label: `active bus ${activeBusId}: ${connection.connection.name}`
})),
texts: [
{
x: boundary.minX,
y: boundary.maxY + annotationSize * 2,
text: `${solverName} · ${phase}${detail ? ` · ${detail}` : ""}`,
color: "#0f172a",
fontSize: annotationSize * 1.5,
anchorSide: "bottom_left"
}
]
}),
title
};
}
visualizeBoundaryRoutingState() {
if (!this.activeRoutingVisualization) {
return this.visualizeWorkState("BoundaryBusRoutingSolver");
}
return {
...mergeGraphics(this.visualizeCurrentState(), this.activeRoutingVisualization),
title: this.activeRoutingVisualization.title
};
}
visualizeAdaptiveRoutingState() {
if (this.activeAdaptiveVisualization) {
return this.activeAdaptiveVisualization;
}
const boundary = this.preparedBuses[0]?.sharedBoundary ?? this.inputSrj.bounds;
const width = boundary.maxX - boundary.minX;
const height = boundary.maxY - boundary.minY;
const annotationSize = Math.max(Math.min(width, height) * 0.02, 0.2);
return {
title: "SingleLayerAdaptiveExitSolver: preparing flow grid",
rects: [
{
center: {
x: (boundary.minX + boundary.maxX) / 2,
y: (boundary.minY + boundary.maxY) / 2
},
width,
height,
fill: "rgba(0, 0, 0, 0)",
stroke: "rgba(14, 165, 233, 0.9)",
label: "adaptive flow grid boundary"
}
],
points: this.preparedBuses.flatMap((bus) => bus.connections.map((connection) => ({
...connection.sourcePoint,
color: "#f97316",
label: "adaptive route source"
}))),
texts: [
{
x: boundary.minX,
y: boundary.maxY + annotationSize * 2,
text: "preparing adaptive flow grid",
color: "#0f172a",
fontSize: annotationSize * 1.5,
anchorSide: "bottom_left"
}
]
};
}
startOperation(params) {
const solver = this.createWorkSolver(params.name, params.generator, params.getProgress);
this.activeOperation = {
solver,
onSolved: params.onSolved
};
this.activeSubSolver = solver;
}
createWorkSolver(name, generator, getProgress, getVisualization) {
return new FanoutWorkSolver(name, generator, getVisualization ?? (() => this.visualizeWorkState(name)), () => ({ ...this.stats }), getProgress ?? (() => 0));
}
*routeBusAlternativesWorkSteps(params, maximumAlternatives) {
const steps = routeBusAlternativesSteps(params, maximumAlternatives, true);
let result = steps.next();
while (!result.done) {
const { winding } = result.value;
if (winding.visualization) {
this.activeRoutingVisualization = winding.visualization;
}
this.stats = {
...this.stats,
phase: "route-boundary-bus-connection",
bus: result.value.busId,
targetLayer: result.value.targetLayer,
routeOrderAttempt: winding.routeOrderAttempt,
routeConnection: `${winding.connectionIndex + 1}/${winding.connectionCount}`,
connection: winding.connectionName,
searchBatch: winding.searchBatch,
expandedStates: winding.expandedStateCount,
connectionComplete: winding.connectionComplete
};
yield;
result = steps.next();
}
return result.value;
}
stepActiveOperation() {
const operation = this.activeOperation;
if (!operation)
return;
operation.solver.step();
if (operation.solver.failed) {
this.failedSubSolvers = [
...this.failedSubSolvers ?? [],
operation.solver
];
this.error = operation.solver.error;
this.failed = true;
this.activeOperation = null;
this.activeSubSolver = null;
return;
}
if (!operation.solver.solved)
return;
const output = operation.solver.getOutput();
this.activeOperation = null;
this.activeSubSolver = null;
operation.onSolved(output);
}
completeBestAttemptEndpoints() {
if (!this.options.completeOriginalEndpoints || this.endpointCompletion || !this.bestAttempt) {
return;
}
this.endpointCompletion = completeOriginalEndpoints({
inputSrj: this.routingSrj,
fanoutSrj: this.bestAttempt.outputSrj,
plans: this.bestAttempt.plans,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
effort: this.options.endpointCompletionEffort,
routeDownstreamConnections: this.options.routeDownstreamConnections
});
}
getValidationBoundary() {
if (this.options.sharedBoundary)
return this.options.sharedBoundary;
const firstBoundary = this.preparedBuses[0]?.sharedBoundary;
if (!firstBoundary)
return this.inputSrj.bounds;
return this.preparedBuses.slice(1).reduce((boundary, bus) => ({
minX: Math.min(boundary.minX, bus.sharedBoundary.minX),
maxX: Math.max(boundary.maxX, bus.sharedBoundary.maxX),
minY: Math.min(boundary.minY, bus.sharedBoundary.minY),
maxY: Math.max(boundary.maxY, bus.sharedBoundary.maxY)
}), { ...firstBoundary });
}
validateCompletePlans(plans, outputSrj) {
return validateFanoutSolution({
inputSrj: this.inputSrj,
outputSrj,
plans,
preparedBuses: this.preparedBuses,
sharedBoundary: this.getValidationBoundary(),
clearance: this.config.clearance,
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias
});
}
matchCompletePlanLengths(plans) {
return matchBusPlanLengths({
plans,
preparedBuses: this.preparedBuses,
inputSrj: this.inputSrj,
sharedBoundary: this.getValidationBoundary(),
clearance: this.config.clearance,
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
allowSameNetMerges: this.config.allowSameNetMerges
});
}
*routeDenseThroughAllMixedTerminationSteps(params) {
if (this.config.allowBlindAndBuriedVias)
return null;
const usePadAlignedDenseRouting = params.denseRoutingStrategy !== "boundary-aligned";
const debugDense = (...values) => {
if (process.env.FANOUT_DEBUG_DENSE === "1") {
if (process.env.FANOUT_DEBUG_DENSE_SUMMARY === "1" && ![
"start",
"plane-match:preflight-failed",
"plane-match:incremental-complete",
"plane-match:incremental-failed",
"plane-route:alternate-candidate-counts",
"plane-route:alternate-search",
"plane-route:csp",
"plane-reservation-core",
"plane-route:promote-failed",
"plane-route:alternate-choice",
"plane-route:promote-zero-candidates",
"length-match:complete",
"length-match:start",
"plane-route:failed",
"dense-validation"
].includes(String(values[0]))) {
return;
}
console.error("dense:", ...values);
}
};
const unsortedBoundaryBuses = params.busesInRoutingOrder.filter((bus) => bus.termination.type === "boundary");
const configuredDensePlaneRouting = this.config.densePlaneReservationBusIds.length > 0 || this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0;
const useAdaptiveDensePlaneRouting = !configuredDensePlaneRouting && shouldUseAdaptiveDensePlaneRouting(this.preparedBuses, this.config.allowBlindAndBuriedVias);
const useConfiguredDensePlaneRouting = configuredDensePlaneRouting || useAdaptiveDensePlaneRouting;
const useAdaptiveJointPlaneSelection = useAdaptiveDensePlaneRouting && (params.planeReservationRetryCount ?? 0) === 0;
const matchLengthsAfterPlanes = useConfiguredDensePlaneRouting && params.lengthMatchingStage !== "before-planes";
const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(unsortedBoundaryBuses.map((bus) => bus.connections.length));
const twoConnectionBoundaryBuses = unsortedBoundaryBuses.filter((bus) => bus.connections.length === 2);
const pairRoutingPriorityKeys = getDenseBoundaryPairRoutingPriorityKeys({
boundaryBusCount: unsortedBoundaryBuses.length,
pairBuses: twoConnectionBoundaryBuses.map((bus) => ({
...bus,
assignedLayer: params.busLayerAssignments[bus.busId]
}))
});
const pairRoutingPriorityKeyByBusId = pairRoutingPriorityKeys ? new Map(twoConnectionBoundaryBuses.map((bus, index) => [
bus.busId,
pairRoutingPriorityKeys[index]
])) : null;
const wideBoundaryBuses = unsortedBoundaryBuses.filter((bus) => bus.connections.length >= 8);
const hasThreeWideBoundaryBuses = useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3;
const getBoundaryTargetSpan = (bus) => {
const coordinates = bus.connections.map((connection) => {
const target = connection.exitTargetPoint ?? connection.targetPoint;
return bus.exitEdge === "top" || bus.exitEdge === "bottom" ? target.x : target.y;
});
return {
minimum: Math.min(...coordinates),
maximum: Math.max(...coordinates)
};
};
const narrowBusOverlapsWideTargetSpan = (bus) => {
if (bus.connections.length >= 8)
return false;
const span = getBoundaryTargetSpan(bus);
return wideBoundaryBuses.some((wideBus) => {
if (wideBus.exitEdge !== bus.exitEdge)
return false;
const wideSpan = getBoundaryTargetSpan(wideBus);
return span.maximum >= wideSpan.minimum - 0.000000001 && span.minimum <= wideSpan.maximum + 0.000000001;
});
};
const getContainingWideSourceField = (bus) => {
if (bus.connections.length >= 8)
return;
return wideBoundaryBuses.find((wideBus) => {
const wideXCoordinates = wideBus.connections.map((connection) => connection.sourcePoint.x);
const wideYCoordinates = wideBus.connections.map((connection) => connection.sourcePoint.y);
const minimumX = Math.min(...wideXCoordinates);
const maximumX = Math.max(...wideXCoordinates);
const minimumY = Math.min(...wideYCoordinates);
const maximumY = Math.max(...wideYCoordinates);
return bus.connections.every((connection) => connection.sourcePoint.x >= minimumX - 0.000000001 && connection.sourcePoint.x <= maximumX + 0.000000001 && connection.sourcePoint.y >= minimumY - 0.000000001 && connection.sourcePoint.y <= maximumY + 0.000000001);
});
};
const narrowBusIsEmbeddedInWideSourceField = (bus) => Boolean(getContainingWideSourceField(bus));
const getThreeWideRoutingPriority = (bus) => narrowBusOverlapsWideTargetSpan(bus) && !narrowBusIsEmbeddedInWideSourceField(bus) ? 0 : bus.connections.length >= 8 ? 1 : bus.connections.length > 1 ? 2 : 3;
const initiallySortedBoundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
if (useJointBoundaryViaReservation || wideBoundaryBuses.length > 0) {
if (hasThreeWideBoundaryBuses) {
const threeWidePriorityDifference = getThreeWideRoutingPriority(first) - getThreeWideRoutingPriority(second);
if (threeWidePriorityDifference !== 0) {
return threeWidePriorityDifference;
}
}
const connectionCountDifference = unsortedBoundaryBuses.length === 2 && Math.max(...unsortedBoundaryBuses.map((bus) => bus.connections.length)) >= 8 ? first.connections.length - second.connections.length : second.connections.length - first.connections.length;
if (connectionCountDifference !== 0)
return connectionCountDifference;
}
const firstLayer = params.busLayerAssignments[first.busId];
const secondLayer = params.busLayerAssignments[second.busId];
const firstPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(first.busId);
const secondPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(second.busId);
if (firstPairRoutingPriority !== undefined && secondPairRoutingPriority !== undefined && firstPairRoutingPriority !== secondPairRoutingPriority) {
return firstPairRoutingPriority - secondPairRoutingPriority;
}
const cornerBandDifference = Number(Boolean(getCornerBandSide(second.exitEdge, second.preferredExit))) - Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)));
if (cornerBandDifference !== 0)
return cornerBandDifference;
const firstIsCorner = Boolean(getCornerBandSide(first.exitEdge, first.preferredExit));
if (!firstIsCorner) {
const getSourceSpan = (bus) => {
const xCoordinates = bus.connections.map((connection) => connection.sourcePoint.x);
const yCoordinates = bus.connections.map((connection) => connection.sourcePoint.y);
return Math.max(...xCoordinates) - Math.min(...xCoordinates) + Math.max(...yCoordinates) - Math.min(...yCoordinates);
};
const sourceSpanDifference = getSourceSpan(second) - getSourceSpan(first);
if (Math.abs(sourceSpanDifference) > 0.000000001) {
return sourceSpanDifference;
}
}
const layerDifference = this.config.layerNames.indexOf(firstLayer ?? "") - this.config.layerNames.indexOf(secondLayer ?? "");
if (layerDifference !== 0)
return -layerDifference;
if (unsortedBoundaryBuses.length !== 6 && unsortedBoundaryBuses.length !== 7 && unsortedBoundaryBuses.length !== 8 && unsortedBoundaryBuses.length !== 9) {
return 0;
}
return first.busId.localeCompare(second.busId);
});
const debugBoundaryOrder = process.env.FANOUT_DEBUG_BOUNDARY_ORDER?.split(",") ?? (process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS ? [process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS] : []);
const boundaryBuses = debugBoundaryOrder.length > 0 ? initiallySortedBoundaryBuses.toSorted((first, second) => {
const firstIndex = debugBoundaryOrder.indexOf(first.busId);
const secondIndex = debugBoundaryOrder.indexOf(second.busId);
return (firstIndex < 0 ? Number.POSITIVE_INFINITY : firstIndex) - (secondIndex < 0 ? Number.POSITIVE_INFINITY : secondIndex);
}) : initiallySortedBoundaryBuses;
const planeBuses = this.preparedBuses.filter((bus) => bus.termination.type === "plane");
const denseAdditionalObstacles = useConfiguredDensePlaneRouting ? this.routingSrj.obstacles : undefined;
const initialPlaneReservationCount = Number.parseInt(process.env.FANOUT_INITIAL_PLANE_RESERVATIONS ?? "8", 10);
const debugInitialPlaneIndices = process.env.FANOUT_DEBUG_INITIAL_PLANE_INDICES?.split(",").map((index) => Number(index) - 1);
const debugInitialPlaneBusIds = process.env.FANOUT_DEBUG_INITIAL_PLANE_BUS_IDS?.split(",");
let activeBoundaryReservationPlaneBuses = debugInitialPlaneBusIds ? planeBuses.filter((bus) => debugInitialPlaneBusIds.includes(bus.busId)) : debugInitialPlaneIndices ? debugInitialPlaneIndices.flatMap((index) => planeBuses[index] ? [planeBuses[index]] : []) : this.config.densePlaneReservationBusIds.length > 0 ? planeBuses.filter((bus) => this.config.densePlaneReservationBusIds.includes(bus.busId)) : useConfiguredDensePlaneRouting ? planeBuses.slice(0, Number.isFinite(initialPlaneReservationCount) ? initialPlaneReservationCount : 8) : planeBuses;
if (params.promotedPlaneReservationBusIds) {
activeBoundaryReservationPlaneBuses = [
...new Set([
...activeBoundaryReservationPlaneBuses,
...planeBuses.filter((bus) => params.promotedPlaneReservationBusIds.includes(bus.busId))
])
];
}
const unroutablePlaneBusIds = new Set;
debugDense("start", boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`), `planes:${planeBuses.length}`, `joint:${useJointBoundaryViaReservation}`);
if (boundaryBuses.length === 0 || boundaryBuses.length > 9 || planeBuses.length < 8 || boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) || planeBuses.some((bus) => bus.connections.length !== 1) || boundaryBuses.length + planeBuses.length !== params.busesInRoutingOrder.length) {
return null;
}
const connectionNameByIndex = new Map(this.preparedBuses.flatMap((bus) => bus.connections.map((connection) => [connection.connectionIndex, connection.connection.name])));
const boundaryBusConnectionCounts = boundaryBuses.map((bus) => bus.connections.length);
const singletonBoundaryBusCount = boundaryBusConnectionCounts.filter((connectionCount) => connectionCount === 1).length;
const useGeometryAwareSingletonOutwardPreference = singletonBoundaryBusCount > 1 && shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts);
const preferredBoundaryPerpendicularSideByBusId = new Map(boundaryBuses.map((bus) => [
bus.busId,
hasThreeWideBoundaryBuses && bus.connections.length < 8 && narrowBusIsEmbeddedInWideSourceField(bus) ? -1 : 1
]));
const preferBoundaryOutwardByBusId = new Map(boundaryBuses.map((bus) => [
bus.busId,
bus.connections.length === 1 ? hasThreeWideBoundaryBuses && narrowBusIsEmbeddedInWideSourceField(bus) ? true : useGeometryAwareSingletonOutwardPreference && getCornerBandSide(bus.exitEdge, bus.preferredExit) ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0 : getExitEdgeForDirection(bus.direction) !== bus.exitEdge : hasThreeWideBoundaryBuses && bus.connections.length >= 8 ? false : getExitEdgeForDirection(bus.direction) !== bus.exitEdge
]));
for (const bus of wideBoundaryBuses) {
if (useConfiguredDensePlaneRouting)
continue;
if (!getCornerBandSide(bus.exitEdge, bus.preferredExit))
continue;
const entersNeighboringSourceField = wideBoundaryBuses.some((other) => {
if (other === bus || other.componentId !== bus.componentId)
return false;
const xs = other.connections.map((connection) => connection.sourcePoint.x);
const ys = other.connections.map((connection) => connection.sourcePoint.y);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
return bus.connections.some(({ sourcePoint }) => {
const x = sourcePoint.x + (bus.direction === "left" ? -bus.pitchX / 2 : bus.direction === "right" ? bus.pitchX / 2 : 0);
const y = sourcePoint.y + (bus.direction === "down" ? -bus.pitchY / 2 : bus.direction === "up" ? bus.pitchY / 2 : 0);
return x >= minX && x <= maxX && y >= minY && y <= maxY;
});
});
if (entersNeighboringSourceField)
preferBoundaryOutwardByBusId.set(bus.busId, false);
}
const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS;
if (debugFlippedBoundaryBus) {
preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1);
}
const debugOutwardBoundaryBus = process.env.FANOUT_DEBUG_OUTWARD_BOUNDARY_BUS;
if (debugOutwardBoundaryBus) {
preferBoundaryOutwardByBusId.set(debugOutwardBoundaryBus, true);
}
const canShareCopper = (firstConnectionIndex, secondConnectionIndex) => {
if (!this.config.allowSameNetMerges)
return false;
const firstConnectionName = connectionNameByIndex.get(firstConnectionIndex);
const secondConnectionName = connectionNameByIndex.get(secondConnectionIndex);
return Boolean(firstConnectionName && secondConnectionName && connectionsShareElectricalNet(this.routingSrj, firstConnectionName, secondConnectionName));
};
const singletonBoundaryBuses = boundaryBuses.filter((bus) => bus.connections.length === 1);
const singletonDeferralCandidates = !hasThreeWideBoundaryBuses && shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts) ? singletonBoundaryBuses.toSorted(boundaryBuses.length === 9 ? compareDenseSingletonBoundaryDeferralPriority : compareReleasedDenseSingletonBoundaryDeferralPriority).slice(0, getDenseSingletonDeferralCandidateCount(boundaryBusConnectionCounts)) : [];
const multiLayerLeadingSingletonBuses = boundaryBuses.length === 8 || boundaryBuses.length === 9 ? singletonDeferralCandidates.filter((singletonBus) => {
const singletonTargetLayer = params.busLayerAssignments[singletonBus.busId];
return Boolean(singletonTargetLayer && isDenseSingletonEmbeddedInMultiLayerWideBus({
singletonBus,
singletonTargetLayer,
wideBuses: boundaryBuses
}));
}) : [];
const throughAllLeadingSingletonBuses = hasThreeWideBoundaryBuses ? singletonBoundaryBuses.filter((singletonBus) => {
const containingWideBus = getContainingWideSourceField(singletonBus);
const singletonTargetLayer = params.busLayerAssignments[singletonBus.busId];
const containingWideLayers = containingWideBus?.routableEscapeLayers ?? containingWideBus?.allowedLayers ?? [];
return Boolean(containingWideBus && singletonTargetLayer && !containingWideLayers.includes(singletonTargetLayer));
}) : [];
const throughAllLeadingCompanionBuses = throughAllLeadingSingletonBuses.flatMap((singletonBus) => {
const containingWideBus = getContainingWideSourceField(singletonBus);
const singletonTargetLayer = params.busLayerAssignments[singletonBus.busId];
return boundaryBuses.filter((candidate) => candidate.connections.length > 1 && candidate.connections.length < 8 && candidate.direction === singletonBus.direction && params.busLayerAssignments[candidate.busId] === singletonTargetLayer && getContainingWideSourceField(candidate) === containingWideBus);
});
const throughAllLeadingBuses = process.env.FANOUT_DEBUG_DISABLE_LEADING_NARROW === "1" ? [] : throughAllLeadingSingletonBuses.flatMap((singletonBus) => [
singletonBus,
...throughAllLeadingCompanionBuses.filter((candidate) => candidate.direction === singletonBus.direction && params.busLayerAssignments[candidate.busId] === params.busLayerAssignments[singletonBus.busId])
]);
const leadingWideSingletonBuses = [
...multiLayerLeadingSingletonBuses,
...throughAllLeadingBuses
].filter((bus, index, buses) => buses.indexOf(bus) === index);
const leadingLaneCountByWideCornerBand = new Map;
if (boundaryBuses.length === 9 && leadingWideSingletonBuses.length > 0) {
for (const bus of leadingWideSingletonBuses) {
preferBoundaryOutwardByBusId.set(bus.busId, true);
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit);
if (!bus.exitEdge || !side)
continue;
const bandKey = `${bus.exitEdge}:${side}`;
const sharesBandWithWideBus = boundaryBuses.some((candidate) => {
if (candidate === bus || candidate.connections.length < 8)
return false;
return candidate.exitEdge === bus.exitEdge && getCornerBandSide(candidate.exitEdge, candidate.preferredExit) === side;
});
if (!sharesBandWithWideBus)
continue;
leadingLaneCountByWideCornerBand.set(bandKey, (leadingLaneCountByWideCornerBand.get(bandKey) ?? 0) + bus.connections.length);
}
}
const viaProvisionalBoundaryBusSet = new Set([
...process.env.FANOUT_DEBUG_PROVISIONAL_NARROW === "1" ? boundaryBuses.filter((bus) => bus.connections.length < 8) : [],
...singletonDeferralCandidates.filter((bus) => {
const containingBus = getContainingWideSourceField(bus);
const sharesContainingBusLayer = usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting && containingBus && params.busLayerAssignments[containingBus.busId] === params.busLayerAssignments[bus.busId];
return !leadingWideSingletonBuses.includes(bus) && !sharesContainingBusLayer;
}),
...hasThreeWideBoundaryBuses ? boundaryBuses.filter((bus) => bus.connections.length === 2 && !narrowBusOverlapsWideTargetSpan(bus) && !leadingWideSingletonBuses.includes(bus)) : []
]);
const getCornerBandTargetTrackOffset = (bus) => {
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit);
if (!bus.exitEdge || !side)
return 0;
const leadingLaneCount = leadingLaneCountByWideCornerBand.get(`${bus.exitEdge}:${side}`) ?? 0;
return getDenseLeadingCornerBandTargetTrackOffset({
leadingLaneCount,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
clearance: this.config.clearance
});
};
const initiallyMatchedBoundaryBuses = boundaryBuses.filter((bus) => !viaProvisionalBoundaryBusSet.has(bus));
const jointViaPoints = useJointBoundaryViaReservation ? matchComponentDogboneViaSites([
...activeBoundaryReservationPlaneBuses,
...initiallyMatchedBoundaryBuses
], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
}) : null;
let seedViaPoints = jointViaPoints ?? matchComponentDogboneViaSites([...activeBoundaryReservationPlaneBuses, boundaryBuses[0]], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 20000,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
const debugFixedVias = process.env.FANOUT_DEBUG_FIXED_VIAS;
if (seedViaPoints && debugFixedVias) {
seedViaPoints = new Map(seedViaPoints);
for (const entry of debugFixedVias.split(",")) {
const parts = entry.split(":");
const rawY = parts.pop();
const rawX = parts.pop();
const connectionName = parts.join(":");
const connectionIndex = [...connectionNameByIndex].find(([, name]) => name === connectionName)?.[0];
const x = Number(rawX);
const y = Number(rawY);
if (connectionIndex !== undefined && Number.isFinite(x) && Number.isFinite(y)) {
seedViaPoints.set(connectionIndex, { x, y });
}
}
}
const debugStagedPlaneBuses = process.env.FANOUT_DEBUG_STAGED_PLANE_BUS_IDS ? planeBuses.filter((bus) => process.env.FANOUT_DEBUG_STAGED_PLANE_BUS_IDS.split(",").includes(bus.busId)) : process.env.FANOUT_DEBUG_STAGED_PLANE_INDICES?.split(",").flatMap((index) => planeBuses[Number(index) - 1] ? [planeBuses[Number(index) - 1]] : []) ?? [];
if (seedViaPoints && debugStagedPlaneBuses.length > 0) {
for (const stagedPlaneBus of debugStagedPlaneBuses) {
if (activeBoundaryReservationPlaneBuses.includes(stagedPlaneBus)) {
continue;
}
const stagedViaPoints = matchComponentDogboneViaSites([
...activeBoundaryReservationPlaneBuses,
stagedPlaneBus,
...initiallyMatchedBoundaryBuses
], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: seedViaPoints,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
debugDense("plane-reservation:staged", stagedPlaneBus.busId, stagedViaPoints?.size ?? "failed");
if (!stagedViaPoints)
break;
seedViaPoints = new Map([...seedViaPoints, ...stagedViaPoints]);
activeBoundaryReservationPlaneBuses.push(stagedPlaneBus);
}
}
debugDense("seed", seedViaPoints?.size ?? "failed");
if (seedViaPoints && process.env.FANOUT_DEBUG_DENSE_POINTS === "1") {
console.error("dense: seed-points", [...seedViaPoints].map(([connectionIndex, point]) => ({
connection: connectionNameByIndex.get(connectionIndex),
point
})));
}
let denseWorkUnitIndex = 1;
const denseWorkUnitCount = boundaryBuses.length + planeBuses.length + 3;
this.setInProgressPlans({
phase: "reserve-dense-via-sites",
plans: [],
strategy: "default",
unitIndex: denseWorkUnitIndex,
unitCount: denseWorkUnitCount
});
yield;
if (seedViaPoints) {
const denseBoundaryBusesInRoutingOrder = [
...multiLayerLeadingSingletonBuses,
...boundaryBuses.filter((bus) => !multiLayerLeadingSingletonBuses.includes(bus) && !throughAllLeadingBuses.includes(bus)).flatMap((bus) => [
...throughAllLeadingBuses.filter((candidate) => getContainingWideSourceField(candidate) === bus),
bus
])
];
let fixedViaPointsByConnectionIndex = seedViaPoints;
let matchedPlans = [];
let matchedRoutingSucceeded = true;
const getReservedVias = (bus) => {
const currentConnectionNames = new Set(bus.connections.map((connection) => connection.connection.name));
return this.preparedBuses.flatMap((preparedBus) => {
const targetLayer = params.busLayerAssignments[preparedBus.busId];
if (!targetLayer)
return [];
return preparedBus.connections.flatMap((connection) => {
if (currentConnectionNames.has(connection.connection.name))
return [];
const center = fixedViaPointsByConnectionIndex.get(connection.connectionIndex);
if (!center)
return [];
return [
{
connectionName: connection.connection.name,
sourceEscapeSegment: matchedPlans.some((plan) => plan.connectionIndex === connection.connectionIndex) ? undefined : {
start: connection.sourcePoint,
end: center,
layer: connection.sourceLayer,
width: this.config.traceWidth
},
via: {
center,
diameter: this.config.viaDiameter,
spanLayers: getViaSpanLayers({
fromLayer: connection.sourceLayer,
toLayer: targetLayer,
layerNames: this.config.layerNames,
allowBlindAndBuriedVias: false
})
}
}
];
});
});
};
const routeMatchedBoundaryBusSteps = function* (bus) {
debugDense("route:start", bus.busId, matchedPlans.length);
if (process.env.FANOUT_DEBUG_DENSE_POINTS === "1") {
console.error("dense: points", bus.busId, bus.connections.map((connection) => ({
source: connection.sourcePoint,
via: fixedViaPointsByConnectionIndex.get(connection.connectionIndex),
target: connection.exitTargetPoint ?? connection.targetPoint
})));
}
const targetLayer = params.busLayerAssignments[bus.busId];
if (!targetLayer) {
return false;
}
const adaptiveWindingRouteOrder = !useConfiguredDensePlaneRouting && !getCornerBandSide(bus.exitEdge, bus.preferredExit) && bus.connections.length > 2 && wideBoundaryBuses.some((candidate) => getCornerBandSide(candidate.exitEdge, candidate.preferredExit));
const routeParams = {
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans: matchedPlans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache,
fixedViaPointsByConnectionIndex,
reservedVias: getReservedVias(bus),
viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
allowBoundarySideViaFallback: bus.connections.length === 1,
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
adaptiveWindingRouteOrder,
alignWindingGridToPads: usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting,
fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder ? 60 : useConfiguredDensePlaneRouting ? 6 : 24,
cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus)
};
const routeAlternatives = function* (candidateRouteParams, maximumAlternatives) {
this.activeRoutingVisualization = null;
const solver = this.createWorkSolver("BoundaryBusRoutingSolver", this.routeBusAlternativesWorkSteps(candidateRouteParams, maximumAlternatives), undefined, () => this.visualizeBoundaryRoutingState());
return yield { type: "subsolver", solver };
}.bind(this);
const routableEscapeLayers = bus.routableEscapeLayers ?? bus.allowedLayers ?? [];
const singleLayerBus = routableEscapeLayers.some((layer) => layer !== targetLayer) ? { ...bus, routableEscapeLayers: [targetLayer] } : bus;
const embeddedNarrowBusAlreadyRouted = boundaryBuses.some((candidate) => candidate.connections.length < 8 && getContainingWideSourceField(candidate) === bus && matchedPlans.some((plan) => plan.busId === candidate.busId));
const preferSingleLayerWinding = useConfiguredDensePlaneRouting && singleLayerBus !== bus && !embeddedNarrowBusAlreadyRouted;
let busPlans = (yield* routeAlternatives(preferSingleLayerWinding ? { ...routeParams, bus: singleLayerBus } : routeParams, 1))[0];
if (!busPlans && preferSingleLayerWinding) {
busPlans = (yield* routeAlternatives(routeParams, 1))[0];
}
if (!busPlans) {
const originalPoints = fixedViaPointsByConnectionIndex;
const originalOutward = preferBoundaryOutwardByBusId.get(bus.busId) ?? true;
const originalSide = preferredBoundaryPerpendicularSideByBusId.get(bus.busId) ?? 1;
for (const [outward, side] of [
[!originalOutward, originalSide],
[originalOutward, -originalSide],
[!originalOutward, -originalSide]
]) {
const rematchedPoints = matchComponentDogboneViaSites([
...new Set([
...activeBoundaryReservationPlaneBuses,
...initiallyMatchedBoundaryBuses,
...this.preparedBuses.filter((candidate) => matchedPlans.some((plan) => plan.busId === candidate.busId)),
bus
])
], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId: new Map([
...preferredBoundaryPerpendicularSideByBusId,
[bus.busId, side]
]),
preferBoundaryOutwardByBusId: new Map([
...preferBoundaryOutwardByBusId,
[bus.busId, outward]
]),
fixedViaPointsByConnectionIndex: new Map(matchedPlans.filter((plan) => plan.via).map((plan) => [plan.connectionIndex, plan.via.center])),
preferredViaPointsByConnectionIndex: new Map([...originalPoints].filter(([index]) => !bus.connections.some((connection) => connection.connectionIndex === index))),
blockingSegments: matchedPlans.flatMap((plan) => plan.segments.map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
}))),
additionalObstacles: this.routingSrj.obstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
debugDense("retry-sites", bus.busId, outward, side, rematchedPoints?.size ?? "failed");
if (!rematchedPoints)
continue;
fixedViaPointsByConnectionIndex = rematchedPoints;
busPlans = (yield* routeAlternatives({
...routeParams,
fixedViaPointsByConnectionIndex: rematchedPoints,
reservedVias: getReservedVias(bus),
alignWindingGridToPads: useConfiguredDensePlaneRouting,
fixedViaFallbackRouteOrderAttempts: 3
}, 1))[0];
if (busPlans)
break;
}
if (!busPlans) {
fixedViaPointsByConnectionIndex = originalPoints;
if (bus.connections.length === 2) {
busPlans = (yield* routeAlternatives({
...routeParams,
fixedViaPointsByConnectionIndex: originalPoints,
reservedVias: getReservedVias(bus),
allowBoundarySideViaFallback: true,
fixedViaFallbackRouteOrderAttempts: 1
}, 1))[0];
}
}
}
if (busPlans && bus.maxLengthSkew !== undefined) {
const lengths = busPlans.map((plan) => plan.length);
const rawSkew = Math.max(...lengths) - Math.min(...lengths);
const needsRouteDiversity = shouldSearchAdditionalBoundaryRouteTopologies({
boundaryBusCount: boundaryBuses.length,
connectionCount: bus.connections.length,
rawSkew,
maximumSkew: bus.maxLengthSkew
});
if (needsRouteDiversity && !matchLengthsAfterPlanes) {
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted((first, second) => {
const firstLengths = first.map((plan) => plan.length);
const secondLengths = second.map((plan) => plan.length);
return Math.max(...firstLengths) - Math.min(...firstLengths) - (Math.max(...secondLengths) - Math.min(...secondLengths));
})[0];
}
}
if (!busPlans) {
debugDense("route:failed", bus.busId);
return false;
}
matchedPlans.push(...busPlans);
debugDense("route:complete", bus.busId, busPlans.length);
return true;
}.bind(this);
const firstBoundaryBus = denseBoundaryBusesInRoutingOrder[0];
const routedBoundaryBuses = [];
const reserveAllPlaneDogbonesAfterFirstWideBus = (bus) => {
if (bus.connections.length >= 8 && !useConfiguredDensePlaneRouting && process.env.FANOUT_DEBUG_NO_PLANE_EXPANSION !== "1" && activeBoundaryReservationPlaneBuses.length < planeBuses.length) {
activeBoundaryReservationPlaneBuses = planeBuses;
debugDense("plane-reservations:expanded", planeBuses.length);
}
};
const firstBoundaryBusRouted = yield* routeMatchedBoundaryBusSteps(firstBoundaryBus);
this.setInProgressPlans({
phase: "route-dense-boundary-buses",
plans: matchedPlans,
strategy: "default",
unitIndex: ++denseWorkUnitIndex,
unitCount: denseWorkUnitCount,
busId: firstBoundaryBus.busId
});
yield;
if (firstBoundaryBusRouted) {
routedBoundaryBuses.push(firstBoundaryBus);
reserveAllPlaneDogbonesAfterFirstWideBus(firstBoundaryBus);
} else {
matchedRoutingSucceeded = false;
}
const remainingBoundaryBuses = denseBoundaryBusesInRoutingOrder.slice(1);
while (matchedRoutingSucceeded && remainingBoundaryBuses.length > 0) {
const blockingSegments = matchedPlans.flatMap((plan) => plan.segments.map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
})));
let selectedBusIndex = -1;
for (let candidateIndex = 0;candidateIndex < remainingBoundaryBuses.length; candidateIndex++) {
const candidateBus = remainingBoundaryBuses[candidateIndex];
debugDense("candidate:start", candidateBus.busId);
const candidateMatchingBase = new Map(fixedViaPointsByConnectionIndex);
const debugLateFixedVias = process.env.FANOUT_DEBUG_LATE_FIXED_VIAS;
if (debugLateFixedVias) {
const candidateConnectionIndices = new Set(candidateBus.connections.map((connection) => connection.connectionIndex));
for (const entry of debugLateFixedVias.split(",")) {
const parts = entry.split(":");
const rawY = parts.pop();
const rawX = parts.pop();
const connectionName = parts.join(":");
const connectionIndex = [...connectionNameByIndex].find(([, name]) => name === connectionName)?.[0];
const x = Number(rawX);
const y = Number(rawY);
if (connectionIndex !== undefined && candidateConnectionIndices.has(connectionIndex) && Number.isFinite(x) && Number.isFinite(y)) {
candidateMatchingBase.set(connectionIndex, { x, y });
}
}
}
const candidateHasFixedViaPoints = candidateBus.connections.every((connection) => candidateMatchingBase.has(connection.connectionIndex));
const newlyMatchedViaPoints = jointViaPoints && candidateHasFixedViaPoints ? new Map(candidateMatchingBase) : matchComponentDogboneViaSites([
...activeBoundaryReservationPlaneBuses,
...routedBoundaryBuses,
candidateBus
], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: candidateMatchingBase,
blockingSegments,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
const extendedViaPoints = newlyMatchedViaPoints ? new Map([...candidateMatchingBase, ...newlyMatchedViaPoints]) : null;
debugDense("candidate:matched", candidateBus.busId, extendedViaPoints?.size ?? "failed");
this.setInProgressPlans({
phase: "reserve-next-dense-boundary-bus",
plans: matchedPlans,
strategy: "default",
unitIndex: denseWorkUnitIndex,
unitCount: denseWorkUnitCount,
busId: candidateBus.busId
});
yield;
if (!extendedViaPoints)
continue;
const previousFixedViaPoints = fixedViaPointsByConnectionIndex;
const previousPlanCount = matchedPlans.length;
const laterBuses = remainingBoundaryBuses.filter((_, laterIndex) => laterIndex !== candidateIndex);
let candidateFixedViaPoints = extendedViaPoints;
if (laterBuses.length === 1) {
const laterBus = laterBuses[0];
const futureAssignment = matchComponentDogboneViaSites([
...activeBoundaryReservationPlaneBuses,
...routedBoundaryBuses,
candidateBus,
laterBus
], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: extendedViaPoints,
blockingSegments,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
debugDense("future:matched", laterBus.busId, futureAssignment?.size ?? "failed");
if (futureAssignment) {
const candidateCountByConnectionIndex = new Map;
for (const candidate of getComponentDogboneViaSiteCandidates([laterBus], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
blockingSegments,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
})) {
candidateCountByConnectionIndex.set(candidate.connectionIndex, (candidateCountByConnectionIndex.get(candidate.connectionIndex) ?? 0) + 1);
}
const constrainedConnections = laterBus.connections.toSorted((first, second) => (candidateCountByConnectionIndex.get(first.connectionIndex) ?? 0) - (candidateCountByConnectionIndex.get(second.connectionIndex) ?? 0) || first.connectionIndex - second.connectionIndex);
const repairedViaPoints = new Map(extendedViaPoints);
for (const connection of constrainedConnections) {
const criticalPoint = futureAssignment.get(connection.connectionIndex);
if (criticalPoint) {
repairedViaPoints.set(connection.connectionIndex, criticalPoint);
}
}
candidateFixedViaPoints = repairedViaPoints;
}
}
fixedViaPointsByConnectionIndex = candidateFixedViaPoints;
if (yield* routeMatchedBoundaryBusSteps(candidateBus)) {
debugDense("lookahead:start", candidateBus.busId);
const candidateLeavesAFeasibleExtension = laterBuses.length === 0 || laterBuses.some((laterBus) => {
const lookaheadBlockingSegments = matchedPlans.flatMap((plan) => plan.segments.map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
})));
return Boolean(matchComponentDogboneViaSites([
...activeBoundaryReservationPlaneBuses,
...routedBoundaryBuses,
candidateBus,
laterBus
], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex,
blockingSegments: lookaheadBlockingSegments,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
}));
});
debugDense("lookahead:complete", candidateBus.busId, candidateLeavesAFeasibleExtension);
if (candidateLeavesAFeasibleExtension) {
selectedBusIndex = candidateIndex;
routedBoundaryBuses.push(candidateBus);
reserveAllPlaneDogbonesAfterFirstWideBus(candidateBus);
this.setInProgressPlans({
phase: "route-dense-boundary-buses",
plans: matchedPlans,
strategy: "default",
unitIndex: ++denseWorkUnitIndex,
unitCount: denseWorkUnitCount,
busId: candidateBus.busId
});
yield;
break;
}
matchedPlans.splice(previousPlanCount);
}
fixedViaPointsByConnectionIndex = previousFixedViaPoints;
this.setInProgressPlans({
phase: "retry-dense-boundary-bus",
plans: matchedPlans,
strategy: "default",
unitIndex: denseWorkUnitIndex,
unitCount: denseWorkUnitCount,
busId: candidateBus.busId
});
yield;
}
if (selectedBusIndex < 0) {
matchedRoutingSucceeded = false;
break;
}
remainingBoundaryBuses.splice(selectedBusIndex, 1);
}
let matchedPlaneBusesInRoutingOrder = null;
if (matchedRoutingSucceeded) {
let feasibleViaPoints = null;
let feasibleAlternatePlanePlans = [];
const matchViaPointsAroundPlans = (candidatePlans, promotedAlternatePlaneBusIds = new Set) => {
feasibleAlternatePlanePlans = [];
const fixedBoundaryViaPoints = new Map(candidatePlans.flatMap((plan) => plan.via ? [[plan.connectionIndex, plan.via.center]] : []));
const blockingSegments = candidatePlans.flatMap((plan) => plan.segments.map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
})));
const preserveBoundaryCopper = !useConfiguredDensePlaneRouting || candidatePlans.some((plan) => plan.segments.filter((segment) => segment.layer === plan.sourceLayer).length > 1);
const boundaryBusesToMatch = !preserveBoundaryCopper ? boundaryBuses : [];
const blockingVias = !preserveBoundaryCopper ? [] : candidatePlans.flatMap((plan) => [
plan.via,
...plan.additionalVias ?? [],
plan.planeEndpointVia
].filter((via) => via !== undefined).map((via) => ({
connectionIndex: plan.connectionIndex,
center: via.center,
diameter: via.diameter,
spanLayers: via.spanLayers
})));
const retainedViaPoints = !preserveBoundaryCopper ? null : matchComponentDogboneViaSites(planeBuses, {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: new Map([
...fixedViaPointsByConnectionIndex,
...fixedBoundaryViaPoints
]),
blockingSegments,
blockingVias,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
if (retainedViaPoints)
return new Map([...fixedBoundaryViaPoints, ...retainedViaPoints]);
if (useConfiguredDensePlaneRouting || process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1") {
let incrementalViaPoints = new Map(fixedBoundaryViaPoints);
const matchedPlaneBuses = [...activeBoundaryReservationPlaneBuses];
for (const planeBus of matchedPlaneBuses) {
for (const connection of planeBus.connections) {
const reservedPoint = fixedViaPointsByConnectionIndex.get(connection.connectionIndex);
if (reservedPoint) {
incrementalViaPoints.set(connection.connectionIndex, reservedPoint);
}
}
}
for (const planeBus of matchedPlaneBuses) {
const targetLayer = params.busLayerAssignments[planeBus.busId];
if (!targetLayer)
return null;
const reservedPlanePlans = routeBus({
srj: this.routingSrj,
bus: planeBus,
targetLayer,
acceptedPlans: [
...candidatePlans,
...feasibleAlternatePlanePlans
],
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache,
fixedViaPointsByConnectionIndex: incrementalViaPoints
});
if (!reservedPlanePlans)
return null;
feasibleAlternatePlanePlans.push(...reservedPlanePlans);
}
const independentlyUnmatchablePlaneBuses = planeBuses.filter((planeBus) => !matchedPlaneBuses.includes(planeBus) && !matchComponentDogboneViaSites([...matchedPlaneBuses, planeBus, ...boundaryBusesToMatch], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: incrementalViaPoints,
blockingSegments,
blockingVias,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
}));
const shallowestPlaneLayerIndex = Math.min(...planeBuses.map((bus) => this.config.layerNames.indexOf(params.busLayerAssignments[bus.busId] ?? "")));
const deeperPlaneBuses = useConfiguredDensePlaneRouting || process.env.FANOUT_DEBUG_ROUTE_DEEP_PLANES_FIRST === "1" ? planeBuses.filter((bus) => !matchedPlaneBuses.includes(bus) && this.config.layerNames.indexOf(params.busLayerAssignments[bus.busId] ?? "") > shallowestPlaneLayerIndex) : [];
const additionalAlternatePlaneBusIds = new Set([
...promotedAlternatePlaneBusIds,
...this.config.denseUnrestrictedPlaneRoutingBusIds,
...process.env.FANOUT_DEBUG_ADDITIONAL_ALTERNATE_PLANE_BUS_IDS?.split(",") ?? []
]);
const additionalAlternatePlaneBuses = planeBuses.filter((bus) => !matchedPlaneBuses.includes(bus) && additionalAlternatePlaneBusIds.has(bus.busId));
const alternatePlaneBuses = [
...deeperPlaneBuses,
...independentlyUnmatchablePlaneBuses.filter((bus) => !deeperPlaneBuses.includes(bus)),
...additionalAlternatePlaneBuses.filter((bus) => !deeperPlaneBuses.includes(bus) && !independentlyUnmatchablePlaneBuses.includes(bus))
];
const debugAlternatePlaneOrder = process.env.FANOUT_DEBUG_ALTERNATE_PLANE_ORDER?.split(",") ?? [];
const orderedAlternatePlaneBuses = alternatePlaneBuses.toSorted((first, second) => {
const firstLayerIndex = this.config.layerNames.indexOf(params.busLayerAssignments[first.busId] ?? "");
const secondLayerIndex = this.config.layerNames.indexOf(params.busLayerAssignments[second.busId] ?? "");
if (firstLayerIndex !== secondLayerIndex && process.env.FANOUT_DEBUG_ALTERNATE_IGNORE_LAYERS !== "1") {
return process.env.FANOUT_DEBUG_ALTERNATE_SHALLOW_FIRST === "1" ? firstLayerIndex - secondLayerIndex : secondLayerIndex - firstLayerIndex;
}
const firstPriority = debugAlternatePlaneOrder.indexOf(first.busId);
const secondPriority = debugAlternatePlaneOrder.indexOf(second.busId);
return (firstPriority < 0 ? debugAlternatePlaneOrder.length : firstPriority) - (secondPriority < 0 ? debugAlternatePlaneOrder.length : secondPriority) || first.connections[0].connectionIndex - second.connections[0].connectionIndex;
});
if (alternatePlaneBuses.length > 0) {
debugDense("plane-match:preflight-failed", independentlyUnmatchablePlaneBuses.map((bus) => bus.busId));
if (!useConfiguredDensePlaneRouting && process.env.FANOUT_DEBUG_ROUTE_UNMATCHED_PLANES !== "1") {
return null;
}
let alternatePlaneSearchStates = 0;
const maximumAlternatePlaneSearchStates = Number(process.env.FANOUT_DEBUG_ALTERNATE_SEARCH_STATES ?? (useConfiguredDensePlaneRouting ? 3000000 : 1000));
const maximumAlternatePlaneRoutes = Number(process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ?? (useConfiguredDensePlaneRouting ? 128 : 8));
let deepestAlternatePlaneSearchIndex = 0;
const alternatePlaneFailureCountByBusId = new Map;
const getPlaneRouteAlternatives = (planeBus, additionalAcceptedPlans, maximumRoutes = maximumAlternatePlaneRoutes) => {
const targetLayer = params.busLayerAssignments[planeBus.busId];
if (!targetLayer)
return [];
return routeBusAlternatives({
srj: this.routingSrj,
bus: planeBus,
targetLayer,
acceptedPlans: [
...candidatePlans,
...additionalAcceptedPlans
],
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache
}, maximumRoutes);
};
const routeAlternatePlaneBuses = (remainingPlaneBuses, acceptedAlternatePlans) => {
if (remainingPlaneBuses.length === 0) {
return acceptedAlternatePlans;
}
if (alternatePlaneSearchStates >= maximumAlternatePlaneSearchStates) {
return null;
}
deepestAlternatePlaneSearchIndex = Math.max(deepestAlternatePlaneSearchIndex, orderedAlternatePlaneBuses.length - remainingPlaneBuses.length);
const alternativesByBus = remainingPlaneBuses.map((planeBus2) => ({
planeBus: planeBus2,
alternatives: getPlaneRouteAlternatives(planeBus2, acceptedAlternatePlans)
}));
const orderedSelections = process.env.FANOUT_DEBUG_DYNAMIC_ALTERNATE_ORDER === "1" ? alternativesByBus.toSorted((first, second) => first.alternatives.length - second.alternatives.length || orderedAlternatePlaneBuses.indexOf(first.planeBus) - orderedAlternatePlaneBuses.indexOf(second.planeBus)) : alternativesByBus;
const selected = orderedSelections[0];
const { planeBus, alternatives } = selected;
if (process.env.FANOUT_DEBUG_ALTERNATE_CHOICES === "1" && alternatePlaneSearchStates < 64) {
debugDense("plane-route:alternate-choice", `depth:${orderedAlternatePlaneBuses.length - remainingPlaneBuses.length}`, planeBus.busId, alternativesByBus.map((entry) => [
entry.planeBus.busId,
entry.alternatives.length
]));
}
if (alternatives.length === 0) {
alternatePlaneFailureCountByBusId.set(planeBus.busId, (alternatePlaneFailureCountByBusId.get(planeBus.busId) ?? 0) + 1);
return null;
}
const selectionsToSearch = process.env.FANOUT_DEBUG_BRANCH_ALTERNATE_ORDER === "1" ? orderedSelections : [selected];
const alternateActions = selectionsToSearch.flatMap((selection) => selection.alternatives.map((alternative) => ({
selection,
alternative,
remaining: remainingPlaneBuses.filter((candidate) => candidate !== selection.planeBus)
})));
const orderedActions = process.env.FANOUT_DEBUG_LEAST_CONSTRAINING_ALTERNATES === "1" ? alternateActions.map((action) => {
const acceptedPlans = [
...acceptedAlternatePlans,
...action.alternative
];
const remainingOptionCounts = action.remaining.map((remainingBus) => getPlaneRouteAlternatives(remainingBus, acceptedPlans, Math.min(4, maximumAlternatePlaneRoutes)).length);
return {
...action,
remainingOptionCounts,
minimumRemainingOptions: remainingOptionCounts.length === 0 ? Number.POSITIVE_INFINITY : Math.min(...remainingOptionCounts),
totalRemainingOptions: remainingOptionCounts.reduce((total, count) => total + count, 0)
};
}).filter((action) => action.minimumRemainingOptions !== 0).toSorted((first, second) => second.minimumRemainingOptions - first.minimumRemainingOptions || second.totalRemainingOptions - first.totalRemainingOptions) : alternateActions;
for (const action of orderedActions) {
alternatePlaneSearchStates++;
const completedPlans = routeAlternatePlaneBuses(action.remaining, [...acceptedAlternatePlans, ...action.alternative]);
if (completedPlans)
return completedPlans;
if (alternatePlaneSearchStates >= maximumAlternatePlaneSearchStates) {
break;
}
}
alternatePlaneFailureCountByBusId.set(planeBus.busId, (alternatePlaneFailureCountByBusId.get(planeBus.busId) ?? 0) + 1);
return null;
};
let alternatePlanePlans;
if (useConfiguredDensePlaneRouting || process.env.FANOUT_DEBUG_EXACT_COVER_ALTERNATES === "1") {
const candidateSets = orderedAlternatePlaneBuses.map((planeBus) => ({
planeBus,
candidates: getPlaneRouteAlternatives(planeBus, feasibleAlternatePlanePlans).map((plans, index) => ({
key: `${planeBus.busId}:${index}`,
planeBus,
plans
}))
}));
if (useAdaptiveJointPlaneSelection) {
const acceptedPlans = [
...candidatePlans,
...feasibleAlternatePlanePlans
];
for (const planeBus of planeBuses) {
if (matchedPlaneBuses.includes(planeBus) || candidateSets.some((set) => set.planeBus === planeBus))
continue;
const candidates = [];
const sites = getComponentDogboneViaSiteCandidates([planeBus], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
blockingSegments: acceptedPlans.flatMap((plan) => plan.segments.map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
}))),
blockingVias: acceptedPlans.flatMap((plan) => [
plan.via,
...plan.additionalVias ?? [],
plan.planeEndpointVia
].filter((via) => via !== undefined).map((via) => ({
connectionIndex: plan.connectionIndex,
center: via.center,
diameter: via.diameter,
spanLayers: via.spanLayers
}))),
additionalObstacles: this.routingSrj.obstacles,
preferPlaneCheckerboardSites: true,
canShareCopper
});
for (const site of sites) {
const plans = routeBus({
srj: this.routingSrj,
bus: planeBus,
targetLayer: params.busLayerAssignments[planeBus.busId],
acceptedPlans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
fixedViaPointsByConnectionIndex: new Map([
[site.connectionIndex, site.point]
])
});
if (plans)
candidates.push({
key: `${planeBus.busId}:local:${candidates.length}`,
planeBus,
plans
});
}
candidateSets.push({ planeBus, candidates });
}
}
debugDense("plane-route:alternate-candidate-counts", candidateSets.map((candidateSet) => [
candidateSet.planeBus.busId,
candidateSet.candidates.length
]));
for (const candidateSet of candidateSets) {
if (candidateSet.candidates.length === 0)
unroutablePlaneBusIds.add(candidateSet.planeBus.busId);
}
const compatibilityByCandidatePair = new Map;
const candidatesAreCompatible = (first, second) => {
const cacheKey = [first.key, second.key].toSorted().join("|");
const cached = compatibilityByCandidatePair.get(cacheKey);
if (cached !== undefined)
return cached;
const compatible = fanoutPlansAreMutuallyClear({
plans: [...first.plans, ...second.plans],
srj: this.routingSrj,
clearance: this.config.clearance,
allowSameNetMerges: this.config.allowSameNetMerges
});
compatibilityByCandidatePair.set(cacheKey, compatible);
return compatible;
};
const selectCompatiblePlaneRoutes = (remainingCandidateSets, selectedCandidates2) => {
if (remainingCandidateSets.length === 0) {
return selectedCandidates2;
}
if (alternatePlaneSearchStates >= maximumAlternatePlaneSearchStates) {
return null;
}
deepestAlternatePlaneSearchIndex = Math.max(deepestAlternatePlaneSearchIndex, orderedAlternatePlaneBuses.length - remainingCandidateSets.length);
const selectedSet = remainingCandidateSets.toSorted((first, second) => first.candidates.length - second.candidates.length)[0];
if (selectedSet.candidates.length === 0) {
alternatePlaneFailureCountByBusId.set(selectedSet.planeBus.busId, (alternatePlaneFailureCountByBusId.get(selectedSet.planeBus.busId) ?? 0) + 1);
return null;
}
const otherSets = remainingCandidateSets.filter((candidateSet) => candidateSet !== selectedSet);
const candidateBatchSize = Number(process.env.FANOUT_DEBUG_ALTERNATE_CANDIDATE_BATCH_SIZE ?? 16);
for (let batchStart = 0;batchStart < selectedSet.candidates.length; batchStart += candidateBatchSize) {
const actions = selectedSet.candidates.slice(batchStart, batchStart + candidateBatchSize).map((candidate) => {
const projectedSets = otherSets.map((candidateSet) => ({
...candidateSet,
candidates: candidateSet.candidates.filter((otherCandidate) => candidatesAreCompatible(candidate, otherCandidate))
}));
const projectedCounts = projectedSets.map((candidateSet) => candidateSet.candidates.length);
return {
candidate,
projectedSets,
minimumProjectedCount: projectedCounts.length === 0 ? Number.POSITIVE_INFINITY : Math.min(...projectedCounts),
totalProjectedCount: projectedCounts.reduce((total, count) => total + count, 0)
};
}).filter((action) => action.minimumProjectedCount !== 0).toSorted((first, second) => second.minimumProjectedCount - first.minimumProjectedCount || second.totalProjectedCount - first.totalProjectedCount);
for (const action of actions) {
alternatePlaneSearchStates++;
const selected = selectCompatiblePlaneRoutes(action.projectedSets, [...selectedCandidates2, action.candidate]);
if (selected)
return selected;
if (alternatePlaneSearchStates >= maximumAlternatePlaneSearchStates) {
break;
}
}
if (alternatePlaneSearchStates >= maximumAlternatePlaneSearchStates) {
break;
}
}
alternatePlaneFailureCountByBusId.set(selectedSet.planeBus.busId, (alternatePlaneFailureCountByBusId.get(selectedSet.planeBus.busId) ?? 0) + 1);
return null;
};
let selectedCandidates;
if (useAdaptiveDensePlaneRouting) {
const getUniqueDomains = (sets) => sets.map((set) => [
...new Map(set.candidates.map((candidate) => [
JSON.stringify(candidate.plans.map((plan) => [
plan.segments,
plan.via,
plan.additionalVias,
plan.planeEndpointSegments,
plan.planeEndpointVia
])),
candidate
])).values()
]);
const runCandidateSelection = (sets, maximumSearchStates = maximumAlternatePlaneSearchStates) => selectCompatibleCandidates({
candidateSets: getUniqueDomains(sets),
maximumSearchStates,
areCompatible: (first, second) => fanoutPlansAreMutuallyClear({
plans: [...first.plans, ...second.plans],
srj: this.routingSrj,
clearance: this.config.clearance,
allowSameNetMerges: this.config.allowSameNetMerges
})
});
const result = runCandidateSelection(candidateSets);
debugDense("plane-route:csp", result.selection ? "complete" : "failed", result.searchStates, result.emptyDomainIndices.map((index) => candidateSets[index].planeBus.busId));
selectedCandidates = result.selection;
alternatePlaneSearchStates = result.searchStates;
if (!result.selection && useAdaptiveDensePlaneRouting) {
const expanded = result.emptyDomainIndices.map((index) => candidateSets[index].planeBus.busId).filter((id) => !orderedAlternatePlaneBuses.some((bus) => bus.busId === id));
if (expanded.length > 0) {
debugDense("plane-route:expand-domains", expanded);
return matchViaPointsAroundPlans(candidatePlans, new Set([...promotedAlternatePlaneBusIds, ...expanded]));
}
}
if (!result.selection)
for (const index of (params.planeReservationRetryCount ?? 0) === 0 ? result.conflictDomainIndices : result.emptyDomainIndices.slice(0, 1))
unroutablePlaneBusIds.add(candidateSets[index].planeBus.busId);
} else {
selectedCandidates = selectCompatiblePlaneRoutes(candidateSets, []);
}
alternatePlanePlans = selectedCandidates ? [
...feasibleAlternatePlanePlans,
...selectedCandidates.flatMap((candidate) => candidate.plans)
] : null;
} else {
alternatePlanePlans = routeAlternatePlaneBuses(orderedAlternatePlaneBuses, feasibleAlternatePlanePlans);
}
debugDense("plane-route:alternate-search", alternatePlanePlans ? "complete" : "failed", alternatePlaneSearchStates, `depth:${deepestAlternatePlaneSearchIndex}/${orderedAlternatePlaneBuses.length}`, [...alternatePlaneFailureCountByBusId].toSorted(([, first], [, second]) => second - first));
if (!alternatePlanePlans)
return null;
feasibleAlternatePlanePlans = alternatePlanePlans;
if (useAdaptiveJointPlaneSelection) {
matchedPlaneBusesInRoutingOrder = [];
return new Map([
...fixedBoundaryViaPoints,
...alternatePlanePlans.filter((plan) => plan.via).map((plan) => [plan.connectionIndex, plan.via.center])
]);
}
}
let planeBusIdsRoutedWithoutDogbones = new Set;
let allBlockingSegments = [...blockingSegments];
let alternateBlockingVias = [];
let planeBusesToMatch = [...planeBuses];
let candidateCountByConnectionIndex = new Map;
let candidatePointsByConnectionIndex = new Map;
const refreshPlaneDogboneCandidates = () => {
planeBusIdsRoutedWithoutDogbones = new Set(feasibleAlternatePlanePlans.map((plan) => plan.busId));
const alternateBlockingSegments = feasibleAlternatePlanePlans.flatMap((plan) => [...plan.segments, ...plan.planeEndpointSegments ?? []].map((segment) => ({
connectionIndex: plan.connectionIndex,
segment
})));
allBlockingSegments = [
...blockingSegments,
...alternateBlockingSegments
];
alternateBlockingVias = feasibleAlternatePlanePlans.flatMap((plan) => [
plan.via,
...plan.additionalVias ?? [],
plan.planeEndpointVia
].flatMap((via) => via ? [
{
connectionIndex: plan.connectionIndex,
center: via.center,
diameter: via.diameter,
spanLayers: via.spanLayers
}
] : []));
planeBusesToMatch = planeBuses.filter((bus) => !planeBusIdsRoutedWithoutDogbones.has(bus.busId));
candidateCountByConnectionIndex = new Map;
candidatePointsByConnectionIndex = new Map;
for (const candidate of getComponentDogboneViaSiteCandidates(planeBusesToMatch, {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
blockingSegments: allBlockingSegments,
blockingVias: alternateBlockingVias,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
})) {
candidateCountByConnectionIndex.set(candidate.connectionIndex, (candidateCountByConnectionIndex.get(candidate.connectionIndex) ?? 0) + 1);
const points = candidatePointsByConnectionIndex.get(candidate.connectionIndex) ?? [];
points.push(candidate.point);
candidatePointsByConnectionIndex.set(candidate.connectionIndex, points);
}
};
refreshPlaneDogboneCandidates();
for (let promotionPass = 0;promotionPass < planeBuses.length; promotionPass++) {
const zeroCandidatePlaneBuses = planeBusesToMatch.filter((bus) => !candidateCountByConnectionIndex.has(bus.connections[0].connectionIndex));
if (zeroCandidatePlaneBuses.length === 0)
break;
debugDense("plane-route:promote-zero-candidates", zeroCandidatePlaneBuses.map((bus) => bus.busId));
if (zeroCandidatePlaneBuses.some((bus) => matchedPlaneBuses.includes(bus))) {
return null;
}
for (const planeBus of zeroCandidatePlaneBuses) {
const targetLayer = params.busLayerAssignments[planeBus.busId];
if (!targetLayer)
return null;
const promotedPlans = routeBusAlternatives({
srj: this.routingSrj,
bus: planeBus,
targetLayer,
acceptedPlans: [
...candidatePlans,
...feasibleAlternatePlanePlans
],
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache
}, 8)[0];
if (!promotedPlans) {
debugDense("plane-route:promote-failed", planeBus.busId);
return matchViaPointsAroundPlans(candidatePlans, new Set([
...promotedAlternatePlaneBusIds,
...zeroCandidatePlaneBuses.map((bus) => bus.busId)
]));
}
feasibleAlternatePlanePlans.push(...promotedPlans);
}
refreshPlaneDogboneCandidates();
}
const debugPlaneMatchOrder = process.env.FANOUT_DEBUG_PLANE_MATCH_ORDER?.split(",") ?? [];
const incrementalPlaneBuses = planeBusesToMatch.toSorted((first, second) => {
const candidateCountDifference = (candidateCountByConnectionIndex.get(first.connections[0].connectionIndex) ?? 0) - (candidateCountByConnectionIndex.get(second.connections[0].connectionIndex) ?? 0);
if (candidateCountDifference !== 0) {
return candidateCountDifference;
}
const firstPriority = debugPlaneMatchOrder.indexOf(first.busId);
const secondPriority = debugPlaneMatchOrder.indexOf(second.busId);
const priorityDifference = (firstPriority < 0 ? debugPlaneMatchOrder.length : firstPriority) - (secondPriority < 0 ? debugPlaneMatchOrder.length : secondPriority);
if (priorityDifference !== 0)
return priorityDifference;
return first.connections[0].connectionIndex - second.connections[0].connectionIndex;
});
for (const planeBus of incrementalPlaneBuses) {
if (matchedPlaneBuses.includes(planeBus))
continue;
if (process.env.FANOUT_DEBUG_PLANE_CANDIDATES?.split(",").includes(planeBus.busId)) {
debugDense("plane-match:candidates", planeBus.busId, candidatePointsByConnectionIndex.get(planeBus.connections[0].connectionIndex));
}
const nextViaPoints = matchComponentDogboneViaSites([...matchedPlaneBuses, planeBus, ...boundaryBusesToMatch], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: incrementalViaPoints,
blockingSegments: allBlockingSegments,
blockingVias: [...blockingVias, ...alternateBlockingVias],
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
debugDense(nextViaPoints ? "plane-match:incremental" : "plane-match:incremental-failed", planeBus.busId, candidateCountByConnectionIndex.get(planeBus.connections[0].connectionIndex) ?? 0, nextViaPoints?.get(planeBus.connections[0].connectionIndex), nextViaPoints?.size ?? "failed");
if (!nextViaPoints)
return null;
incrementalViaPoints = new Map([
...incrementalViaPoints,
...nextViaPoints
]);
matchedPlaneBuses.push(planeBus);
}
matchedPlaneBusesInRoutingOrder = matchedPlaneBuses.filter((bus) => !planeBusIdsRoutedWithoutDogbones.has(bus.busId));
debugDense("plane-match:incremental-complete", incrementalViaPoints.size);
return incrementalViaPoints;
}
const planeViaPoints = matchComponentDogboneViaSites([...planeBuses, ...boundaryBusesToMatch], {
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
maximumSearchStates: 1e5,
preferredBoundaryPerpendicularSideByBusId,
preferBoundaryOutwardByBusId,
fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
blockingSegments,
blockingVias,
additionalObstacles: denseAdditionalObstacles,
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
canShareCopper
});
return planeViaPoints ? new Map([...fixedBoundaryViaPoints, ...planeViaPoints]) : null;
};
debugDense("length-match:start", matchedPlans.length);
const matchedLengthResult = matchLengthsAfterPlanes ? { plans: matchedPlans } : matchBusPlanLengths({
plans: matchedPlans,
preparedBuses: this.preparedBuses,
inputSrj: this.inputSrj,
sharedBoundary: this.getValidationBoundary(),
clearance: this.config.clearance,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
allowMatchingInsideDenseBounds: true,
candidatePlansAreFeasible: (candidatePlans) => {
const candidateViaPoints = matchViaPointsAroundPlans(candidatePlans);
if (!candidateViaPoints)
return false;
feasibleViaPoints = candidateViaPoints;
return true;
}
});
debugDense("length-match:complete", matchedLengthResult.plans?.length ?? "failed");
this.setInProgressPlans({
phase: "match-dense-boundary-lengths",
plans: matchedLengthResult.plans ?? matchedPlans,
strategy: "default",
unitIndex: ++denseWorkUnitIndex,
unitCount: denseWorkUnitCount
});
yield;
if (matchedLengthResult.plans) {
matchedPlans = matchedLengthResult.plans;
const rematchedViaPoints = feasibleViaPoints ?? matchViaPointsAroundPlans(matchedPlans);
if (rematchedViaPoints) {
fixedViaPointsByConnectionIndex = rematchedViaPoints;
matchedPlans.push(...feasibleAlternatePlanePlans);
} else {
matchedRoutingSucceeded = false;
}
} else {
matchedRoutingSucceeded = false;
}
this.setInProgressPlans({
phase: "rematch-dense-via-sites",
plans: matchedPlans,
strategy: "default",
unitIndex: denseWorkUnitIndex,
unitCount: denseWorkUnitCount
});
yield;
}
if (matchedRoutingSucceeded) {
for (const bus of matchedPlaneBusesInRoutingOrder ?? planeBuses) {
debugDense("plane-route:start", bus.busId);
const targetLayer = params.busLayerAssignments[bus.busId];
const blockingBusCounts = new Map;
const busPlans = targetLayer ? routeBus({
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans: matchedPlans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache,
blockingBusCounts,
fixedViaPointsByConnectionIndex
}) : null;
if (!busPlans) {
debugDense("plane-route:failed", bus.busId, fixedViaPointsByConnectionIndex.get(bus.connections[0].connectionIndex), [...blockingBusCounts]);
matchedRoutingSucceeded = false;
break;
}
matchedPlans.push(...busPlans);
debugDense("plane-route:complete", bus.busId);
this.setInProgressPlans({
phase: "route-dense-plane-buses",
plans: matchedPlans,
strategy: "default",
unitIndex: ++denseWorkUnitIndex,
unitCount: denseWorkUnitCount,
busId: bus.busId
});
yield;
}
}
if (matchedRoutingSucceeded && matchLengthsAfterPlanes) {
const lengthMatchingParams = {
plans: matchedPlans,
preparedBuses: this.preparedBuses,
inputSrj: this.inputSrj,
sharedBoundary: this.getValidationBoundary(),
clearance: this.config.clearance,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
allowMatchingInsideDenseBounds: true,
allowPairLaneSpreading: true
};
let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams);
const shortenedBusIds = new Set;
while (!matchedLengthResult.plans && matchedLengthResult.failedBus && !shortenedBusIds.has(matchedLengthResult.failedBus.busId)) {
const bus = matchedLengthResult.failedBus;
shortenedBusIds.add(bus.busId);
const shortened = shortenBusPlans({
plans: matchedPlans,
bus,
srj: this.inputSrj,
sharedBoundary: this.getValidationBoundary(),
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
allowSameNetMerges: this.config.allowSameNetMerges
});
if (shortened.every((plan, index) => plan === matchedPlans[index]))
break;
matchedPlans = shortened;
matchedLengthResult = matchBusPlanLengths({
...lengthMatchingParams,
plans: matchedPlans
});
}
if (matchedLengthResult.plans) {
matchedPlans = matchedLengthResult.plans;
} else {
matchedRoutingSucceeded = false;
}
this.setInProgressPlans({
phase: "match-dense-complete-lengths",
plans: matchedPlans,
strategy: "default",
unitIndex: ++denseWorkUnitIndex,
unitCount: denseWorkUnitCount
});
yield;
}
const densePlansAreClear = matchedRoutingSucceeded && fanoutPlansAreClear({
plans: matchedPlans,
srj: this.routingSrj,
sharedBoundary: boundaryBuses[0].sharedBoundary,
clearance: this.config.clearance,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges
});
debugDense("dense-validation", matchedRoutingSucceeded, matchedPlans.length, densePlansAreClear);
if (densePlansAreClear) {
return { plans: matchedPlans, failedBusIds: [] };
}
}
if (useAdaptiveDensePlaneRouting && unroutablePlaneBusIds.size > 0 && (params.planeReservationRetryCount ?? 0) < 5) {
const newlyPromoted = [...unroutablePlaneBusIds].filter((id) => !activeBoundaryReservationPlaneBuses.some((bus) => bus.busId === id));
const refinedPromotions = (params.planeReservationRetryCount ?? 0) === 0 ? refineAdaptivePlaneReservationCore({
candidateBusIds: newlyPromoted,
activeBusIds: new Set(activeBoundaryReservationPlaneBuses.map((bus) => bus.busId)),
planeBuses
}) : newlyPromoted;
if (refinedPromotions.length > 0) {
debugDense("plane-reservation-core", refinedPromotions);
debugDense("promote-reservations", refinedPromotions);
return yield* this.routeDenseThroughAllMixedTerminationSteps({
...params,
promotedPlaneReservationBusIds: [
...params.promotedPlaneReservationBusIds ?? [],
...refinedPromotions
],
planeReservationRetryCount: (params.planeReservationRetryCount ?? 0) + 1
});
}
}
if (matchLengthsAfterPlanes) {
return yield* this.routeDenseThroughAllMixedTerminationSteps({
...params,
lengthMatchingStage: "before-planes"
});
}
if (usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting) {
const boundaryAlignedState = yield* this.routeDenseThroughAllMixedTerminationSteps({
...params,
denseRoutingStrategy: "boundary-aligned"
});
if (boundaryAlignedState)
return boundaryAlignedState;
}
if (!usePadAlignedDenseRouting || process.env.FANOUT_DEBUG_DENSE_ONLY === "1")
return null;
const maximumStates = 8;
const getBoundaryStates = (alternativesPerBoundaryBus, initialPlans = []) => {
let states = [
{ plans: [...initialPlans], failedBusIds: [] }
];
for (const bus of boundaryBuses) {
const targetLayer = params.busLayerAssignments[bus.busId];
if (!targetLayer)
return null;
const nextStates = [];
const alternativesByState = states.map((state) => ({
state,
alternatives: routeBusAlternatives({
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans: state.plans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache
}, alternativesPerBoundaryBus)
}));
for (let alternativeIndex = 0;alternativeIndex < alternativesPerBoundaryBus; alternativeIndex++) {
for (const { state, alternatives } of alternativesByState) {
const alternative = alternatives[alternativeIndex];
if (!alternative)
continue;
nextStates.push({
plans: [...state.plans, ...alternative],
failedBusIds: []
});
if (nextStates.length >= maximumStates)
break;
}
if (nextStates.length >= maximumStates)
break;
}
if (nextStates.length === 0)
return null;
states = nextStates;
}
return states;
};
const getJointReservedBoundaryState = (initialPlans) => {
if (boundaryBuses.length !== 2)
return null;
const [firstBus, secondBus] = boundaryBuses;
if (!firstBus || !secondBus)
return null;
const routeBoundaryBus = (bus, acceptedPlans, rejectedViaMinimalCandidates) => {
const targetLayer = params.busLayerAssignments[bus.busId];
if (!targetLayer)
return null;
return routeBusAlternatives({
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache,
rejectedViaMinimalCandidates,
stopAfterFirstRejectedViaMinimalCandidate: rejectedViaMinimalCandidates !== undefined
}, 1)[0] ?? null;
};
const firstPlans = routeBoundaryBus(firstBus, [...initialPlans]);
if (!firstPlans)
return null;
const rejectedSecondCandidates = [];
const secondPlans = routeBoundaryBus(secondBus, [...initialPlans, ...firstPlans], rejectedSecondCandidates);
if (secondPlans) {
return {
plans: [...initialPlans, ...firstPlans, ...secondPlans],
failedBusIds: []
};
}
const rejectedSecondPlans = rejectedSecondCandidates[0];
if (!rejectedSecondPlans)
return null;
const reservedSecondPlans = rejectedSecondPlans.map((plan) => ({
...plan,
termination: {
type: "plane",
layer: plan.targetLayer
}
}));
const reroutedFirstPlans = routeBoundaryBus(firstBus, [
...initialPlans,
...reservedSecondPlans
]);
if (!reroutedFirstPlans)
return null;
const combinedPlans = [
...initialPlans,
...reroutedFirstPlans,
...rejectedSecondPlans
];
if (!fanoutPlansAreClear({
plans: combinedPlans,
srj: this.routingSrj,
sharedBoundary: firstBus.sharedBoundary,
clearance: this.config.clearance,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges
})) {
return null;
}
return { plans: combinedPlans, failedBusIds: [] };
};
const planeBusById = new Map(planeBuses.map((bus) => [bus.busId, bus]));
const routePlaneBus = (bus, acceptedPlans, blockingBusCounts) => {
const targetLayer = params.busLayerAssignments[bus.busId];
if (!targetLayer)
return null;
return routeBus({
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: false,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache,
blockingBusCounts
});
};
const routePlaneOrder = (boundaryPlans, planeOrder) => {
const state = {
plans: [...boundaryPlans],
failedBusIds: []
};
for (const bus of planeOrder) {
const blockingBusCounts = new Map;
let busPlans = routePlaneBus(bus, state.plans, blockingBusCounts);
if (!busPlans) {
const blockerIds = [...blockingBusCounts.entries()].filter(([busId]) => state.plans.some((plan) => plan.busId === busId)).filter(([busId]) => planeBusById.has(busId)).toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount).slice(0, 8).map(([busId]) => busId);
const ripupSets = [
...blockerIds.map((busId) => [busId]),
...blockerIds.flatMap((first, firstIndex) => blockerIds.slice(firstIndex + 1).map((second) => [first, second]))
];
for (const ripupIds of ripupSets) {
const ripupIdSet = new Set(ripupIds);
const candidatePlans = state.plans.filter((plan) => !ripupIdSet.has(plan.busId));
const constrainedPlans = routePlaneBus(bus, candidatePlans);
if (!constrainedPlans)
continue;
candidatePlans.push(...constrainedPlans);
let repairSucceeded = true;
for (const blockerId of ripupIds) {
const blockerBus = planeBusById.get(blockerId);
const replacementPlans = blockerBus ? routePlaneBus(blockerBus, candidatePlans) : null;
if (!replacementPlans) {
repairSucceeded = false;
break;
}
candidatePlans.push(...replacementPlans);
}
if (!repairSucceeded)
continue;
state.plans = candidatePlans;
busPlans = [];
break;
}
}
if (busPlans)
state.plans.push(...busPlans);
else
state.failedBusIds.push(bus.busId);
}
return state;
};
let bestState = null;
let mostRecentBoundaryBestState = null;
const evaluateBoundaryStates = (states, initialPlaneOrder) => {
let localBestState = null;
for (const boundaryState of states) {
let planeOrder = [...initialPlaneOrder];
const seenPlaneOrders = new Set;
for (let retryIndex = 0;retryIndex < 3; retryIndex++) {
const orderKey = planeOrder.map((bus) => bus.busId).join("\x00");
if (seenPlaneOrders.has(orderKey))
break;
seenPlaneOrders.add(orderKey);
const state = routePlaneOrder(boundaryState.plans, planeOrder);
if (!bestState || state.plans.length > bestState.plans.length || state.plans.length === bestState.plans.length && state.failedBusIds.length < bestState.failedBusIds.length) {
bestState = state;
}
if (!localBestState || state.plans.length > localBestState.plans.length || state.plans.length === localBestState.plans.length && state.failedBusIds.length < localBestState.failedBusIds.length) {
localBestState = state;
}
if (state.failedBusIds.length === 0)
return state;
const failedBusIds = new Set(state.failedBusIds);
planeOrder = [
...state.failedBusIds.flatMap((busId) => {
const bus = planeBusById.get(busId);
return bus ? [bus] : [];
}),
...planeOrder.filter((bus) => !failedBusIds.has(bus.busId))
];
}
}
mostRecentBoundaryBestState = localBestState;
return null;
};
for (const alternativesPerBoundaryBus of [1, 4]) {
const states = getBoundaryStates(alternativesPerBoundaryBus);
if (!states)
continue;
const completeState = evaluateBoundaryStates(states, planeBuses);
if (completeState)
return completeState;
const seedFailureIds = bestState?.failedBusIds.slice(0, 3);
if (alternativesPerBoundaryBus === 1 && seedFailureIds) {
for (const failedBusId of seedFailureIds) {
const seededPlanePlans = [];
const seededPlaneBusIds = new Set;
let nextFailedBusId = failedBusId;
for (let seedDepth = 0;seedDepth < 3 && nextFailedBusId; seedDepth++) {
const failedPlaneBus = planeBusById.get(nextFailedBusId);
if (!failedPlaneBus)
break;
const nextSeedPlans = routePlaneBus(failedPlaneBus, seededPlanePlans);
if (!nextSeedPlans)
break;
seededPlanePlans.push(...nextSeedPlans);
seededPlaneBusIds.add(nextFailedBusId);
const jointReservedBoundaryState = getJointReservedBoundaryState(seededPlanePlans);
if (!jointReservedBoundaryState)
break;
const jointCompleteState = evaluateBoundaryStates([jointReservedBoundaryState], planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)));
if (jointCompleteState)
return jointCompleteState;
const recentFailedBusIds = mostRecentBoundaryBestState?.failedBusIds;
nextFailedBusId = recentFailedBusIds?.find((busId) => !seededPlaneBusIds.has(busId));
}
if (seededPlanePlans.length === 0)
continue;
const seededBoundaryStates = getBoundaryStates(1, seededPlanePlans);
if (!seededBoundaryStates)
continue;
const seededCompleteState = evaluateBoundaryStates(seededBoundaryStates, planeBuses.filter((bus) => !seededPlaneBusIds.has(bus.busId)));
if (seededCompleteState)
return seededCompleteState;
}
}
}
return bestState;
}
*evaluateAssignmentWithStrategySteps(assignmentIndex, busLayerAssignments, routingStrategy) {
let plans = [];
let failedBusIds = [];
let blockingBusCounts = new Map;
const isSingleLayerFanout = this.config.escapeLayers.length === 1;
const useSingleLayerPushAndShove = isSingleLayerFanout && this.config.singleLayerPushAndShove && !this.preparedBuses.some((bus) => bus.exitEdge && bus.preferredExit?.includes("-"));
if (useSingleLayerPushAndShove) {
const singleLayerParams = {
srj: this.routingSrj,
buses: this.preparedBuses,
traceWidth: this.config.traceWidth,
clearance: this.config.clearance,
borderDistribution: this.config.borderDistribution
};
let singleLayerPlans = routeSingleLayerWithPushAndShove(singleLayerParams);
if (!singleLayerPlans && this.config.singleLayerAdaptiveExits) {
this.setInProgressPlans({
phase: "prepare-single-layer-adaptive-exits",
plans,
strategy: routingStrategy
});
yield;
this.setInProgressPlans({
phase: "route-single-layer-adaptive-exits",
plans,
strategy: routingStrategy
});
this.activeAdaptiveVisualization = null;
const adaptiveSolver = this.createWorkSolver("SingleLayerAdaptiveExitSolver", routeSingleLayerWithAdaptiveExitsSteps({
...singleLayerParams,
availableBoundaryRegions: resolveAvailableBoundaryRegions(this.options.availableCornersAndSides),
onProgress: (visualization, adaptiveStats) => {
this.activeAdaptiveVisualization = visualization;
this.stats = { ...this.stats, ...adaptiveStats };
}
}), undefined, () => this.visualizeAdaptiveRoutingState());
singleLayerPlans = yield {
type: "subsolver",
solver: adaptiveSolver
};
}
if (singleLayerPlans) {
plans.push(...singleLayerPlans);
} else {
failedBusIds.push(...this.preparedBuses.map((bus) => bus.busId));
}
this.setInProgressPlans({
phase: "route-single-layer",
plans,
strategy: routingStrategy,
unitIndex: 1,
unitCount: 1
});
yield;
}
const busesInRoutingOrder = [...this.preparedBuses].sort((a, b) => {
const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a);
const bUsesCoordinatedWinding = busUsesCoordinatedWinding(b);
const aLayerIndex = this.config.layerNames.indexOf(busLayerAssignments[a.busId] ?? "");
const bLayerIndex = this.config.layerNames.indexOf(busLayerAssignments[b.busId] ?? "");
return comparePlaneRoutingPriority(a, b, this.config.allowBlindAndBuriedVias) || Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) || (aUsesCoordinatedWinding && bUsesCoordinatedWinding ? bLayerIndex - aLayerIndex : 0) || (routingStrategy === "group-by-layer" ? (busLayerAssignments[a.busId] ?? "").localeCompare(busLayerAssignments[b.busId] ?? "") : 0) || b.componentObstacles.length - a.componentObstacles.length || (isSingleLayerFanout ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a) : b.connections.length - a.connections.length || (routingStrategy === "deep-first" ? getBusDistanceToBoundary(b) - getBusDistanceToBoundary(a) : getBusDistanceToBoundary(a) - getBusDistanceToBoundary(b)));
});
let mixedTerminationState = null;
if (!useSingleLayerPushAndShove && routingStrategy === "default") {
const denseSolver = this.createWorkSolver("DenseMixedTerminationSolver", this.routeDenseThroughAllMixedTerminationSteps({
busLayerAssignments,
busesInRoutingOrder
}), () => {
const workUnit = Number(this.stats.workUnit ?? 0);
const workUnitCount = Number(this.stats.workUnitCount ?? 0);
return workUnitCount > 0 ? workUnit / workUnitCount : 0;
});
mixedTerminationState = yield {
type: "subsolver",
solver: denseSolver
};
}
if (!mixedTerminationState && !useSingleLayerPushAndShove && routingStrategy === "default" && process.env.FANOUT_DEBUG_DENSE_ONLY === "1") {
mixedTerminationState = {
plans: [],
failedBusIds: this.preparedBuses.map((bus) => bus.busId)
};
}
if (mixedTerminationState) {
plans = mixedTerminationState.plans;
failedBusIds = mixedTerminationState.failedBusIds;
this.setInProgressPlans({
phase: "route-dense-mixed-terminations",
plans,
strategy: routingStrategy,
unitIndex: this.preparedBuses.length,
unitCount: this.preparedBuses.length
});
yield;
}
let routingPrefixKey = `${routingStrategy}|`;
let routedBusIndex = 0;
for (const bus of useSingleLayerPushAndShove || mixedTerminationState ? [] : busesInRoutingOrder) {
routedBusIndex++;
const targetLayer = busLayerAssignments[bus.busId];
if (!targetLayer) {
throw new Error(`FanoutSolver: assignment ${assignmentIndex} has no layer for bus "${bus.busId}"`);
}
routingPrefixKey += `${bus.busId.length}:${bus.busId};${targetLayer.length}:${targetLayer};`;
const cachedPrefix = this.routingPrefixCache.get(routingPrefixKey);
if (cachedPrefix) {
plans = [...cachedPrefix.plans];
failedBusIds = [...cachedPrefix.failedBusIds];
blockingBusCounts = new Map(cachedPrefix.blockingBusCounts);
this.setInProgressPlans({
phase: "route-assignment",
plans,
strategy: routingStrategy,
unitIndex: routedBusIndex,
unitCount: busesInRoutingOrder.length,
busId: bus.busId
});
yield;
continue;
}
const currentBusBlockingCounts = new Map;
const busPlans = routeBus({
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans: plans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache,
blockingBusCounts: currentBusBlockingCounts
});
if (!busPlans) {
failedBusIds.push(bus.busId);
for (const [blockingBusId, count] of currentBusBlockingCounts) {
blockingBusCounts.set(blockingBusId, (blockingBusCounts.get(blockingBusId) ?? 0) + count);
}
} else {
plans.push(...busPlans);
}
this.routingPrefixCache.set(routingPrefixKey, {
plans: [...plans],
failedBusIds: [...failedBusIds],
blockingBusCounts: new Map(blockingBusCounts)
});
this.setInProgressPlans({
phase: "route-assignment",
plans,
strategy: routingStrategy,
unitIndex: routedBusIndex,
unitCount: busesInRoutingOrder.length,
busId: bus.busId
});
yield;
}
let validationIssues;
if (plans.length === this.inputSrj.connections.length) {
const lengthMatching = this.matchCompletePlanLengths(plans);
if (lengthMatching.plans) {
plans = lengthMatching.plans;
} else {
const constrainedBus = lengthMatching.failedBus;
const lengthMatchingIssue = {
code: "bus-length-skew",
message: `Bus ${constrainedBus.busId} could not satisfy its ${constrainedBus.maxLengthSkew.toFixed(6)}mm routed-length skew within the fanout boundary`,
busId: constrainedBus.busId
};
validationIssues = [lengthMatchingIssue];
this.lengthMatchingFailure ??= lengthMatchingIssue;
plans = [];
failedBusIds = [
constrainedBus.busId,
...this.preparedBuses.map((bus) => bus.busId).filter((busId) => busId !== constrainedBus.busId)
];
blockingBusCounts.clear();
}
}
let outputSrj = buildOutputSimpleRouteJson({
inputSrj: this.inputSrj,
plans,
layerNames: this.config.layerNames
});
const validation = plans.length === this.inputSrj.connections.length ? this.validateCompletePlans(plans, outputSrj) : null;
if (validation && !validation.valid) {
validationIssues = validation.issues;
plans = [];
failedBusIds = this.preparedBuses.map((bus) => bus.busId);
blockingBusCounts.clear();
outputSrj = buildOutputSimpleRouteJson({
inputSrj: this.inputSrj,
plans,
layerNames: this.config.layerNames
});
}
const routedBusCount = this.preparedBuses.length - failedBusIds.length;
const routeLength = plans.reduce((total, plan) => total + plan.length, 0);
const unroutedConnectionCount = this.inputSrj.connections.length - plans.length;
const score = unroutedConnectionCount * 1e6 + failedBusIds.length * 1e5 + routeLength + getPlanViaCount(plans) * 0.1 + assignmentLoadPenalty(busLayerAssignments, this.preparedBuses, this.config.balanceLayerLoadByConnectionCount) * getLayerLoadPenaltyWeight(this.config);
const summary = {
assignmentIndex,
busLayerAssignments,
routedBusCount,
routedConnectionCount: plans.length,
failedBusIds,
score,
...validationIssues ? { validationIssues } : {}
};
this.setInProgressPlans({
phase: "finalize-assignment-strategy",
plans,
strategy: routingStrategy
});
return {
summary,
plans,
blockingBusIds: [...blockingBusCounts.entries()].toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount).map(([busId]) => busId),
outputSrj
};
}
*evaluateAssignmentSteps(assignmentIndex, busLayerAssignments) {
let bestAttempt = yield* this.evaluateAssignmentWithStrategySteps(assignmentIndex, busLayerAssignments, "default");
if (process.env.FANOUT_DEBUG_DENSE_ONLY === "1")
return bestAttempt;
if (bestAttempt.summary.routedConnectionCount === this.inputSrj.connections.length && this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0) {
return bestAttempt;
}
for (const routingStrategy of ["group-by-layer", "deep-first"]) {
const attempt = yield* this.evaluateAssignmentWithStrategySteps(assignmentIndex, busLayerAssignments, routingStrategy);
if (this.isAttemptBetter(attempt, bestAttempt)) {
bestAttempt = attempt;
}
if (bestAttempt.summary.routedConnectionCount === this.inputSrj.connections.length && this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0) {
return bestAttempt;
}
}
return bestAttempt;
}
*evaluateGroupedBeamSteps(assignmentIndex, groupByDirection = false) {
if (this.config.escapeLayers.length < 2)
return null;
if (this.preparedBuses.length > 56)
return null;
const totalConnections = this.inputSrj.connections.length;
if (totalConnections > 64)
return null;
if (new Set(this.preparedBuses.map((bus) => bus.componentId)).size !== 1) {
return null;
}
const getMaximumViaSpan = (bus) => {
const sourceLayerIndex = this.config.layerNames.indexOf(bus.connections[0]?.sourceLayer ?? "");
const candidateLayers = bus.termination.type === "plane" ? [bus.termination.layer] : this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers;
return Math.max(0, ...candidateLayers.map((layer) => Math.abs(this.config.layerNames.indexOf(layer) - sourceLayerIndex)));
};
const busesInSearchOrder = [...this.preparedBuses].sort((a, b) => {
const aUsesCoordinatedWinding = busUsesCoordinatedWinding(a);
const bUsesCoordinatedWinding = busUsesCoordinatedWinding(b);
const aLayerCount = a.termination.type === "plane" ? 1 : this.escapeLayersByBusId[a.busId]?.length ?? this.config.escapeLayers.length;
const bLayerCount = b.termination.type === "plane" ? 1 : this.escapeLayersByBusId[b.busId]?.length ?? this.config.escapeLayers.length;
return comparePlaneRoutingPriority(a, b, this.config.allowBlindAndBuriedVias) || Number(bUsesCoordinatedWinding) - Number(aUsesCoordinatedWinding) || (aUsesCoordinatedWinding && bUsesCoordinatedWinding ? getMaximumViaSpan(b) - getMaximumViaSpan(a) : 0) || (groupByDirection ? a.direction.localeCompare(b.direction) : 0) || aLayerCount - bLayerCount || b.componentObstacles.length - a.componentObstacles.length || b.connections.length - a.connections.length || getBusDepthInRows(b) - getBusDepthInRows(a) || a.busId.localeCompare(b.busId);
});
const isSmallProblem = totalConnections <= 24;
const hasMultiConnectionBus = this.preparedBuses.some((bus) => bus.connections.length > 1);
const beamWidth = isSmallProblem ? 48 : totalConnections <= 32 ? 24 : 12;
const alternativesPerLayer = isSmallProblem && !hasMultiConnectionBus ? 4 : 1;
let states = [{ assignment: {}, plans: [] }];
const getStateScore = (state) => {
const routeLength = state.plans.reduce((total, plan) => total + plan.length, 0);
const viaCount = getPlanViaCount(state.plans);
const offEndpointLayerConnectionCount = this.preparedBuses.reduce((count, bus) => {
if (bus.termination.type !== "boundary" || !busUsesDestinationGuidedTracks(bus)) {
return count;
}
const sourceLayer = bus.connections[0]?.sourceLayer;
const preferredLayer = getCommonExplicitExitTargetLayer(bus) ?? sourceLayer;
return state.assignment[bus.busId] === preferredLayer ? count : count + bus.connections.length;
}, 0);
return routeLength + viaCount * 0.1 + offEndpointLayerConnectionCount * 1e4 + assignmentLoadPenalty(state.assignment, this.preparedBuses, this.config.balanceLayerLoadByConnectionCount) * getLayerLoadPenaltyWeight(this.config);
};
let searchedBusIndex = 0;
for (const bus of busesInSearchOrder) {
searchedBusIndex++;
const nextStates = [];
for (const state of states) {
const candidateLayers = bus.termination.type === "plane" ? [bus.termination.layer] : this.escapeLayersByBusId[bus.busId] ?? this.config.escapeLayers;
const layerLoads = new Map;
for (const [assignedBusId, layer] of Object.entries(state.assignment)) {
const assignedBus = this.preparedBuses.find((candidate) => candidate.busId === assignedBusId);
layerLoads.set(layer, (layerLoads.get(layer) ?? 0) + (this.config.balanceLayerLoadByConnectionCount ? assignedBus?.connections.length ?? 1 : 1));
}
const sourceLayer = bus.connections[0]?.sourceLayer;
const commonExitTargetLayer = getCommonExplicitExitTargetLayer(bus);
const preferSourceLayer = busUsesDestinationGuidedTracks(bus);
const orderedLayers = candidateLayers.toSorted((first, second) => (layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) || Number(second === commonExitTargetLayer) - Number(first === commonExitTargetLayer) || (preferSourceLayer ? Number(second === sourceLayer) - Number(first === sourceLayer) : Number(first === sourceLayer) - Number(second === sourceLayer)) || first.localeCompare(second));
for (const targetLayer of orderedLayers) {
const busAlternatives = routeBusAlternatives({
srj: this.routingSrj,
bus,
targetLayer,
acceptedPlans: state.plans,
layerNames: this.config.layerNames,
traceWidth: this.config.traceWidth,
viaDiameter: this.config.viaDiameter,
viaHoleDiameter: this.config.viaHoleDiameter,
clearance: this.config.clearance,
compactBusTracks: this.config.compactBusTracks,
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias,
allowSameNetMerges: this.config.allowSameNetMerges,
staticClearanceCache: this.routeStaticClearanceCache
}, alternativesPerLayer);
for (const busPlans of busAlternatives) {
nextStates.push({
assignment: {
...state.assignment,
[bus.busId]: targetLayer
},
plans: [...state.plans, ...busPlans]
});
}
}
}
if (nextStates.length === 0)
return null;
nextStates.sort((first, second) => {
const additionalViaDifference = this.getCoordinatedAdditionalViaCount(first.plans) - this.getCoordinatedAdditionalViaCount(second.plans);
if (additionalViaDifference !== 0)
return additionalViaDifference;
const scoreDifference = getStateScore(first) - getStateScore(second);
if (Math.abs(scoreDifference) > 0.000000001)
return scoreDifference;
return JSON.stringify(first.assignment).localeCompare(JSON.stringify(second.assignment));
});
const statesByAssignment = new Map;
states = [];
for (const state of nextStates) {
const key = JSON.stringify(state.assignment);
const sameAssignmentCount = statesByAssignment.get(key) ?? 0;
if (sameAssignmentCount >= 2)
continue;
statesByAssignment.set(key, sameAssignmentCount + 1);
states.push(state);
if (states.length >= beamWidth)
break;
}
this.setInProgressPlans({
phase: "route-grouped-beam",
plans: states[0]?.plans ?? [],
strategy: "grouped-beam",
unitIndex: searchedBusIndex,
unitCount: busesInSearchOrder.length,
busId: bus.busId
});
yield;
}
let bestState;
let outputSrj;
let bestMatchedScore = Number.POSITIVE_INFINITY;
let bestAdditionalViaCount = Number.POSITIVE_INFINITY;
const getCompleteStateScore = (state) => state.plans.reduce((total, plan) => total + plan.length, 0) + getPlanViaCount(state.plans) * 0.1 + assignmentLoadPenalty(state.assignment, this.preparedBuses, this.config.balanceLayerLoadByConnectionCount) * getLayerLoadPenaltyWeight(this.config);
const hasLengthConstraints = this.preparedBuses.some((bus) => bus.maxLengthSkew !== undefined);
for (const state of states) {
if (state.plans.length !== this.inputSrj.connections.length) {
yield;
continue;
}
const lengthMatching = this.matchCompletePlanLengths(state.plans);
if (!lengthMatching.plans) {
yield;
continue;
}
const lengthMatchedPlans = lengthMatching.plans;
const candidateOutput = buildOutputSimpleRouteJson({
inputSrj: this.inputSrj,
plans: lengthMatchedPlans,
layerNames: this.config.layerNames
});
if (!this.validateCompletePlans(lengthMatchedPlans, candidateOutput).valid) {
yield;
continue;
}
const candidateState = { ...state, plans: lengthMatchedPlans };
const candidateAdditionalViaCount = this.getCoordinatedAdditionalViaCount(lengthMatchedPlans);
const candidateScore = getCompleteStateScore(candidateState);
if (!bestState || candidateAdditionalViaCount < bestAdditionalViaCount || candidateAdditionalViaCount === bestAdditionalViaCount && candidateScore < bestMatchedScore) {
bestState = candidateState;
outputSrj = candidateOutput;
bestMatchedScore = candidateScore;
bestAdditionalViaCount = candidateAdditionalViaCount;
}
if (!hasLengthConstraints)
break;
this.setInProgressPlans({
phase: "validate-grouped-beam",
plans: lengthMatchedPlans,
strategy: "grouped-beam"
});
yield;
}
if (!bestState || !outputSrj)
return null;
const score = bestMatchedScore;
if (!Number.isFinite(score))
return null;
const summary = {
assignmentIndex,
busLayerAssignments: bestState.assignment,
routedBusCount: this.preparedBuses.length,
routedConnectionCount: bestState.plans.length,
failedBusIds: [],
score
};
return {
summary,
plans: bestState.plans,
blockingBusIds: [],
outputSrj
};
}
*evaluateGroupedBeamAlternativesSteps(assignmentIndex) {
const primaryAttempt = yield* this.evaluateGroupedBeamSteps(assignmentIndex);
if (primaryAttempt)
return primaryAttempt;
return yield* this.evaluateGroupedBeamSteps(assignmentIndex, true);
}
prioritizeFailedBusRepairs(assignment, failedBusIds, blockingBusIds) {
const assignmentKey = JSON.stringify(assignment);
const repairDepth = this.assignmentRepairDepthByKey.get(assignmentKey) ?? 0;
if (repairDepth >= 2)
return;
const maximumRepairs = 8;
const repairs = [];
const repairKeys = new Set;
const addRepair = (repair) => {
const key = JSON.stringify(repair);
if (repairKeys.has(key) || this.evaluatedAssignmentKeys.has(key) || this.queuedAssignmentKeys.has(key)) {
return;
}
repairKeys.add(key);
this.queuedAssignmentKeys.add(key);
this.assignmentRepairDepthByKey.set(key, repairDepth + 1);
repairs.push(repair);
};
const repairBusIds = [];
for (let index = 0;index < Math.max(failedBusIds.length, blockingBusIds.length); index++) {
const failedBusId = failedBusIds[index];
const blockingBusId = blockingBusIds[index];
if (failedBusId && !repairBusIds.includes(failedBusId)) {
repairBusIds.push(failedBusId);
}
if (blockingBusId && !repairBusIds.includes(blockingBusId)) {
repairBusIds.push(blockingBusId);
}
}
for (const failedBusId of failedBusIds) {
const failedLayer = assignment[failedBusId];
const failedCandidateLayers = this.escapeLayersByBusId[failedBusId];
if (!failedLayer || !failedCandidateLayers)
continue;
for (const blockingBusId of blockingBusIds.slice(0, 4)) {
const blockingLayer = assignment[blockingBusId];
const blockingCandidateLayers = this.escapeLayersByBusId[blockingBusId];
if (!blockingLayer || !blockingCandidateLayers || !failedCandidateLayers.includes(blockingLayer) || !blockingCandidateLayers.includes(failedLayer)) {
continue;
}
addRepair({
...assignment,
[failedBusId]: blockingLayer,
[blockingBusId]: failedLayer
});
if (repairs.length >= maximumRepairs)
break;
}
if (repairs.length >= maximumRepairs)
break;
}
for (const busId of repairBusIds) {
const currentLayer = assignment[busId];
const candidateLayers = this.escapeLayersByBusId[busId];
if (!currentLayer || !candidateLayers)
continue;
const currentLayerIndex = candidateLayers.indexOf(currentLayer);
for (let shift = 1;shift < candidateLayers.length; shift++) {
const candidateLayer = candidateLayers[(Math.max(currentLayerIndex, 0) + shift) % candidateLayers.length];
if (candidateLayer === currentLayer)
continue;
addRepair({ ...assignment, [busId]: candidateLayer });
if (repairs.length >= maximumRepairs)
break;
}
if (repairs.length >= maximumRepairs)
break;
}
this.pendingRepairAssignments.push(...repairs);
}
hasCompleteBestAttempt() {
return this.bestAttempt?.summary.routedConnectionCount === this.inputSrj.connections.length;
}
getCoordinatedAdditionalViaCount(plans) {
const coordinatedBusIds = new Set(this.preparedBuses.filter(busUsesCoordinatedWinding).map((bus) => bus.busId));
return plans.reduce((count, plan) => count + (coordinatedBusIds.has(plan.busId) ? plan.additionalVias?.length ?? 0 : 0), 0);
}
isAttemptBetter(candidate, current) {
if (candidate.summary.routedConnectionCount !== current.summary.routedConnectionCount) {
return candidate.summary.routedConnectionCount > current.summary.routedConnectionCount;
}
if (candidate.summary.routedBusCount !== current.summary.routedBusCount) {
return candidate.summary.routedBusCount > current.summary.routedBusCount;
}
const candidateAdditionalVias = this.getCoordinatedAdditionalViaCount(candidate.plans);
const currentAdditionalVias = this.getCoordinatedAdditionalViaCount(current.plans);
if (candidateAdditionalVias !== currentAdditionalVias) {
return candidateAdditionalVias < currentAdditionalVias;
}
return candidate.summary.score < current.summary.score;
}
hasGloballyViaMinimalBestAttempt() {
if (!this.hasCompleteBestAttempt() || !this.bestAttempt)
return false;
if (this.preparedBuses.length === 0 || !this.preparedBuses.every(busUsesCoordinatedWinding)) {
return false;
}
return this.bestAttempt.plans.every((plan) => plan.via !== undefined && (plan.additionalVias?.length ?? 0) === 0);
}
shouldEvaluateGroupedBeam() {
if (this.groupedBeamEvaluated || this.nextAssignmentIndex === 0) {
return false;
}
const targetedRepairSearchFinished = this.hasCompleteBestAttempt() || this.pendingRepairAssignments.length === 0 || this.nextAssignmentIndex >= this.config.maxLayerCombinations;
return targetedRepairSearchFinished;
}
commitGroupedBeamAttempt(beamAttempt) {
if (!beamAttempt) {
if (this.hasCompleteBestAttempt() && this.bestAttempt && this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0) {
this.completeBestAttemptEndpoints();
this.solved = true;
}
return;
}
this.attempts.push(beamAttempt.summary);
if (!this.bestAttempt || this.isAttemptBetter(beamAttempt, this.bestAttempt)) {
this.bestAttempt = beamAttempt;
}
const bestSummary = this.bestAttempt.summary;
this.stats = {
phase: "complete-grouped-beam",
assignment: bestSummary.assignmentIndex < 0 ? 0 : bestSummary.assignmentIndex + 1,
assignmentCount: this.config.maxLayerCombinations,
routedBuses: `${bestSummary.routedBusCount}/${this.preparedBuses.length}`,
routedConnections: `${bestSummary.routedConnectionCount}/${this.inputSrj.connections.length}`,
failedBuses: "none",
bestScore: bestSummary.score
};
if (this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0) {
this.completeBestAttemptEndpoints();
this.solved = true;
}
}
commitAssignmentAttempt(assignment, attempt) {
this.nextAssignmentIndex++;
this.evaluatedAssignmentKeys.add(JSON.stringify(assignment));
if (!this.bestAttempt || attempt.summary.routedConnectionCount >= this.bestAttempt.summary.routedConnectionCount) {
this.prioritizeFailedBusRepairs(assignment, attempt.summary.failedBusIds, attempt.blockingBusIds);
}
this.attempts.push(attempt.summary);
if (!this.bestAttempt || this.isAttemptBetter(attempt, this.bestAttempt)) {
this.bestAttempt = attempt;
}
this.stats = {
phase: "complete-assignment",
assignment: attempt.summary.assignmentIndex + 1,
assignmentCount: this.config.maxLayerCombinations,
routedBuses: `${attempt.summary.routedBusCount}/${this.preparedBuses.length}`,
routedConnections: `${attempt.summary.routedConnectionCount}/${this.inputSrj.connections.length}`,
failedBuses: attempt.summary.failedBusIds.join(", ") || "none",
bestScore: this.bestAttempt.summary.score
};
if (this.groupedBeamEvaluated && attempt.summary.routedConnectionCount === this.inputSrj.connections.length && this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0) {
this.completeBestAttemptEndpoints();
this.solved = true;
}
}
getNextAssignment() {
let assignment;
while (!assignment && this.nextAssignmentIndex < this.config.maxLayerCombinations) {
const preferGeneratedAssignment = this.nextAssignmentIndex % 3 === 0;
let candidate;
let candidateCameFromRepairQueue = false;
if (!this.groupedBeamEvaluated && this.nextAssignmentIndex > 0) {
candidate = this.pendingRepairAssignments.pop();
candidateCameFromRepairQueue = candidate !== undefined;
} else if (preferGeneratedAssignment) {
candidate = this.layerAssignments[this.nextGeneratedAssignmentIndex++];
} else {
candidate = this.pendingRepairAssignments.pop();
candidateCameFromRepairQueue = candidate !== undefined;
}
if (!candidate && (this.groupedBeamEvaluated || this.nextAssignmentIndex === 0)) {
candidate = preferGeneratedAssignment ? this.pendingRepairAssignments.pop() : this.layerAssignments[this.nextGeneratedAssignmentIndex++];
candidateCameFromRepairQueue = preferGeneratedAssignment && candidate !== undefined;
}
if (!candidate)
break;
const candidateKey = JSON.stringify(candidate);
if (candidateCameFromRepairQueue) {
this.queuedAssignmentKeys.delete(candidateKey);
}
if (this.evaluatedAssignmentKeys.has(candidateKey))
continue;
assignment = candidate;
}
return assignment;
}
finishWithoutAnotherAssignment() {
if (this.hasCompleteBestAttempt()) {
this.completeBestAttemptEndpoints();
this.solved = true;
return;
}
this.failed = true;
const validationMessage = this.lengthMatchingFailure?.message ?? this.bestAttempt?.summary.validationIssues?.[0]?.message;
this.error = validationMessage ? `FanoutSolver: ${validationMessage}` : this.bestAttempt ? `FanoutSolver: best layer assignment routed ${this.bestAttempt.summary.routedConnectionCount}/${this.inputSrj.connections.length} connections` : "FanoutSolver: no layer assignment could be evaluated";
}
_step() {
if (this.activeOperation) {
this.stepActiveOperation();
return;
}
if (!this.routingInitialized) {
this.startOperation({
name: "FanoutCandidateLayerSolver",
generator: this.initializeRoutingSteps(),
onSolved: () => {},
getProgress: () => this.nextCandidateLayerBusIndex / Math.max(1, this.boundaryBuses.length + 1)
});
return;
}
if (this.nextAssignmentIndex > 0 && this.hasGloballyViaMinimalBestAttempt()) {
this.completeBestAttemptEndpoints();
this.solved = true;
return;
}
if (this.shouldEvaluateGroupedBeam()) {
this.groupedBeamEvaluated = true;
this.startOperation({
name: "FanoutGroupedBeamSolver",
generator: this.evaluateGroupedBeamAlternativesSteps(-1),
onSolved: (attempt) => this.commitGroupedBeamAttempt(attempt),
getProgress: () => {
const workUnit = Number(this.stats.workUnit ?? 0);
const workUnitCount = Number(this.stats.workUnitCount ?? 0);
return workUnitCount > 0 ? workUnit / workUnitCount : 0;
}
});
this.stats = { ...this.stats, phase: "prepare-grouped-beam" };
return;
}
const assignment = this.getNextAssignment();
if (!assignment && !this.groupedBeamEvaluated)
return;
if (!assignment) {
this.finishWithoutAnotherAssignment();
return;
}
this.startOperation({
name: "FanoutAssignmentSolver",
generator: this.evaluateAssignmentSteps(this.nextAssignmentIndex, assignment),
onSolved: (attempt) => this.commitAssignmentAttempt(assignment, attempt),
getProgress: () => {
const strategyIndex = this.stats.routingStrategy === "group-by-layer" ? 1 : this.stats.routingStrategy === "deep-first" ? 2 : 0;
const workUnit = Number(this.stats.workUnit ?? 0);
const workUnitCount = Number(this.stats.workUnitCount ?? 0);
const strategyFraction = workUnitCount > 0 ? Math.min(1, workUnit / workUnitCount) : 0;
return (strategyIndex + strategyFraction) / 3;
}
});
this.stats = {
...this.stats,
phase: "prepare-assignment",
assignment: this.nextAssignmentIndex + 1,
assignmentCount: this.config.maxLayerCombinations,
routedConnections: `0/${this.inputSrj.connections.length}`
};
}
computeProgress() {
if (this.solved || this.failed)
return 1;
if (!this.routingInitialized) {
return 0.05 * (this.nextCandidateLayerBusIndex / Math.max(1, this.boundaryBuses.length + 1));
}
let activeAssignmentFraction = 0;
if (this.activeSubSolver?.getSolverName() === "FanoutAssignmentSolver") {
const strategyIndex = this.stats.routingStrategy === "group-by-layer" ? 1 : this.stats.routingStrategy === "deep-first" ? 2 : 0;
const workUnit = Number(this.stats.workUnit ?? 0);
const workUnitCount = Number(this.stats.workUnitCount ?? 0);
const strategyFraction = workUnitCount > 0 ? Math.min(1, workUnit / workUnitCount) : 0;
activeAssignmentFraction = (strategyIndex + strategyFraction) / 3;
}
return Math.min(0.99, 0.05 + 0.95 * ((this.nextAssignmentIndex + activeAssignmentFraction) / this.config.maxLayerCombinations));
}
getConstructorParams() {
return [this.inputSrj, this.options];
}
getOutput() {
if (!this.solved || !this.bestAttempt) {
throw new Error("FanoutSolver: getOutput() called before a complete fanout was solved");
}
const validation = this.validateCompletePlans(this.bestAttempt.plans, this.bestAttempt.outputSrj);
if (!validation.valid) {
throw new Error(`FanoutSolver: completed output failed validation: ${validation.issues[0]?.message ?? "unknown validation error"}`);
}
const finalSrj = addViaLayerMetadataToSrj({
srj: this.endpointCompletion?.simpleRouteJson ?? this.bestAttempt.outputSrj,
layerNames: this.config.layerNames,
allowBlindAndBuriedVias: this.config.allowBlindAndBuriedVias
});
const finalTraceById = new Map((finalSrj.traces ?? []).map((trace) => [trace.pcb_trace_id, trace]));
return {
simpleRouteJson: finalSrj,
fanoutTraces: this.bestAttempt.plans.flatMap((plan) => [
finalTraceById.get(plan.trace.pcb_trace_id) ?? plan.trace,
...plan.planeEndpointTrace ? [
finalTraceById.get(plan.planeEndpointTrace.pcb_trace_id) ?? plan.planeEndpointTrace
] : []
]),
completionTraces: (this.endpointCompletion?.traces ?? []).map((trace) => finalTraceById.get(trace.pcb_trace_id) ?? trace),
...this.endpointCompletion ? { endpointCompletion: this.endpointCompletion.report } : {},
planeTerminations: this.bestAttempt.plans.flatMap((plan) => plan.termination.type === "plane" && plan.via ? [
{
busId: plan.busId,
connectionName: plan.connectionName,
layer: plan.termination.layer,
via: plan.via
}
] : []),
busLayerAssignments: this.bestAttempt.summary.busLayerAssignments,
busDirections: Object.fromEntries(this.preparedBuses.map((bus) => [bus.busId, bus.direction])),
attempts: [...this.attempts],
validation
};
}
getOutputSimpleRouteJson() {
return this.getOutput().simpleRouteJson;
}
visualize() {
return this.activeSubSolver?.visualize() ?? this.visualizeCurrentState();
}
}
export {
FanoutSolver
};