@tscircuit/cli 0.1.1209 → 0.1.1211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -37959,7 +37959,7 @@ var init_chunk_SK323GHE = __esm(() => {
37959
37959
  debug2.namespace = namespace;
37960
37960
  debug2.useColors = createDebug.useColors();
37961
37961
  debug2.color = createDebug.selectColor(namespace);
37962
- debug2.extend = extend;
37962
+ debug2.extend = extend2;
37963
37963
  debug2.destroy = createDebug.destroy;
37964
37964
  Object.defineProperty(debug2, "enabled", {
37965
37965
  enumerable: true,
@@ -37983,7 +37983,7 @@ var init_chunk_SK323GHE = __esm(() => {
37983
37983
  }
37984
37984
  return debug2;
37985
37985
  }
37986
- function extend(namespace, delimiter) {
37986
+ function extend2(namespace, delimiter) {
37987
37987
  const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace);
37988
37988
  newDebug.log = this.log;
37989
37989
  return newDebug;
@@ -39002,7 +39002,7 @@ var init_chunk_SK323GHE = __esm(() => {
39002
39002
  element: "element",
39003
39003
  text: "text"
39004
39004
  };
39005
- var createNode = function createNode2(params2) {
39005
+ var createNode2 = function createNode22(params2) {
39006
39006
  return Object.assign({
39007
39007
  name: "",
39008
39008
  type: NodeType.element,
@@ -39030,7 +39030,7 @@ var init_chunk_SK323GHE = __esm(() => {
39030
39030
  current2 = rootNode;
39031
39031
  current2.name = data.value;
39032
39032
  } else {
39033
- var node = createNode({
39033
+ var node = createNode2({
39034
39034
  name: data.value,
39035
39035
  parent: current2
39036
39036
  });
@@ -39063,7 +39063,7 @@ var init_chunk_SK323GHE = __esm(() => {
39063
39063
  break;
39064
39064
  case Type.text:
39065
39065
  if (current2) {
39066
- current2.children.push(createNode({
39066
+ current2.children.push(createNode2({
39067
39067
  type: NodeType.text,
39068
39068
  value: data.value,
39069
39069
  parent: options.parentNodes ? current2 : null
@@ -39082,7 +39082,7 @@ var init_chunk_SK323GHE = __esm(() => {
39082
39082
  reader.reset = function() {
39083
39083
  lexer = Lexer.create({ debug: options.debug });
39084
39084
  lexer.on("data", handleLexerData);
39085
- rootNode = createNode();
39085
+ rootNode = createNode2();
39086
39086
  current2 = null;
39087
39087
  attrName = "";
39088
39088
  reader.parse = lexer.write;
@@ -95866,7 +95866,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
95866
95866
  glob2.sync = globSync;
95867
95867
  var GlobSync = glob2.GlobSync = globSync.GlobSync;
95868
95868
  glob2.glob = glob2;
95869
- function extend(origin, add) {
95869
+ function extend2(origin, add) {
95870
95870
  if (add === null || typeof add !== "object") {
95871
95871
  return origin;
95872
95872
  }
@@ -95878,7 +95878,7 @@ See: https://github.com/isaacs/node-glob/issues/167`);
95878
95878
  return origin;
95879
95879
  }
95880
95880
  glob2.hasMagic = function(pattern, options_) {
95881
- var options = extend({}, options_);
95881
+ var options = extend2({}, options_);
95882
95882
  options.noprocess = true;
95883
95883
  var g6 = new Glob(pattern, options);
95884
95884
  var set = g6.minimatch.set;
@@ -99964,7 +99964,7 @@ var import_perfect_cli = __toESM2(require_dist2(), 1);
99964
99964
  // lib/getVersion.ts
99965
99965
  import { createRequire as createRequire2 } from "node:module";
99966
99966
  // package.json
99967
- var version = "0.1.1208";
99967
+ var version = "0.1.1209";
99968
99968
  var package_default = {
99969
99969
  name: "@tscircuit/cli",
99970
99970
  version,
@@ -99976,7 +99976,7 @@ var package_default = {
99976
99976
  devDependencies: {
99977
99977
  "@babel/standalone": "^7.26.9",
99978
99978
  "@biomejs/biome": "^1.9.4",
99979
- "@tscircuit/circuit-json-placement-analysis": "^0.0.5",
99979
+ "@tscircuit/circuit-json-placement-analysis": "^0.0.6",
99980
99980
  "@tscircuit/circuit-json-routing-analysis": "^0.0.1",
99981
99981
  "@tscircuit/fake-snippets": "^0.0.182",
99982
99982
  "@tscircuit/file-server": "^0.0.32",
@@ -99996,13 +99996,15 @@ var package_default = {
99996
99996
  "bun-match-svg": "^0.0.12",
99997
99997
  chokidar: "4.0.1",
99998
99998
  "circuit-json": "^0.0.403",
99999
- "circuit-json-to-gerber": "^0.0.48",
100000
99999
  "circuit-json-to-bom-csv": "^0.0.7",
100001
- "circuit-json-to-pnp-csv": "^0.0.7",
100000
+ "circuit-json-to-gerber": "^0.0.48",
100002
100001
  "circuit-json-to-kicad": "^0.0.96",
100002
+ "circuit-json-to-pnp-csv": "^0.0.7",
100003
100003
  "circuit-json-to-readable-netlist": "^0.0.15",
100004
100004
  "circuit-json-to-spice": "^0.0.10",
100005
+ "circuit-json-to-step": "^0.0.20",
100005
100006
  "circuit-json-to-tscircuit": "^0.0.9",
100007
+ "circuit-json-trace-length-analysis": "github:tscircuit/circuit-json-trace-length-analysis#2b44792a40df0ca83b6bfb6ac95ed5e35e7168b8",
100006
100008
  commander: "^14.0.0",
100007
100009
  conf: "^13.1.0",
100008
100010
  configstore: "^7.0.0",
@@ -100030,13 +100032,12 @@ var package_default = {
100030
100032
  prompts: "^2.4.2",
100031
100033
  redaxios: "^0.5.1",
100032
100034
  semver: "^7.6.3",
100035
+ stepts: "^0.0.3",
100033
100036
  tempy: "^3.1.0",
100034
100037
  tscircuit: "0.0.1590-libonly",
100035
100038
  tsx: "^4.7.1",
100036
100039
  "typed-ky": "^0.0.4",
100037
- zod: "^3.23.8",
100038
- "circuit-json-to-step": "^0.0.20",
100039
- stepts: "^0.0.3"
100040
+ zod: "^3.23.8"
100040
100041
  },
100041
100042
  peerDependencies: {
100042
100043
  tscircuit: "*"
@@ -111779,7 +111780,12 @@ var convertCircuitJsonToReadableNetlist = (circuitJson) => {
111779
111780
  };
111780
111781
 
111781
111782
  // cli/check/netlist/register.ts
111783
+ import {
111784
+ categorizeErrorOrWarning as categorizeErrorOrWarning2
111785
+ } from "@tscircuit/circuit-json-util";
111782
111786
  import path40 from "node:path";
111787
+ var normalizeCategory2 = (category) => category === "netlist" || category === "pin_specification" || category === "placement" || category === "routing" ? category : "unknown";
111788
+ var isNetlistDiagnostic = (issue) => normalizeCategory2(categorizeErrorOrWarning2(issue)) === "netlist";
111783
111789
  var resolveInputFilePath = async (file) => {
111784
111790
  if (file) {
111785
111791
  return path40.isAbsolute(file) ? file : path40.resolve(process.cwd(), file);
@@ -111805,13 +111811,21 @@ var checkNetlist = async (file) => {
111805
111811
  });
111806
111812
  const typedCircuitJson = circuitJson;
111807
111813
  const diagnostics = analyzeCircuitJson(typedCircuitJson);
111814
+ const netlistErrors = diagnostics.errors.filter(isNetlistDiagnostic);
111815
+ const netlistWarnings = diagnostics.warnings.filter(isNetlistDiagnostic);
111808
111816
  const readableNetlist = convertCircuitJsonToReadableNetlist(typedCircuitJson);
111809
111817
  const diagnosticsLines = [
111810
- `Errors: ${diagnostics.errors.length}`,
111811
- `Warnings: ${diagnostics.warnings.length}`
111818
+ `Errors: ${netlistErrors.length}`,
111819
+ `Warnings: ${netlistWarnings.length}`
111812
111820
  ];
111813
- if (diagnostics.errors.length > 0) {
111814
- diagnosticsLines.push(...diagnostics.errors.map((err) => `- ${err.type}: ${err.message ?? ""}`));
111821
+ if (netlistErrors.length > 0) {
111822
+ diagnosticsLines.push(...netlistErrors.map((err) => `- ${err.type}: ${err.message ?? ""}`));
111823
+ }
111824
+ if (netlistWarnings.length > 0) {
111825
+ diagnosticsLines.push(...netlistWarnings.map((warning) => {
111826
+ const issueType = warning.warning_type ?? warning.error_type ?? warning.type;
111827
+ return `- ${issueType}: ${warning.message ?? ""}`;
111828
+ }));
111815
111829
  }
111816
111830
  return `${diagnosticsLines.join(`
111817
111831
  `)}
@@ -111914,6 +111928,454 @@ var registerCheckPinSpecification = (program2) => {
111914
111928
  });
111915
111929
  };
111916
111930
 
111931
+ // node_modules/@tscircuit/circuit-json-placement-analysis/dist/index.js
111932
+ import Flatbush from "flatbush";
111933
+
111934
+ // node_modules/quickselect/index.js
111935
+ function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {
111936
+ while (right > left) {
111937
+ if (right - left > 600) {
111938
+ const n = right - left + 1;
111939
+ const m = k - left + 1;
111940
+ const z2 = Math.log(n);
111941
+ const s = 0.5 * Math.exp(2 * z2 / 3);
111942
+ const sd = 0.5 * Math.sqrt(z2 * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
111943
+ const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
111944
+ const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
111945
+ quickselect(arr, k, newLeft, newRight, compare);
111946
+ }
111947
+ const t = arr[k];
111948
+ let i = left;
111949
+ let j = right;
111950
+ swap(arr, left, k);
111951
+ if (compare(arr[right], t) > 0)
111952
+ swap(arr, left, right);
111953
+ while (i < j) {
111954
+ swap(arr, i, j);
111955
+ i++;
111956
+ j--;
111957
+ while (compare(arr[i], t) < 0)
111958
+ i++;
111959
+ while (compare(arr[j], t) > 0)
111960
+ j--;
111961
+ }
111962
+ if (compare(arr[left], t) === 0)
111963
+ swap(arr, left, j);
111964
+ else {
111965
+ j++;
111966
+ swap(arr, j, right);
111967
+ }
111968
+ if (j <= k)
111969
+ left = j + 1;
111970
+ if (k <= j)
111971
+ right = j - 1;
111972
+ }
111973
+ }
111974
+ function swap(arr, i, j) {
111975
+ const tmp = arr[i];
111976
+ arr[i] = arr[j];
111977
+ arr[j] = tmp;
111978
+ }
111979
+ function defaultCompare(a, b) {
111980
+ return a < b ? -1 : a > b ? 1 : 0;
111981
+ }
111982
+
111983
+ // node_modules/rbush/index.js
111984
+ class RBush {
111985
+ constructor(maxEntries = 9) {
111986
+ this._maxEntries = Math.max(4, maxEntries);
111987
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
111988
+ this.clear();
111989
+ }
111990
+ all() {
111991
+ return this._all(this.data, []);
111992
+ }
111993
+ search(bbox) {
111994
+ let node = this.data;
111995
+ const result = [];
111996
+ if (!intersects(bbox, node))
111997
+ return result;
111998
+ const toBBox = this.toBBox;
111999
+ const nodesToSearch = [];
112000
+ while (node) {
112001
+ for (let i = 0;i < node.children.length; i++) {
112002
+ const child = node.children[i];
112003
+ const childBBox = node.leaf ? toBBox(child) : child;
112004
+ if (intersects(bbox, childBBox)) {
112005
+ if (node.leaf)
112006
+ result.push(child);
112007
+ else if (contains(bbox, childBBox))
112008
+ this._all(child, result);
112009
+ else
112010
+ nodesToSearch.push(child);
112011
+ }
112012
+ }
112013
+ node = nodesToSearch.pop();
112014
+ }
112015
+ return result;
112016
+ }
112017
+ collides(bbox) {
112018
+ let node = this.data;
112019
+ if (!intersects(bbox, node))
112020
+ return false;
112021
+ const nodesToSearch = [];
112022
+ while (node) {
112023
+ for (let i = 0;i < node.children.length; i++) {
112024
+ const child = node.children[i];
112025
+ const childBBox = node.leaf ? this.toBBox(child) : child;
112026
+ if (intersects(bbox, childBBox)) {
112027
+ if (node.leaf || contains(bbox, childBBox))
112028
+ return true;
112029
+ nodesToSearch.push(child);
112030
+ }
112031
+ }
112032
+ node = nodesToSearch.pop();
112033
+ }
112034
+ return false;
112035
+ }
112036
+ load(data) {
112037
+ if (!(data && data.length))
112038
+ return this;
112039
+ if (data.length < this._minEntries) {
112040
+ for (let i = 0;i < data.length; i++) {
112041
+ this.insert(data[i]);
112042
+ }
112043
+ return this;
112044
+ }
112045
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
112046
+ if (!this.data.children.length) {
112047
+ this.data = node;
112048
+ } else if (this.data.height === node.height) {
112049
+ this._splitRoot(this.data, node);
112050
+ } else {
112051
+ if (this.data.height < node.height) {
112052
+ const tmpNode = this.data;
112053
+ this.data = node;
112054
+ node = tmpNode;
112055
+ }
112056
+ this._insert(node, this.data.height - node.height - 1, true);
112057
+ }
112058
+ return this;
112059
+ }
112060
+ insert(item) {
112061
+ if (item)
112062
+ this._insert(item, this.data.height - 1);
112063
+ return this;
112064
+ }
112065
+ clear() {
112066
+ this.data = createNode([]);
112067
+ return this;
112068
+ }
112069
+ remove(item, equalsFn) {
112070
+ if (!item)
112071
+ return this;
112072
+ let node = this.data;
112073
+ const bbox = this.toBBox(item);
112074
+ const path42 = [];
112075
+ const indexes = [];
112076
+ let i, parent, goingUp;
112077
+ while (node || path42.length) {
112078
+ if (!node) {
112079
+ node = path42.pop();
112080
+ parent = path42[path42.length - 1];
112081
+ i = indexes.pop();
112082
+ goingUp = true;
112083
+ }
112084
+ if (node.leaf) {
112085
+ const index = findItem(item, node.children, equalsFn);
112086
+ if (index !== -1) {
112087
+ node.children.splice(index, 1);
112088
+ path42.push(node);
112089
+ this._condense(path42);
112090
+ return this;
112091
+ }
112092
+ }
112093
+ if (!goingUp && !node.leaf && contains(node, bbox)) {
112094
+ path42.push(node);
112095
+ indexes.push(i);
112096
+ i = 0;
112097
+ parent = node;
112098
+ node = node.children[0];
112099
+ } else if (parent) {
112100
+ i++;
112101
+ node = parent.children[i];
112102
+ goingUp = false;
112103
+ } else
112104
+ node = null;
112105
+ }
112106
+ return this;
112107
+ }
112108
+ toBBox(item) {
112109
+ return item;
112110
+ }
112111
+ compareMinX(a, b) {
112112
+ return a.minX - b.minX;
112113
+ }
112114
+ compareMinY(a, b) {
112115
+ return a.minY - b.minY;
112116
+ }
112117
+ toJSON() {
112118
+ return this.data;
112119
+ }
112120
+ fromJSON(data) {
112121
+ this.data = data;
112122
+ return this;
112123
+ }
112124
+ _all(node, result) {
112125
+ const nodesToSearch = [];
112126
+ while (node) {
112127
+ if (node.leaf)
112128
+ result.push(...node.children);
112129
+ else
112130
+ nodesToSearch.push(...node.children);
112131
+ node = nodesToSearch.pop();
112132
+ }
112133
+ return result;
112134
+ }
112135
+ _build(items, left, right, height) {
112136
+ const N = right - left + 1;
112137
+ let M = this._maxEntries;
112138
+ let node;
112139
+ if (N <= M) {
112140
+ node = createNode(items.slice(left, right + 1));
112141
+ calcBBox(node, this.toBBox);
112142
+ return node;
112143
+ }
112144
+ if (!height) {
112145
+ height = Math.ceil(Math.log(N) / Math.log(M));
112146
+ M = Math.ceil(N / Math.pow(M, height - 1));
112147
+ }
112148
+ node = createNode([]);
112149
+ node.leaf = false;
112150
+ node.height = height;
112151
+ const N2 = Math.ceil(N / M);
112152
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
112153
+ multiSelect(items, left, right, N1, this.compareMinX);
112154
+ for (let i = left;i <= right; i += N1) {
112155
+ const right2 = Math.min(i + N1 - 1, right);
112156
+ multiSelect(items, i, right2, N2, this.compareMinY);
112157
+ for (let j = i;j <= right2; j += N2) {
112158
+ const right3 = Math.min(j + N2 - 1, right2);
112159
+ node.children.push(this._build(items, j, right3, height - 1));
112160
+ }
112161
+ }
112162
+ calcBBox(node, this.toBBox);
112163
+ return node;
112164
+ }
112165
+ _chooseSubtree(bbox, node, level, path42) {
112166
+ while (true) {
112167
+ path42.push(node);
112168
+ if (node.leaf || path42.length - 1 === level)
112169
+ break;
112170
+ let minArea = Infinity;
112171
+ let minEnlargement = Infinity;
112172
+ let targetNode;
112173
+ for (let i = 0;i < node.children.length; i++) {
112174
+ const child = node.children[i];
112175
+ const area = bboxArea(child);
112176
+ const enlargement = enlargedArea(bbox, child) - area;
112177
+ if (enlargement < minEnlargement) {
112178
+ minEnlargement = enlargement;
112179
+ minArea = area < minArea ? area : minArea;
112180
+ targetNode = child;
112181
+ } else if (enlargement === minEnlargement) {
112182
+ if (area < minArea) {
112183
+ minArea = area;
112184
+ targetNode = child;
112185
+ }
112186
+ }
112187
+ }
112188
+ node = targetNode || node.children[0];
112189
+ }
112190
+ return node;
112191
+ }
112192
+ _insert(item, level, isNode) {
112193
+ const bbox = isNode ? item : this.toBBox(item);
112194
+ const insertPath = [];
112195
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
112196
+ node.children.push(item);
112197
+ extend(node, bbox);
112198
+ while (level >= 0) {
112199
+ if (insertPath[level].children.length > this._maxEntries) {
112200
+ this._split(insertPath, level);
112201
+ level--;
112202
+ } else
112203
+ break;
112204
+ }
112205
+ this._adjustParentBBoxes(bbox, insertPath, level);
112206
+ }
112207
+ _split(insertPath, level) {
112208
+ const node = insertPath[level];
112209
+ const M = node.children.length;
112210
+ const m = this._minEntries;
112211
+ this._chooseSplitAxis(node, m, M);
112212
+ const splitIndex = this._chooseSplitIndex(node, m, M);
112213
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
112214
+ newNode.height = node.height;
112215
+ newNode.leaf = node.leaf;
112216
+ calcBBox(node, this.toBBox);
112217
+ calcBBox(newNode, this.toBBox);
112218
+ if (level)
112219
+ insertPath[level - 1].children.push(newNode);
112220
+ else
112221
+ this._splitRoot(node, newNode);
112222
+ }
112223
+ _splitRoot(node, newNode) {
112224
+ this.data = createNode([node, newNode]);
112225
+ this.data.height = node.height + 1;
112226
+ this.data.leaf = false;
112227
+ calcBBox(this.data, this.toBBox);
112228
+ }
112229
+ _chooseSplitIndex(node, m, M) {
112230
+ let index;
112231
+ let minOverlap = Infinity;
112232
+ let minArea = Infinity;
112233
+ for (let i = m;i <= M - m; i++) {
112234
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
112235
+ const bbox2 = distBBox(node, i, M, this.toBBox);
112236
+ const overlap = intersectionArea(bbox1, bbox2);
112237
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
112238
+ if (overlap < minOverlap) {
112239
+ minOverlap = overlap;
112240
+ index = i;
112241
+ minArea = area < minArea ? area : minArea;
112242
+ } else if (overlap === minOverlap) {
112243
+ if (area < minArea) {
112244
+ minArea = area;
112245
+ index = i;
112246
+ }
112247
+ }
112248
+ }
112249
+ return index || M - m;
112250
+ }
112251
+ _chooseSplitAxis(node, m, M) {
112252
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
112253
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
112254
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
112255
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
112256
+ if (xMargin < yMargin)
112257
+ node.children.sort(compareMinX);
112258
+ }
112259
+ _allDistMargin(node, m, M, compare) {
112260
+ node.children.sort(compare);
112261
+ const toBBox = this.toBBox;
112262
+ const leftBBox = distBBox(node, 0, m, toBBox);
112263
+ const rightBBox = distBBox(node, M - m, M, toBBox);
112264
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
112265
+ for (let i = m;i < M - m; i++) {
112266
+ const child = node.children[i];
112267
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
112268
+ margin += bboxMargin(leftBBox);
112269
+ }
112270
+ for (let i = M - m - 1;i >= m; i--) {
112271
+ const child = node.children[i];
112272
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
112273
+ margin += bboxMargin(rightBBox);
112274
+ }
112275
+ return margin;
112276
+ }
112277
+ _adjustParentBBoxes(bbox, path42, level) {
112278
+ for (let i = level;i >= 0; i--) {
112279
+ extend(path42[i], bbox);
112280
+ }
112281
+ }
112282
+ _condense(path42) {
112283
+ for (let i = path42.length - 1, siblings;i >= 0; i--) {
112284
+ if (path42[i].children.length === 0) {
112285
+ if (i > 0) {
112286
+ siblings = path42[i - 1].children;
112287
+ siblings.splice(siblings.indexOf(path42[i]), 1);
112288
+ } else
112289
+ this.clear();
112290
+ } else
112291
+ calcBBox(path42[i], this.toBBox);
112292
+ }
112293
+ }
112294
+ }
112295
+ function findItem(item, items, equalsFn) {
112296
+ if (!equalsFn)
112297
+ return items.indexOf(item);
112298
+ for (let i = 0;i < items.length; i++) {
112299
+ if (equalsFn(item, items[i]))
112300
+ return i;
112301
+ }
112302
+ return -1;
112303
+ }
112304
+ function calcBBox(node, toBBox) {
112305
+ distBBox(node, 0, node.children.length, toBBox, node);
112306
+ }
112307
+ function distBBox(node, k, p, toBBox, destNode) {
112308
+ if (!destNode)
112309
+ destNode = createNode(null);
112310
+ destNode.minX = Infinity;
112311
+ destNode.minY = Infinity;
112312
+ destNode.maxX = -Infinity;
112313
+ destNode.maxY = -Infinity;
112314
+ for (let i = k;i < p; i++) {
112315
+ const child = node.children[i];
112316
+ extend(destNode, node.leaf ? toBBox(child) : child);
112317
+ }
112318
+ return destNode;
112319
+ }
112320
+ function extend(a, b) {
112321
+ a.minX = Math.min(a.minX, b.minX);
112322
+ a.minY = Math.min(a.minY, b.minY);
112323
+ a.maxX = Math.max(a.maxX, b.maxX);
112324
+ a.maxY = Math.max(a.maxY, b.maxY);
112325
+ return a;
112326
+ }
112327
+ function compareNodeMinX(a, b) {
112328
+ return a.minX - b.minX;
112329
+ }
112330
+ function compareNodeMinY(a, b) {
112331
+ return a.minY - b.minY;
112332
+ }
112333
+ function bboxArea(a) {
112334
+ return (a.maxX - a.minX) * (a.maxY - a.minY);
112335
+ }
112336
+ function bboxMargin(a) {
112337
+ return a.maxX - a.minX + (a.maxY - a.minY);
112338
+ }
112339
+ function enlargedArea(a, b) {
112340
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
112341
+ }
112342
+ function intersectionArea(a, b) {
112343
+ const minX = Math.max(a.minX, b.minX);
112344
+ const minY = Math.max(a.minY, b.minY);
112345
+ const maxX = Math.min(a.maxX, b.maxX);
112346
+ const maxY = Math.min(a.maxY, b.maxY);
112347
+ return Math.max(0, maxX - minX) * Math.max(0, maxY - minY);
112348
+ }
112349
+ function contains(a, b) {
112350
+ return a.minX <= b.minX && a.minY <= b.minY && b.maxX <= a.maxX && b.maxY <= a.maxY;
112351
+ }
112352
+ function intersects(a, b) {
112353
+ return b.minX <= a.maxX && b.minY <= a.maxY && b.maxX >= a.minX && b.maxY >= a.minY;
112354
+ }
112355
+ function createNode(children) {
112356
+ return {
112357
+ children,
112358
+ height: 1,
112359
+ leaf: true,
112360
+ minX: Infinity,
112361
+ minY: Infinity,
112362
+ maxX: -Infinity,
112363
+ maxY: -Infinity
112364
+ };
112365
+ }
112366
+ function multiSelect(arr, left, right, n, compare) {
112367
+ const stack = [left, right];
112368
+ while (stack.length) {
112369
+ right = stack.pop();
112370
+ left = stack.pop();
112371
+ if (right - left <= n)
112372
+ continue;
112373
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
112374
+ quickselect(arr, mid, left, right, compare);
112375
+ stack.push(left, mid, mid, right);
112376
+ }
112377
+ }
112378
+
111917
112379
  // node_modules/@tscircuit/circuit-json-placement-analysis/dist/index.js
111918
112380
  var CENTER_ANCHOR = "center";
111919
112381
  var toNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -112417,6 +112879,9 @@ var analyzeComponentPlacement = (circuitJson, componentName) => {
112417
112879
  };
112418
112880
  var TOP_ISSUE_LIMIT = 5;
112419
112881
  var CENTER_ANCHOR2 = "center";
112882
+ var LARGE_EMPTY_SPACE_THRESHOLD_RATIO = 0.05;
112883
+ var EMPTY_SPACE_SAMPLE_STEP_MM = 5;
112884
+ var GEOMETRY_EPSILON = 0.000001;
112420
112885
  var ISSUE_TYPE_ORDER = [
112421
112886
  "pad_overlap",
112422
112887
  "off_board",
@@ -112431,6 +112896,8 @@ var fmtNumber2 = (value) => {
112431
112896
  return value.toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
112432
112897
  };
112433
112898
  var fmtMm2 = (value) => `${fmtNumber2(value)}mm`;
112899
+ var fmtArea = (value) => `${fmtNumber2(value)}mm^2`;
112900
+ var fmtPercent = (value) => `${fmtNumber2(value)}%`;
112434
112901
  var getBoundsFromCenterAndSize = (centerX, centerY, width, height) => ({
112435
112902
  min_x: centerX - width / 2,
112436
112903
  max_x: centerX + width / 2,
@@ -112476,6 +112943,22 @@ var getOverlap = (a, b) => {
112476
112943
  clearance: -Math.min(overlapX, overlapY)
112477
112944
  };
112478
112945
  };
112946
+ var getBoundsIntersection = (a, b) => {
112947
+ const min_x = Math.max(a.min_x, b.min_x);
112948
+ const max_x = Math.min(a.max_x, b.max_x);
112949
+ const min_y = Math.max(a.min_y, b.min_y);
112950
+ const max_y = Math.min(a.max_y, b.max_y);
112951
+ if (min_x >= max_x || min_y >= max_y)
112952
+ return null;
112953
+ return {
112954
+ min_x,
112955
+ max_x,
112956
+ min_y,
112957
+ max_y,
112958
+ width: max_x - min_x,
112959
+ height: max_y - min_y
112960
+ };
112961
+ };
112479
112962
  var layersIntersect = (a, b) => {
112480
112963
  const bSet = new Set(b);
112481
112964
  return a.some((layer) => bSet.has(layer));
@@ -112485,6 +112968,14 @@ var getArea = (bounds) => {
112485
112968
  return Number.POSITIVE_INFINITY;
112486
112969
  return bounds.width * bounds.height;
112487
112970
  };
112971
+ var toPlacementBounds = (bounds) => ({
112972
+ width: bounds.width,
112973
+ height: bounds.height,
112974
+ min_x: bounds.min_x,
112975
+ max_x: bounds.max_x,
112976
+ min_y: bounds.min_y,
112977
+ max_y: bounds.max_y
112978
+ });
112488
112979
  var stripNumericSuffix = (name) => {
112489
112980
  const stripped = name.replace(/\d+$/, "");
112490
112981
  return stripped.length >= 2 ? stripped : name;
@@ -112784,6 +113275,263 @@ var buildComponentContexts = (circuitJson) => {
112784
113275
  boardBounds
112785
113276
  };
112786
113277
  };
113278
+ var getTopCopperBounds = (component) => {
113279
+ const topPadBounds = component.pads.filter((pad) => pad.layers.includes("top")).map((pad) => pad.bounds);
113280
+ if (topPadBounds.length === 0)
113281
+ return null;
113282
+ return {
113283
+ min_x: Math.min(...topPadBounds.map((bounds) => bounds.min_x)),
113284
+ max_x: Math.max(...topPadBounds.map((bounds) => bounds.max_x)),
113285
+ min_y: Math.min(...topPadBounds.map((bounds) => bounds.min_y)),
113286
+ max_y: Math.max(...topPadBounds.map((bounds) => bounds.max_y)),
113287
+ width: Math.max(...topPadBounds.map((bounds) => bounds.max_x)) - Math.min(...topPadBounds.map((bounds) => bounds.min_x)),
113288
+ height: Math.max(...topPadBounds.map((bounds) => bounds.max_y)) - Math.min(...topPadBounds.map((bounds) => bounds.min_y))
113289
+ };
113290
+ };
113291
+ var getTopOccupancyBounds = (component) => {
113292
+ if (component.layer !== "top")
113293
+ return [];
113294
+ if (component.courtyards.length > 0)
113295
+ return component.courtyards;
113296
+ const topCopperBounds = getTopCopperBounds(component);
113297
+ return topCopperBounds ? [topCopperBounds] : [];
113298
+ };
113299
+ var getUniqueSortedCoordinates = (values) => [...new Set(values)].sort((a, b) => a - b);
113300
+ var createBounds = (min_x, max_x, min_y, max_y) => ({
113301
+ min_x,
113302
+ max_x,
113303
+ min_y,
113304
+ max_y,
113305
+ width: max_x - min_x,
113306
+ height: max_y - min_y
113307
+ });
113308
+ var pointIsInsideBounds = (x, y, bounds) => x > bounds.min_x + GEOMETRY_EPSILON && x < bounds.max_x - GEOMETRY_EPSILON && y > bounds.min_y + GEOMETRY_EPSILON && y < bounds.max_y - GEOMETRY_EPSILON;
113309
+ var buildFlatbushIndex = (boundsList) => {
113310
+ if (boundsList.length === 0)
113311
+ return null;
113312
+ const index = new Flatbush(boundsList.length);
113313
+ for (const bounds of boundsList) {
113314
+ index.add(bounds.min_x, bounds.min_y, bounds.max_x, bounds.max_y);
113315
+ }
113316
+ index.finish();
113317
+ return index;
113318
+ };
113319
+ var boundsOverlapWithArea = (a, b) => getBoundsIntersection(a, b) !== null;
113320
+ var isBoundsEmpty = (bounds, occupiedBounds, occupiedIndex) => {
113321
+ if (bounds.width <= GEOMETRY_EPSILON || bounds.height <= GEOMETRY_EPSILON) {
113322
+ return true;
113323
+ }
113324
+ const candidates = occupiedIndex ? occupiedIndex.search(bounds.min_x, bounds.min_y, bounds.max_x, bounds.max_y) : occupiedBounds.map((_, index) => index);
113325
+ return !candidates.some((candidateIndex) => boundsOverlapWithArea(bounds, occupiedBounds[candidateIndex]));
113326
+ };
113327
+ var isPointOccupied = (x, y, occupiedBounds, occupiedIndex) => {
113328
+ const candidates = occupiedIndex ? occupiedIndex.search(x, y, x, y) : occupiedBounds.map((_, index) => index);
113329
+ return candidates.some((candidateIndex) => pointIsInsideBounds(x, y, occupiedBounds[candidateIndex]));
113330
+ };
113331
+ var getSampleAxisValues = (min, max, step) => {
113332
+ const values = /* @__PURE__ */ new Set([min, max]);
113333
+ let current = min;
113334
+ while (current < max - GEOMETRY_EPSILON) {
113335
+ const next = Math.min(current + step, max);
113336
+ values.add(current);
113337
+ values.add((current + next) / 2);
113338
+ values.add(next);
113339
+ current = next;
113340
+ }
113341
+ return [...values].sort((a, b) => a - b);
113342
+ };
113343
+ var getExpansionDelta = (bounds, boardBounds, direction, occupiedBounds, occupiedIndex) => {
113344
+ const searchIndices = (() => {
113345
+ switch (direction) {
113346
+ case "left":
113347
+ return occupiedIndex ? occupiedIndex.search(boardBounds.min_x, bounds.min_y, bounds.min_x, bounds.max_y) : occupiedBounds.map((_, index) => index);
113348
+ case "right":
113349
+ return occupiedIndex ? occupiedIndex.search(bounds.max_x, bounds.min_y, boardBounds.max_x, bounds.max_y) : occupiedBounds.map((_, index) => index);
113350
+ case "up":
113351
+ return occupiedIndex ? occupiedIndex.search(bounds.min_x, boardBounds.min_y, bounds.max_x, bounds.min_y) : occupiedBounds.map((_, index) => index);
113352
+ case "down":
113353
+ return occupiedIndex ? occupiedIndex.search(bounds.min_x, bounds.max_y, bounds.max_x, boardBounds.max_y) : occupiedBounds.map((_, index) => index);
113354
+ }
113355
+ })();
113356
+ const overlappingCandidates = searchIndices.map((index) => occupiedBounds[index]).filter((candidate) => {
113357
+ if (direction === "left" || direction === "right") {
113358
+ return Math.min(bounds.max_y, candidate.max_y) - Math.max(bounds.min_y, candidate.min_y) > GEOMETRY_EPSILON;
113359
+ }
113360
+ return Math.min(bounds.max_x, candidate.max_x) - Math.max(bounds.min_x, candidate.min_x) > GEOMETRY_EPSILON;
113361
+ });
113362
+ switch (direction) {
113363
+ case "left":
113364
+ return Math.max(0, Math.min(bounds.min_x - boardBounds.min_x, ...overlappingCandidates.filter((candidate) => candidate.max_x <= bounds.min_x + GEOMETRY_EPSILON).map((candidate) => bounds.min_x - candidate.max_x)));
113365
+ case "right":
113366
+ return Math.max(0, Math.min(boardBounds.max_x - bounds.max_x, ...overlappingCandidates.filter((candidate) => candidate.min_x >= bounds.max_x - GEOMETRY_EPSILON).map((candidate) => candidate.min_x - bounds.max_x)));
113367
+ case "up":
113368
+ return Math.max(0, Math.min(bounds.min_y - boardBounds.min_y, ...overlappingCandidates.filter((candidate) => candidate.max_y <= bounds.min_y + GEOMETRY_EPSILON).map((candidate) => bounds.min_y - candidate.max_y)));
113369
+ case "down":
113370
+ return Math.max(0, Math.min(boardBounds.max_y - bounds.max_y, ...overlappingCandidates.filter((candidate) => candidate.min_y >= bounds.max_y - GEOMETRY_EPSILON).map((candidate) => candidate.min_y - bounds.max_y)));
113371
+ }
113372
+ };
113373
+ var expandBounds = (bounds, direction, delta) => {
113374
+ switch (direction) {
113375
+ case "left":
113376
+ return createBounds(bounds.min_x - delta, bounds.max_x, bounds.min_y, bounds.max_y);
113377
+ case "right":
113378
+ return createBounds(bounds.min_x, bounds.max_x + delta, bounds.min_y, bounds.max_y);
113379
+ case "up":
113380
+ return createBounds(bounds.min_x, bounds.max_x, bounds.min_y - delta, bounds.max_y);
113381
+ case "down":
113382
+ return createBounds(bounds.min_x, bounds.max_x, bounds.min_y, bounds.max_y + delta);
113383
+ }
113384
+ };
113385
+ var growBoundsInDirection = (initialBounds, boardBounds, occupiedBounds, occupiedIndex, direction) => {
113386
+ const delta = getExpansionDelta(initialBounds, boardBounds, direction, occupiedBounds, occupiedIndex);
113387
+ if (delta <= GEOMETRY_EPSILON)
113388
+ return initialBounds;
113389
+ const nextBounds = expandBounds(initialBounds, direction, delta);
113390
+ return isBoundsEmpty(nextBounds, occupiedBounds, occupiedIndex) ? nextBounds : initialBounds;
113391
+ };
113392
+ var growRectangleFully = (initialBounds, boardBounds, occupiedBounds, occupiedIndex) => {
113393
+ let currentBounds = initialBounds;
113394
+ while (true) {
113395
+ const candidates = ["left", "right", "up", "down"].map((direction) => {
113396
+ const delta = getExpansionDelta(currentBounds, boardBounds, direction, occupiedBounds, occupiedIndex);
113397
+ if (delta <= GEOMETRY_EPSILON)
113398
+ return null;
113399
+ const nextBounds = expandBounds(currentBounds, direction, delta);
113400
+ if (!isBoundsEmpty(nextBounds, occupiedBounds, occupiedIndex))
113401
+ return null;
113402
+ return {
113403
+ direction,
113404
+ bounds: nextBounds,
113405
+ area: nextBounds.width * nextBounds.height
113406
+ };
113407
+ }).filter((candidate) => Boolean(candidate)).sort((a, b) => {
113408
+ if (b.area !== a.area)
113409
+ return b.area - a.area;
113410
+ return a.direction.localeCompare(b.direction);
113411
+ });
113412
+ const bestCandidate = candidates[0];
113413
+ if (!bestCandidate)
113414
+ return currentBounds;
113415
+ currentBounds = bestCandidate.bounds;
113416
+ }
113417
+ };
113418
+ var getLargestEmptySpaceFromPoint = (x, y, boardBounds, occupiedBounds, occupiedIndex) => {
113419
+ if (isPointOccupied(x, y, occupiedBounds, occupiedIndex))
113420
+ return null;
113421
+ const horizontalSpan = [
113422
+ "left",
113423
+ "right"
113424
+ ].reduce((bounds, direction) => growBoundsInDirection(bounds, boardBounds, occupiedBounds, occupiedIndex, direction), createBounds(x, x, y - GEOMETRY_EPSILON, y + GEOMETRY_EPSILON));
113425
+ const verticalSpan = [
113426
+ "up",
113427
+ "down"
113428
+ ].reduce((bounds, direction) => growBoundsInDirection(bounds, boardBounds, occupiedBounds, occupiedIndex, direction), createBounds(x - GEOMETRY_EPSILON, x + GEOMETRY_EPSILON, y, y));
113429
+ const horizontalFirst = growRectangleFully(["up", "down"].reduce((bounds, direction) => growBoundsInDirection(bounds, boardBounds, occupiedBounds, occupiedIndex, direction), horizontalSpan), boardBounds, occupiedBounds, occupiedIndex);
113430
+ const verticalFirst = growRectangleFully(["left", "right"].reduce((bounds, direction) => growBoundsInDirection(bounds, boardBounds, occupiedBounds, occupiedIndex, direction), verticalSpan), boardBounds, occupiedBounds, occupiedIndex);
113431
+ const horizontalArea = horizontalFirst.width * horizontalFirst.height;
113432
+ const verticalArea = verticalFirst.width * verticalFirst.height;
113433
+ if (horizontalArea <= GEOMETRY_EPSILON && verticalArea <= GEOMETRY_EPSILON) {
113434
+ return null;
113435
+ }
113436
+ return horizontalArea >= verticalArea ? horizontalFirst : verticalFirst;
113437
+ };
113438
+ var createEmptySpaceIndexItem = (bounds, boardArea, sequenceNumber) => {
113439
+ const area = bounds.width * bounds.height;
113440
+ return {
113441
+ id: `empty-space-${sequenceNumber}`,
113442
+ minX: bounds.min_x,
113443
+ minY: bounds.min_y,
113444
+ maxX: bounds.max_x,
113445
+ maxY: bounds.max_y,
113446
+ area,
113447
+ areaPercent: boardArea === 0 ? 0 : area / boardArea * 100,
113448
+ bounds: toPlacementBounds(bounds)
113449
+ };
113450
+ };
113451
+ var buildLargeEmptySpaces = (boardBounds, occupiedBounds, occupiedIndex, boardArea, thresholdArea) => {
113452
+ const emptySpaceIndex = new RBush;
113453
+ const sampleXs = getSampleAxisValues(boardBounds.min_x, boardBounds.max_x, EMPTY_SPACE_SAMPLE_STEP_MM);
113454
+ const sampleYs = getSampleAxisValues(boardBounds.min_y, boardBounds.max_y, EMPTY_SPACE_SAMPLE_STEP_MM);
113455
+ let sequenceNumber = 0;
113456
+ for (const y of sampleYs) {
113457
+ for (const x of sampleXs) {
113458
+ const bounds = getLargestEmptySpaceFromPoint(x, y, boardBounds, occupiedBounds, occupiedIndex);
113459
+ if (!bounds)
113460
+ continue;
113461
+ const area = bounds.width * bounds.height;
113462
+ if (area <= thresholdArea)
113463
+ continue;
113464
+ const candidate = createEmptySpaceIndexItem(bounds, boardArea, sequenceNumber++);
113465
+ const overlappingSpaces = emptySpaceIndex.search(candidate).filter((space) => boundsOverlapWithArea(bounds, createBounds(space.minX, space.maxX, space.minY, space.maxY)));
113466
+ if (overlappingSpaces.some((space) => space.area >= candidate.area)) {
113467
+ continue;
113468
+ }
113469
+ for (const overlappingSpace of overlappingSpaces) {
113470
+ emptySpaceIndex.remove(overlappingSpace);
113471
+ }
113472
+ emptySpaceIndex.insert(candidate);
113473
+ }
113474
+ }
113475
+ return emptySpaceIndex.all().map(({ area, areaPercent, bounds }) => ({
113476
+ area,
113477
+ areaPercent,
113478
+ bounds
113479
+ })).sort((a, b) => {
113480
+ if (b.area !== a.area)
113481
+ return b.area - a.area;
113482
+ if (a.bounds.min_y !== b.bounds.min_y) {
113483
+ return a.bounds.min_y - b.bounds.min_y;
113484
+ }
113485
+ return a.bounds.min_x - b.bounds.min_x;
113486
+ });
113487
+ };
113488
+ var buildBoardTopLayerReport = (components, boardBounds) => {
113489
+ if (!boardBounds)
113490
+ return null;
113491
+ const boardArea = boardBounds.width * boardBounds.height;
113492
+ const largeEmptySpaceThresholdArea = boardArea * LARGE_EMPTY_SPACE_THRESHOLD_RATIO;
113493
+ const occupancyBounds = components.flatMap((component) => getTopOccupancyBounds(component).map((bounds) => getBoundsIntersection(bounds, boardBounds)).filter((bounds) => Boolean(bounds)));
113494
+ const occupiedIndex = buildFlatbushIndex(occupancyBounds);
113495
+ const xCoords = getUniqueSortedCoordinates([
113496
+ boardBounds.min_x,
113497
+ boardBounds.max_x,
113498
+ ...occupancyBounds.flatMap((bounds) => [bounds.min_x, bounds.max_x])
113499
+ ]);
113500
+ const yCoords = getUniqueSortedCoordinates([
113501
+ boardBounds.min_y,
113502
+ boardBounds.max_y,
113503
+ ...occupancyBounds.flatMap((bounds) => [bounds.min_y, bounds.max_y])
113504
+ ]);
113505
+ const occupied = [];
113506
+ let occupiedArea = 0;
113507
+ for (let yi = 0;yi < yCoords.length - 1; yi += 1) {
113508
+ const row = [];
113509
+ for (let xi = 0;xi < xCoords.length - 1; xi += 1) {
113510
+ const cellBounds = {
113511
+ min_x: xCoords[xi],
113512
+ max_x: xCoords[xi + 1],
113513
+ min_y: yCoords[yi],
113514
+ max_y: yCoords[yi + 1],
113515
+ width: xCoords[xi + 1] - xCoords[xi],
113516
+ height: yCoords[yi + 1] - yCoords[yi]
113517
+ };
113518
+ const candidateIndices = occupiedIndex ? occupiedIndex.search(cellBounds.min_x, cellBounds.min_y, cellBounds.max_x, cellBounds.max_y) : occupancyBounds.map((_, index) => index);
113519
+ const isOccupied = candidateIndices.some((candidateIndex) => boundsOverlapWithArea(cellBounds, occupancyBounds[candidateIndex]));
113520
+ row.push(isOccupied);
113521
+ if (isOccupied)
113522
+ occupiedArea += cellBounds.width * cellBounds.height;
113523
+ }
113524
+ occupied.push(row);
113525
+ }
113526
+ const largeEmptySpaces = buildLargeEmptySpaces(boardBounds, occupancyBounds, occupiedIndex, boardArea, largeEmptySpaceThresholdArea);
113527
+ return {
113528
+ boardArea,
113529
+ occupiedArea,
113530
+ utilizationPercent: boardArea === 0 ? 0 : occupiedArea / boardArea * 100,
113531
+ largeEmptySpaceThresholdArea,
113532
+ largeEmptySpaces
113533
+ };
113534
+ };
112787
113535
  var buildIssues = (components, boardBounds) => {
112788
113536
  const issues = [];
112789
113537
  for (const component of components) {
@@ -113000,6 +113748,7 @@ var formatSourcePlacement = (component) => {
113000
113748
  }
113001
113749
  return bits.join(", ");
113002
113750
  };
113751
+ var formatBounds = (bounds) => `bounds=(minX=${fmtMm2(bounds.min_x)}, maxX=${fmtMm2(bounds.max_x)}, minY=${fmtMm2(bounds.min_y)}, maxY=${fmtMm2(bounds.max_y)})`;
113003
113752
  var formatResolvedPlacement = (component) => {
113004
113753
  const bits = [];
113005
113754
  const center = component.resolvedPlacement.center;
@@ -113008,7 +113757,7 @@ var formatResolvedPlacement = (component) => {
113008
113757
  bits.push(`center=(${fmtMm2(center.x)}, ${fmtMm2(center.y)}) on ${center.layer}`);
113009
113758
  }
113010
113759
  if (bounds) {
113011
- bits.push(`bounds=(minX=${fmtMm2(bounds.min_x)}, maxX=${fmtMm2(bounds.max_x)}, minY=${fmtMm2(bounds.min_y)}, maxY=${fmtMm2(bounds.max_y)})`);
113760
+ bits.push(formatBounds(bounds));
113012
113761
  bits.push(`size=(width=${fmtMm2(bounds.width)}, height=${fmtMm2(bounds.height)})`);
113013
113762
  }
113014
113763
  bits.push(`anchor_alignment="${component.resolvedPlacement.anchorAlignment}"`);
@@ -113039,6 +113788,20 @@ var formatPlacementAnalysisReport = (report) => {
113039
113788
  lines.push(`- ${cluster.clusterName}: ${cluster.componentNames.join(", ")}`);
113040
113789
  }
113041
113790
  }
113791
+ if (report.boardTopLayer) {
113792
+ const emptySpaceThresholdPercent = report.boardTopLayer.boardArea === 0 ? 0 : report.boardTopLayer.largeEmptySpaceThresholdArea / report.boardTopLayer.boardArea * 100;
113793
+ lines.push("");
113794
+ lines.push("board top-layer utilization:");
113795
+ lines.push(`- occupied: ${fmtPercent(report.boardTopLayer.utilizationPercent)} (${fmtArea(report.boardTopLayer.occupiedArea)} of ${fmtArea(report.boardTopLayer.boardArea)})`);
113796
+ if (report.boardTopLayer.largeEmptySpaces.length > 0) {
113797
+ lines.push(`- empty spaces over ${fmtPercent(emptySpaceThresholdPercent)} of board area:`);
113798
+ for (const emptySpace of report.boardTopLayer.largeEmptySpaces) {
113799
+ lines.push(` - ${fmtPercent(emptySpace.areaPercent)} (${fmtArea(emptySpace.area)}); ${formatBounds(emptySpace.bounds)}`);
113800
+ }
113801
+ } else {
113802
+ lines.push(`- empty spaces over ${fmtPercent(emptySpaceThresholdPercent)} of board area: none`);
113803
+ }
113804
+ }
113042
113805
  lines.push("");
113043
113806
  lines.push("board-edge status:");
113044
113807
  for (const component of report.components) {
@@ -113071,6 +113834,7 @@ var buildPlacementAnalysisReport = (circuitJson) => {
113071
113834
  const issues = buildIssues(components, boardBounds);
113072
113835
  const clusters = buildClusters(components, issues);
113073
113836
  const countsByType = buildCountsByType(issues);
113837
+ const boardTopLayer = buildBoardTopLayerReport(components, boardBounds);
113074
113838
  return {
113075
113839
  summary: {
113076
113840
  totalIssueCount: issues.length,
@@ -113078,6 +113842,7 @@ var buildPlacementAnalysisReport = (circuitJson) => {
113078
113842
  topIssues: issues.slice(0, TOP_ISSUE_LIMIT),
113079
113843
  likelyBadClusters: clusters
113080
113844
  },
113845
+ boardTopLayer,
113081
113846
  components: buildComponentStatuses(components, boardBounds, issues),
113082
113847
  issues
113083
113848
  };
@@ -113109,10 +113874,10 @@ var analyzeAllPlacements = (circuitJson) => {
113109
113874
 
113110
113875
  // cli/check/placement/register.ts
113111
113876
  import {
113112
- categorizeErrorOrWarning as categorizeErrorOrWarning2
113877
+ categorizeErrorOrWarning as categorizeErrorOrWarning3
113113
113878
  } from "@tscircuit/circuit-json-util";
113114
- var normalizeCategory2 = (category) => category === "netlist" || category === "pin_specification" || category === "placement" || category === "routing" ? category : "unknown";
113115
- var isPlacementDiagnostic = (issue) => normalizeCategory2(categorizeErrorOrWarning2(issue)) === "placement";
113879
+ var normalizeCategory3 = (category) => category === "netlist" || category === "pin_specification" || category === "placement" || category === "routing" ? category : "unknown";
113880
+ var isPlacementDiagnostic = (issue) => normalizeCategory3(categorizeErrorOrWarning3(issue)) === "placement";
113116
113881
  var getIssueType = (issue) => issue.error_type ?? issue.warning_type ?? issue.type ?? "unknown_issue";
113117
113882
  var getScopedComponentIds = (circuitJson, refdes) => {
113118
113883
  const sourceComponentIds = new Set;
@@ -113210,8 +113975,8 @@ var registerCheck = (program2) => {
113210
113975
 
113211
113976
  // node_modules/@tscircuit/circuit-json-routing-analysis/dist/index.js
113212
113977
  init_chunk_SK323GHE();
113213
- import Flatbush from "flatbush";
113214
113978
  import Flatbush2 from "flatbush";
113979
+ import Flatbush22 from "flatbush";
113215
113980
  import { webcrypto as crypto4 } from "crypto";
113216
113981
  import Flatbush3 from "flatbush";
113217
113982
  var require_object_hash = __commonJS2({
@@ -135515,7 +136280,7 @@ var require_is_extendable = __commonJS2({
135515
136280
  var require_extend_shallow = __commonJS2({
135516
136281
  "node_modules/extend-shallow/index.js"(exports2, module2) {
135517
136282
  var isObject22 = require_is_extendable();
135518
- module2.exports = function extend(o22) {
136283
+ module2.exports = function extend2(o22) {
135519
136284
  if (!isObject22(o22)) {
135520
136285
  o22 = {};
135521
136286
  }
@@ -135543,10 +136308,10 @@ var require_extend_shallow = __commonJS2({
135543
136308
  var require_condense_newlines = __commonJS2({
135544
136309
  "node_modules/condense-newlines/index.js"(exports2, module2) {
135545
136310
  var isWhitespace2 = require_is_whitespace();
135546
- var extend = require_extend_shallow();
136311
+ var extend2 = require_extend_shallow();
135547
136312
  var typeOf = require_kind_of();
135548
136313
  module2.exports = function(str, options) {
135549
- var opts = extend({}, options);
136314
+ var opts = extend2({}, options);
135550
136315
  var sep = opts.sep || `
135551
136316
 
135552
136317
  `;
@@ -135596,7 +136361,7 @@ var require_pretty = __commonJS2({
135596
136361
  "node_modules/pretty/index.js"(exports2, module2) {
135597
136362
  var beautify = require_js();
135598
136363
  var condense = require_condense_newlines();
135599
- var extend = require_extend_shallow();
136364
+ var extend2 = require_extend_shallow();
135600
136365
  var defaults = {
135601
136366
  unformatted: ["code", "pre", "em", "strong", "span"],
135602
136367
  indent_inner_html: true,
@@ -135606,7 +136371,7 @@ var require_pretty = __commonJS2({
135606
136371
  `
135607
136372
  };
135608
136373
  module2.exports = function pretty2(str, options) {
135609
- var opts = extend({}, defaults, options);
136374
+ var opts = extend2({}, defaults, options);
135610
136375
  str = beautify.html(str, opts);
135611
136376
  if (opts.ocd === true) {
135612
136377
  if (opts.newlines)
@@ -135921,7 +136686,7 @@ var translateBounds = (bounds, direction2, distance7) => {
135921
136686
  var MAX_FREE_SPACE_MM = 5;
135922
136687
  var BINARY_SEARCH_ITERATIONS = 10;
135923
136688
  var buildComponentSpatialIndex = (components) => {
135924
- const index = new Flatbush(components.length);
136689
+ const index = new Flatbush2(components.length);
135925
136690
  for (const component of components) {
135926
136691
  index.add(component.bounds.minX, component.bounds.minY, component.bounds.maxX, component.bounds.maxY);
135927
136692
  }
@@ -136848,7 +137613,7 @@ var require_reader4 = __commonJS22({
136848
137613
  element: "element",
136849
137614
  text: "text"
136850
137615
  };
136851
- var createNode = function createNode2(params2) {
137616
+ var createNode2 = function createNode22(params2) {
136852
137617
  return Object.assign({
136853
137618
  name: "",
136854
137619
  type: NodeType.element,
@@ -136876,7 +137641,7 @@ var require_reader4 = __commonJS22({
136876
137641
  current2 = rootNode;
136877
137642
  current2.name = data.value;
136878
137643
  } else {
136879
- var node = createNode({
137644
+ var node = createNode2({
136880
137645
  name: data.value,
136881
137646
  parent: current2
136882
137647
  });
@@ -136909,7 +137674,7 @@ var require_reader4 = __commonJS22({
136909
137674
  break;
136910
137675
  case Type.text:
136911
137676
  if (current2) {
136912
- current2.children.push(createNode({
137677
+ current2.children.push(createNode2({
136913
137678
  type: NodeType.text,
136914
137679
  value: data.value,
136915
137680
  parent: options.parentNodes ? current2 : null
@@ -136928,7 +137693,7 @@ var require_reader4 = __commonJS22({
136928
137693
  reader.reset = function() {
136929
137694
  lexer = Lexer.create({ debug: options.debug });
136930
137695
  lexer.on("data", handleLexerData);
136931
- rootNode = createNode();
137696
+ rootNode = createNode2();
136932
137697
  current2 = null;
136933
137698
  attrName = "";
136934
137699
  reader.parse = lexer.write;
@@ -171024,8 +171789,8 @@ var isPointInsidePolygon = (point5, polygon2) => {
171024
171789
  const yi22 = polygon2[i2].y;
171025
171790
  const xj = polygon2[j22].x;
171026
171791
  const yj = polygon2[j22].y;
171027
- const intersects = yi22 > point5.y !== yj > point5.y && point5.x < (xj - xi22) * (point5.y - yi22) / (yj - yi22) + xi22;
171028
- if (intersects) {
171792
+ const intersects2 = yi22 > point5.y !== yj > point5.y && point5.x < (xj - xi22) * (point5.y - yi22) / (yj - yi22) + xi22;
171793
+ if (intersects2) {
171029
171794
  inside2 = !inside2;
171030
171795
  }
171031
171796
  }
@@ -192678,7 +193443,7 @@ function getChildFunc(next, adapter) {
192678
193443
  }
192679
193444
  var filters = {
192680
193445
  contains(next, text, { adapter }) {
192681
- return function contains(elem) {
193446
+ return function contains2(elem) {
192682
193447
  return next(elem) && adapter.getText(elem).includes(text);
192683
193448
  };
192684
193449
  },
@@ -194911,7 +195676,7 @@ var DE9IM = class {
194911
195676
  }
194912
195677
  };
194913
195678
  function ray_shoot(polygon2, point5) {
194914
- let contains = undefined;
195679
+ let contains2 = undefined;
194915
195680
  let ray2 = new Flatten.Ray(point5);
194916
195681
  let line2 = new Flatten.Line(ray2.pt, ray2.norm);
194917
195682
  const searchBox = new Flatten.Box(ray2.box.xmin - Flatten.DP_TOL, ray2.box.ymin - Flatten.DP_TOL, ray2.box.xmax + Flatten.DP_TOL, ray2.box.ymax + Flatten.DP_TOL);
@@ -195010,8 +195775,8 @@ function ray_shoot(polygon2, point5) {
195010
195775
  }
195011
195776
  }
195012
195777
  }
195013
- contains = counter % 2 === 1 ? INSIDE$2 : OUTSIDE$1;
195014
- return contains;
195778
+ contains2 = counter % 2 === 1 ? INSIDE$2 : OUTSIDE$1;
195779
+ return contains2;
195015
195780
  }
195016
195781
  function equal(shape1, shape2) {
195017
195782
  return relate(shape1, shape2).equal();
@@ -200377,23 +201142,23 @@ function computeOverlapDistance(compPoly, boardPoly, componentCenter, componentW
200377
201142
  }
200378
201143
  try {
200379
201144
  const intersection = BooleanOperations.intersect(compPoly, boardPoly);
200380
- let intersectionArea = 0;
201145
+ let intersectionArea2 = 0;
200381
201146
  if (!intersection) {
200382
- intersectionArea = 0;
201147
+ intersectionArea2 = 0;
200383
201148
  } else if (Array.isArray(intersection)) {
200384
- intersectionArea = intersection.reduce((sum, p3) => sum + (typeof p3.area === "function" ? p3.area() : 0), 0);
201149
+ intersectionArea2 = intersection.reduce((sum, p3) => sum + (typeof p3.area === "function" ? p3.area() : 0), 0);
200385
201150
  } else if (typeof intersection.area === "function") {
200386
- intersectionArea = intersection.area();
201151
+ intersectionArea2 = intersection.area();
200387
201152
  } else {
200388
- intersectionArea = 0;
201153
+ intersectionArea2 = 0;
200389
201154
  }
200390
201155
  const compArea = compPoly.area();
200391
- if (intersectionArea > 0 && intersectionArea < compArea) {
200392
- const overlapRatio = 1 - intersectionArea / compArea;
201156
+ if (intersectionArea2 > 0 && intersectionArea2 < compArea) {
201157
+ const overlapRatio = 1 - intersectionArea2 / compArea;
200393
201158
  const compWidth = Math.abs(componentWidth);
200394
201159
  const compHeight = Math.abs(componentHeight);
200395
201160
  return Math.min(compWidth, compHeight) * overlapRatio;
200396
- } else if (intersectionArea === 0) {
201161
+ } else if (intersectionArea2 === 0) {
200397
201162
  return 0.1;
200398
201163
  } else {
200399
201164
  return 0.1;
@@ -237108,8 +237873,8 @@ var isPointInRing = (point6, ring2) => {
237108
237873
  for (const current2 of ring2) {
237109
237874
  if (isPointOnSegment2(point6, previous, current2))
237110
237875
  return true;
237111
- const intersects = current2.y > point6.y !== previous.y > point6.y && point6.x < (previous.x - current2.x) * (point6.y - current2.y) / (previous.y - current2.y) + current2.x;
237112
- if (intersects)
237876
+ const intersects2 = current2.y > point6.y !== previous.y > point6.y && point6.x < (previous.x - current2.x) * (point6.y - current2.y) / (previous.y - current2.y) + current2.x;
237877
+ if (intersects2)
237113
237878
  inside2 = !inside2;
237114
237879
  previous = current2;
237115
237880
  }
@@ -239299,7 +240064,7 @@ var fmtNumber3 = (value) => {
239299
240064
  return value.toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
239300
240065
  };
239301
240066
  var fmtMm22 = (value) => `${value.toFixed(1)}mm`;
239302
- var fmtPercent = (value) => `${fmtNumber3(value * 100)}%`;
240067
+ var fmtPercent2 = (value) => `${fmtNumber3(value * 100)}%`;
239303
240068
  var isCrampedPortPoint = (portPointId) => portPointId?.includes("_cramped") ?? false;
239304
240069
  var clamp012 = (value) => Math.max(0, Math.min(1, value));
239305
240070
  var roundProbability = (value) => Number.parseFloat(value.toFixed(3));
@@ -239376,7 +240141,7 @@ var analyzeGlobalCapacityNodes = (nodes, circuitJson) => {
239376
240141
  const bounds = getBoundsFromNode(node);
239377
240142
  return {
239378
240143
  lineItemType: "CongestedRegion",
239379
- probabilityOfFailure: fmtPercent(getProbabilityOfFailure(node, maxDensity, maxPortPointCount)),
240144
+ probabilityOfFailure: fmtPercent2(getProbabilityOfFailure(node, maxDensity, maxPortPointCount)),
239380
240145
  bounds,
239381
240146
  width: getBoundsWidth(bounds),
239382
240147
  height: getBoundsHeight(bounds),
@@ -239665,6 +240430,634 @@ var registerCheckRouting = (program2) => {
239665
240430
  });
239666
240431
  };
239667
240432
 
240433
+ // node_modules/circuit-json-trace-length-analysis/lib/analyze-circuit-json-trace-length.ts
240434
+ class Trace {
240435
+ id;
240436
+ label;
240437
+ connectionType;
240438
+ connectionTarget;
240439
+ connectionTargetPosition;
240440
+ connectedPins;
240441
+ pinPositions;
240442
+ requirements;
240443
+ points;
240444
+ lengthMm;
240445
+ straightLineDistanceMm;
240446
+ sourceTraceId;
240447
+ displayName;
240448
+ constructor(model) {
240449
+ this.id = model.id;
240450
+ this.label = model.label;
240451
+ this.connectionType = model.connectionType;
240452
+ this.connectionTarget = model.connectionTarget;
240453
+ this.connectionTargetPosition = model.connectionTargetPosition;
240454
+ this.connectedPins = model.connectedPins;
240455
+ this.pinPositions = model.pinPositions;
240456
+ this.requirements = model.requirements;
240457
+ this.points = model.points;
240458
+ this.lengthMm = model.lengthMm;
240459
+ this.straightLineDistanceMm = model.straightLineDistanceMm;
240460
+ this.sourceTraceId = model.sourceTraceId;
240461
+ this.displayName = model.displayName;
240462
+ }
240463
+ toString() {
240464
+ const lines = [
240465
+ `<Trace id="${escapeXml(this.id)}" label="${escapeXml(this.label)}" connectionType="${escapeXml(this.connectionType)}" lengthMm="${formatNumber3(this.lengthMm)}" straightLineDistanceMm="${formatNumber3(this.straightLineDistanceMm)}">`,
240466
+ ` <ConnectedPins>`,
240467
+ ...this.connectedPins.map((pin) => ` <Pin ref="${escapeXml(pin.ref)}" />`),
240468
+ ` </ConnectedPins>`,
240469
+ ` <PinPositions>`,
240470
+ ...this.pinPositions.map((pin) => renderPinPosition(pin, " ")),
240471
+ ` </PinPositions>`,
240472
+ renderConnection(this),
240473
+ renderRequirements(this.requirements, " "),
240474
+ `</Trace>`
240475
+ ];
240476
+ if (this.points.length > 0) {
240477
+ lines.splice(lines.length - 1, 0, ` <Path>`, ...this.points.map((point5) => ` <Point x="${formatNumber3(point5.x)}" y="${formatNumber3(point5.y)}" layer="${escapeXml(point5.layer)}" kind="${escapeXml(point5.kind)}" />`), ` </Path>`);
240478
+ }
240479
+ return lines.join(`
240480
+ `);
240481
+ }
240482
+ }
240483
+
240484
+ class TraceLengthAnalysis {
240485
+ requestedTarget;
240486
+ resolvedTarget;
240487
+ targetKind;
240488
+ totalLengthMm;
240489
+ totalStraightLineDistanceMm;
240490
+ traceCount;
240491
+ #traces;
240492
+ constructor(args) {
240493
+ this.requestedTarget = args.requestedTarget;
240494
+ this.resolvedTarget = args.resolvedTarget;
240495
+ this.targetKind = args.targetKind;
240496
+ this.#traces = [...args.traces];
240497
+ this.totalLengthMm = this.#traces.reduce((sum, trace) => sum + trace.lengthMm, 0);
240498
+ this.totalStraightLineDistanceMm = this.#traces.reduce((sum, trace) => sum + trace.straightLineDistanceMm, 0);
240499
+ this.traceCount = this.#traces.length;
240500
+ }
240501
+ listTraces() {
240502
+ return [...this.#traces];
240503
+ }
240504
+ toString() {
240505
+ const lines = [
240506
+ `<TraceLengthAnalysis requestedTarget="${escapeXml(this.requestedTarget)}" resolvedTarget="${escapeXml(this.resolvedTarget)}" targetKind="${escapeXml(this.targetKind)}" traceCount="${this.traceCount}" totalLengthMm="${formatNumber3(this.totalLengthMm)}" totalStraightLineDistanceMm="${formatNumber3(this.totalStraightLineDistanceMm)}">`,
240507
+ ...this.#traces.flatMap((trace) => trace.toString().split(`
240508
+ `).map((line2) => ` ${line2}`)),
240509
+ `</TraceLengthAnalysis>`
240510
+ ];
240511
+ return lines.join(`
240512
+ `);
240513
+ }
240514
+ }
240515
+ function analyzeCircuitJsonTraceLength(circuitJson, options) {
240516
+ const index = new CircuitJsonIndex(circuitJson);
240517
+ const target = resolveTarget(index, options.targetPinOrNet);
240518
+ const traces = target.kind === "pin" ? collectPinTraces(index, target) : collectNetTraces(index, target);
240519
+ return new TraceLengthAnalysis({
240520
+ requestedTarget: target.requestedTarget,
240521
+ resolvedTarget: target.resolvedTarget,
240522
+ targetKind: target.kind,
240523
+ traces
240524
+ });
240525
+ }
240526
+
240527
+ class CircuitJsonIndex {
240528
+ componentsById = new Map;
240529
+ componentsByName = new Map;
240530
+ portsById = new Map;
240531
+ portsByComponentId = new Map;
240532
+ netsById = new Map;
240533
+ netsByName = new Map;
240534
+ pcbPortsBySourcePortId = new Map;
240535
+ pcbTracesBySourceTraceId = new Map;
240536
+ pcbTracesByConnectionName = new Map;
240537
+ tracesById = new Map;
240538
+ tracesByPortId = new Map;
240539
+ tracesByNetId = new Map;
240540
+ constructor(circuitJson) {
240541
+ for (const item of circuitJson) {
240542
+ switch (item.type) {
240543
+ case "source_component":
240544
+ this.addComponent(item);
240545
+ break;
240546
+ case "source_port":
240547
+ this.addPort(item);
240548
+ break;
240549
+ case "source_net":
240550
+ this.addNet(item);
240551
+ break;
240552
+ case "pcb_port":
240553
+ this.addPcbPort(item);
240554
+ break;
240555
+ case "pcb_trace":
240556
+ this.addPcbTrace(item);
240557
+ break;
240558
+ case "source_trace":
240559
+ this.addTrace(item);
240560
+ break;
240561
+ default:
240562
+ break;
240563
+ }
240564
+ }
240565
+ }
240566
+ getComponentByName(name) {
240567
+ return this.lookupWithCaseFallback(this.componentsByName, name);
240568
+ }
240569
+ getNetByName(name) {
240570
+ return this.lookupWithCaseFallback(this.netsByName, name);
240571
+ }
240572
+ getPcbPort(sourcePortId) {
240573
+ return this.pcbPortsBySourcePortId.get(sourcePortId)?.[0];
240574
+ }
240575
+ getPortPosition(sourcePortId) {
240576
+ const pcbPort = this.getPcbPort(sourcePortId);
240577
+ if (!pcbPort || typeof pcbPort.x !== "number" || typeof pcbPort.y !== "number") {
240578
+ return null;
240579
+ }
240580
+ const layers = normalizeLayers(pcbPort.layers);
240581
+ return {
240582
+ x: pcbPort.x,
240583
+ y: pcbPort.y,
240584
+ layers
240585
+ };
240586
+ }
240587
+ getPortReference(sourcePortId) {
240588
+ const port = this.portsById.get(sourcePortId);
240589
+ if (!port) {
240590
+ return sourcePortId;
240591
+ }
240592
+ const component = this.componentsById.get(port.source_component_id);
240593
+ const componentName = component?.name ?? port.source_component_id;
240594
+ const portName = port.name ?? String(port.pin_number ?? port.source_port_id);
240595
+ return `${componentName}.${portName}`;
240596
+ }
240597
+ getPcbPathPoints(sourceTraceId) {
240598
+ const candidates = dedupePcbTraces([
240599
+ ...this.pcbTracesBySourceTraceId.get(sourceTraceId) ?? [],
240600
+ ...this.pcbTracesByConnectionName.get(sourceTraceId) ?? []
240601
+ ]);
240602
+ const pcbTraceIds = new Set(candidates.map((trace) => trace.pcb_trace_id));
240603
+ if (candidates.length === 0 || pcbTraceIds.size > 1) {
240604
+ return [];
240605
+ }
240606
+ const routePoints = candidates.flatMap((trace) => normalizePcbTraceRoute(trace));
240607
+ return convertPcbTraceRouteToPath(routePoints);
240608
+ }
240609
+ getConnectedPinsForNet(sourceNetId) {
240610
+ const traces = this.tracesByNetId.get(sourceNetId) ?? [];
240611
+ const sourcePortIds = new Set;
240612
+ for (const trace of traces) {
240613
+ for (const sourcePortId of trace.connected_source_port_ids ?? []) {
240614
+ sourcePortIds.add(sourcePortId);
240615
+ }
240616
+ }
240617
+ return [...sourcePortIds].map((sourcePortId) => this.toConnectedPin(sourcePortId));
240618
+ }
240619
+ toConnectedPin(sourcePortId) {
240620
+ const position2 = this.getPortPosition(sourcePortId);
240621
+ return {
240622
+ ref: this.getPortReference(sourcePortId),
240623
+ x: position2?.x ?? null,
240624
+ y: position2?.y ?? null,
240625
+ layers: position2?.layers ?? []
240626
+ };
240627
+ }
240628
+ addComponent(component) {
240629
+ this.componentsById.set(component.source_component_id, component);
240630
+ const name = component.name;
240631
+ if (!name) {
240632
+ return;
240633
+ }
240634
+ this.addLookup(this.componentsByName, name, component);
240635
+ }
240636
+ addPort(port) {
240637
+ this.portsById.set(port.source_port_id, port);
240638
+ this.addLookup(this.portsByComponentId, port.source_component_id, port);
240639
+ }
240640
+ addNet(net) {
240641
+ this.netsById.set(net.source_net_id, net);
240642
+ const name = net.name;
240643
+ if (!name) {
240644
+ return;
240645
+ }
240646
+ this.addLookup(this.netsByName, name, net);
240647
+ }
240648
+ addPcbPort(pcbPort) {
240649
+ this.addLookup(this.pcbPortsBySourcePortId, pcbPort.source_port_id, pcbPort);
240650
+ }
240651
+ addPcbTrace(pcbTrace) {
240652
+ if (pcbTrace.source_trace_id) {
240653
+ this.addLookup(this.pcbTracesBySourceTraceId, pcbTrace.source_trace_id, pcbTrace);
240654
+ }
240655
+ if (pcbTrace.connection_name) {
240656
+ this.addLookup(this.pcbTracesByConnectionName, pcbTrace.connection_name, pcbTrace);
240657
+ }
240658
+ }
240659
+ addTrace(trace) {
240660
+ this.tracesById.set(trace.source_trace_id, trace);
240661
+ for (const sourcePortId of trace.connected_source_port_ids ?? []) {
240662
+ this.addLookup(this.tracesByPortId, sourcePortId, trace);
240663
+ }
240664
+ for (const sourceNetId of trace.connected_source_net_ids ?? []) {
240665
+ this.addLookup(this.tracesByNetId, sourceNetId, trace);
240666
+ }
240667
+ }
240668
+ addLookup(map, key, value) {
240669
+ const existing = map.get(key) ?? [];
240670
+ existing.push(value);
240671
+ map.set(key, existing);
240672
+ }
240673
+ lookupWithCaseFallback(map, key) {
240674
+ const exactMatch = map.get(key);
240675
+ if (exactMatch?.length) {
240676
+ return exactMatch;
240677
+ }
240678
+ const loweredKey = key.toLowerCase();
240679
+ const fuzzyMatches = [];
240680
+ for (const [candidateKey, values] of map.entries()) {
240681
+ if (candidateKey.toLowerCase() === loweredKey) {
240682
+ fuzzyMatches.push(...values);
240683
+ }
240684
+ }
240685
+ return fuzzyMatches;
240686
+ }
240687
+ }
240688
+ function resolveTarget(index, targetPinOrNet) {
240689
+ if (targetPinOrNet.includes(".")) {
240690
+ if (targetPinOrNet.startsWith("net.")) {
240691
+ const netName = targetPinOrNet.slice("net.".length);
240692
+ const net = resolveNetByName(index, netName);
240693
+ return {
240694
+ kind: "net",
240695
+ requestedTarget: targetPinOrNet,
240696
+ resolvedTarget: `net.${net.name ?? net.source_net_id}`,
240697
+ net
240698
+ };
240699
+ }
240700
+ return resolvePinTarget(index, targetPinOrNet);
240701
+ }
240702
+ const matchingNet = resolveOptionalNetByName(index, targetPinOrNet);
240703
+ if (matchingNet) {
240704
+ const resolvedTarget = `net.${matchingNet.name ?? matchingNet.source_net_id}`;
240705
+ console.log(`inferring ${resolvedTarget}`);
240706
+ return {
240707
+ kind: "net",
240708
+ requestedTarget: targetPinOrNet,
240709
+ resolvedTarget,
240710
+ net: matchingNet
240711
+ };
240712
+ }
240713
+ const matchingPin = resolveOptionalPinByLabel(index, targetPinOrNet);
240714
+ if (matchingPin) {
240715
+ const resolvedTarget = index.getPortReference(matchingPin.source_port_id);
240716
+ console.log(`inferring ${resolvedTarget}`);
240717
+ return {
240718
+ kind: "pin",
240719
+ requestedTarget: targetPinOrNet,
240720
+ resolvedTarget,
240721
+ port: matchingPin
240722
+ };
240723
+ }
240724
+ throw new Error(`Unable to resolve target "${targetPinOrNet}" to a pin or net`);
240725
+ }
240726
+ function resolvePinTarget(index, targetPinOrNet) {
240727
+ const [componentName, ...pinParts] = targetPinOrNet.split(".");
240728
+ const pinLabelOrNumber = pinParts.join(".");
240729
+ if (!componentName || !pinLabelOrNumber) {
240730
+ throw new Error(`Invalid pin target "${targetPinOrNet}"`);
240731
+ }
240732
+ const components = index.getComponentByName(componentName);
240733
+ if (components.length !== 1) {
240734
+ throw new Error(components.length === 0 ? `Unable to find component "${componentName}"` : `Component "${componentName}" is ambiguous`);
240735
+ }
240736
+ const component = components[0];
240737
+ if (!component) {
240738
+ throw new Error(`Unable to find component "${componentName}"`);
240739
+ }
240740
+ const ports = index.portsByComponentId.get(component.source_component_id) ?? [];
240741
+ const matchingPorts = ports.filter((port2) => portMatchesLabel(port2, pinLabelOrNumber));
240742
+ if (matchingPorts.length !== 1) {
240743
+ throw new Error(matchingPorts.length === 0 ? `Unable to find pin "${pinLabelOrNumber}" on ${componentName}` : `Pin "${pinLabelOrNumber}" on ${componentName} is ambiguous`);
240744
+ }
240745
+ const port = matchingPorts[0];
240746
+ if (!port) {
240747
+ throw new Error(`Unable to find pin "${pinLabelOrNumber}" on ${componentName}`);
240748
+ }
240749
+ return {
240750
+ kind: "pin",
240751
+ requestedTarget: targetPinOrNet,
240752
+ resolvedTarget: index.getPortReference(port.source_port_id),
240753
+ port
240754
+ };
240755
+ }
240756
+ function resolveOptionalPinByLabel(index, pinLabelOrNumber) {
240757
+ const matchingPorts = [...index.portsById.values()].filter((port) => portMatchesLabel(port, pinLabelOrNumber));
240758
+ if (matchingPorts.length !== 1) {
240759
+ return null;
240760
+ }
240761
+ return matchingPorts[0] ?? null;
240762
+ }
240763
+ function resolveNetByName(index, netName) {
240764
+ const matchingNets = index.getNetByName(netName);
240765
+ if (matchingNets.length !== 1) {
240766
+ throw new Error(matchingNets.length === 0 ? `Unable to find net "${netName}"` : `Net "${netName}" is ambiguous`);
240767
+ }
240768
+ const net = matchingNets[0];
240769
+ if (!net) {
240770
+ throw new Error(`Unable to find net "${netName}"`);
240771
+ }
240772
+ return net;
240773
+ }
240774
+ function resolveOptionalNetByName(index, netName) {
240775
+ const matchingNets = index.getNetByName(netName);
240776
+ if (matchingNets.length !== 1) {
240777
+ return null;
240778
+ }
240779
+ return matchingNets[0] ?? null;
240780
+ }
240781
+ function collectPinTraces(index, target) {
240782
+ const traces = index.tracesByPortId.get(target.port.source_port_id) ?? [];
240783
+ return traces.flatMap((trace) => {
240784
+ const netIds = trace.connected_source_net_ids ?? [];
240785
+ if (netIds.length > 0) {
240786
+ return netIds.map((sourceNetId) => createNetTraceModel(index, {
240787
+ trace,
240788
+ sourceNetId,
240789
+ focusSourcePortId: target.port.source_port_id
240790
+ }));
240791
+ }
240792
+ return [
240793
+ createDirectTraceModel(index, {
240794
+ trace,
240795
+ focusSourcePortId: target.port.source_port_id
240796
+ })
240797
+ ];
240798
+ }).sort(compareTraceModels).map((model) => new Trace(model));
240799
+ }
240800
+ function collectNetTraces(index, target) {
240801
+ const traces = index.tracesByNetId.get(target.net.source_net_id) ?? [];
240802
+ return traces.flatMap((trace) => {
240803
+ const connectedSourcePortIds = trace.connected_source_port_ids ?? [];
240804
+ if (connectedSourcePortIds.length === 0) {
240805
+ return [];
240806
+ }
240807
+ return connectedSourcePortIds.map((focusSourcePortId) => createNetTraceModel(index, {
240808
+ trace,
240809
+ sourceNetId: target.net.source_net_id,
240810
+ focusSourcePortId
240811
+ }));
240812
+ }).sort(compareTraceModels).map((model) => new Trace(model));
240813
+ }
240814
+ function createDirectTraceModel(index, args) {
240815
+ const connectedSourcePortIds = args.trace.connected_source_port_ids ?? [];
240816
+ if (connectedSourcePortIds.length === 0) {
240817
+ throw new Error(`Trace ${args.trace.source_trace_id} has no connected ports`);
240818
+ }
240819
+ const orderedPortIds = prioritizeFocus(connectedSourcePortIds, args.focusSourcePortId);
240820
+ const firstPortId = orderedPortIds[0];
240821
+ const secondPortId = orderedPortIds[1] ?? orderedPortIds[0];
240822
+ if (!firstPortId || !secondPortId) {
240823
+ throw new Error(`Trace ${args.trace.source_trace_id} is missing connected ports`);
240824
+ }
240825
+ const firstPosition = requirePortPosition(index, firstPortId);
240826
+ const secondPosition = requirePortPosition(index, secondPortId);
240827
+ const secondLayer = primaryLayer(secondPosition.layers);
240828
+ const connectedPins = orderedPortIds.map((sourcePortId) => index.toConnectedPin(sourcePortId));
240829
+ const connectionTarget4 = index.getPortReference(secondPortId);
240830
+ const straightLineDistanceMm = distanceBetween(firstPosition, secondPosition);
240831
+ const points = index.getPcbPathPoints(args.trace.source_trace_id);
240832
+ const lengthMm = points.length > 1 ? measurePathLength(points) : straightLineDistanceMm;
240833
+ return {
240834
+ id: args.trace.source_trace_id,
240835
+ label: `${index.getPortReference(firstPortId)} -> ${connectionTarget4}`,
240836
+ connectionType: "direct connection",
240837
+ connectionTarget: connectionTarget4,
240838
+ connectionTargetPosition: {
240839
+ x: secondPosition.x,
240840
+ y: secondPosition.y,
240841
+ layer: secondLayer
240842
+ },
240843
+ connectedPins,
240844
+ pinPositions: connectedPins,
240845
+ requirements: {
240846
+ maxLengthMm: typeof args.trace.max_length === "number" ? args.trace.max_length : null
240847
+ },
240848
+ points,
240849
+ lengthMm,
240850
+ straightLineDistanceMm,
240851
+ sourceTraceId: args.trace.source_trace_id,
240852
+ displayName: args.trace.display_name ?? null
240853
+ };
240854
+ }
240855
+ function createNetTraceModel(index, args) {
240856
+ const net = index.netsById.get(args.sourceNetId);
240857
+ if (!net) {
240858
+ throw new Error(`Unable to find net ${args.sourceNetId}`);
240859
+ }
240860
+ const focusPosition = requirePortPosition(index, args.focusSourcePortId);
240861
+ const focusLayer = primaryLayer(focusPosition.layers);
240862
+ const connectedPins = index.getConnectedPinsForNet(args.sourceNetId).sort((a2, b) => a2.ref.localeCompare(b.ref));
240863
+ const hub = inferNetHub(connectedPins, focusLayer);
240864
+ const focusPin = index.toConnectedPin(args.focusSourcePortId);
240865
+ const points = index.getPcbPathPoints(args.trace.source_trace_id);
240866
+ const straightLineDistanceMm = distanceBetween(focusPosition, hub);
240867
+ const lengthMm = points.length > 1 ? measurePathLength(points) : straightLineDistanceMm;
240868
+ return {
240869
+ id: `${args.trace.source_trace_id}:${args.focusSourcePortId}`,
240870
+ label: `${focusPin.ref} -> net.${net.name ?? net.source_net_id}`,
240871
+ connectionType: "via net",
240872
+ connectionTarget: `net.${net.name ?? net.source_net_id}`,
240873
+ connectionTargetPosition: hub,
240874
+ connectedPins: prioritizePin(focusPin, connectedPins),
240875
+ pinPositions: prioritizePin(focusPin, connectedPins),
240876
+ requirements: {
240877
+ maxLengthMm: typeof args.trace.max_length === "number" ? args.trace.max_length : null
240878
+ },
240879
+ points,
240880
+ lengthMm,
240881
+ straightLineDistanceMm,
240882
+ sourceTraceId: args.trace.source_trace_id,
240883
+ displayName: args.trace.display_name ?? null
240884
+ };
240885
+ }
240886
+ function requirePortPosition(index, sourcePortId) {
240887
+ const position2 = index.getPortPosition(sourcePortId);
240888
+ if (!position2) {
240889
+ throw new Error(`Unable to analyze ${index.getPortReference(sourcePortId)} because it has no pcb_port position`);
240890
+ }
240891
+ return position2;
240892
+ }
240893
+ function inferNetHub(connectedPins, fallbackLayer) {
240894
+ const positionedPins = connectedPins.filter((pin) => typeof pin.x === "number" && typeof pin.y === "number");
240895
+ if (positionedPins.length === 0) {
240896
+ return {
240897
+ x: 0,
240898
+ y: 0,
240899
+ layer: fallbackLayer
240900
+ };
240901
+ }
240902
+ const x3 = positionedPins.reduce((sum, pin) => sum + pin.x, 0) / positionedPins.length;
240903
+ const y4 = positionedPins.reduce((sum, pin) => sum + pin.y, 0) / positionedPins.length;
240904
+ const layerCounts = new Map;
240905
+ for (const pin of positionedPins) {
240906
+ const layer2 = pin.layers[0] ?? fallbackLayer;
240907
+ layerCounts.set(layer2, (layerCounts.get(layer2) ?? 0) + 1);
240908
+ }
240909
+ const sortedLayers = [...layerCounts.entries()].sort((left, right) => {
240910
+ if (right[1] !== left[1]) {
240911
+ return right[1] - left[1];
240912
+ }
240913
+ return left[0].localeCompare(right[0]);
240914
+ });
240915
+ const layer = sortedLayers[0]?.[0] ?? fallbackLayer;
240916
+ return { x: x3, y: y4, layer };
240917
+ }
240918
+ function measurePathLength(points) {
240919
+ let lengthMm = 0;
240920
+ for (let index = 1;index < points.length; index += 1) {
240921
+ const previous = points[index - 1];
240922
+ const current2 = points[index];
240923
+ if (!previous || !current2) {
240924
+ continue;
240925
+ }
240926
+ lengthMm += distanceBetween(previous, current2);
240927
+ }
240928
+ return lengthMm;
240929
+ }
240930
+ function dedupePcbTraces(pcbTraces) {
240931
+ const seen = new Set;
240932
+ const deduped = [];
240933
+ for (const pcbTrace of pcbTraces) {
240934
+ if (seen.has(pcbTrace)) {
240935
+ continue;
240936
+ }
240937
+ seen.add(pcbTrace);
240938
+ deduped.push(pcbTrace);
240939
+ }
240940
+ return deduped;
240941
+ }
240942
+ function normalizePcbTraceRoute(pcbTrace) {
240943
+ if (!Array.isArray(pcbTrace.route)) {
240944
+ return [];
240945
+ }
240946
+ return pcbTrace.route.filter((point5) => typeof point5.x === "number" && typeof point5.y === "number").map((point5) => ({
240947
+ ...point5,
240948
+ layer: typeof point5.layer === "string" && point5.layer.length > 0 ? point5.layer : typeof pcbTrace.layer === "string" && pcbTrace.layer.length > 0 ? pcbTrace.layer : "top"
240949
+ }));
240950
+ }
240951
+ function convertPcbTraceRouteToPath(routePoints) {
240952
+ const deduped = routePoints.filter((point5, index) => {
240953
+ const previous = routePoints[index - 1];
240954
+ return !previous || previous.x !== point5.x || previous.y !== point5.y || previous.layer !== point5.layer;
240955
+ });
240956
+ return deduped.map((point5, index) => ({
240957
+ x: point5.x,
240958
+ y: point5.y,
240959
+ layer: point5.layer,
240960
+ kind: classifyPcbTracePoint(point5, index, deduped.length)
240961
+ }));
240962
+ }
240963
+ function classifyPcbTracePoint(point5, index, pointCount) {
240964
+ if (point5.route_type === "via") {
240965
+ return "via";
240966
+ }
240967
+ if (index === 0 || index === pointCount - 1 || point5.start_pcb_port_id || point5.end_pcb_port_id) {
240968
+ return "endpoint";
240969
+ }
240970
+ return "track";
240971
+ }
240972
+ function renderPinPosition(pin, indent) {
240973
+ if (typeof pin.x !== "number" || typeof pin.y !== "number") {
240974
+ return `${indent}<Pin ref="${escapeXml(pin.ref)}" position="unavailable" />`;
240975
+ }
240976
+ const layers = pin.layers.length > 0 ? pin.layers.join(",") : "unknown";
240977
+ return `${indent}<Pin ref="${escapeXml(pin.ref)}" x="${formatNumber3(pin.x)}" y="${formatNumber3(pin.y)}" layers="${escapeXml(layers)}" />`;
240978
+ }
240979
+ function renderConnection(trace) {
240980
+ if (trace.connectionTargetPosition) {
240981
+ return ` <Connection kind="${escapeXml(trace.connectionType)}" target="${escapeXml(trace.connectionTarget)}" x="${formatNumber3(trace.connectionTargetPosition.x)}" y="${formatNumber3(trace.connectionTargetPosition.y)}" layer="${escapeXml(trace.connectionTargetPosition.layer)}" />`;
240982
+ }
240983
+ return ` <Connection kind="${escapeXml(trace.connectionType)}" target="${escapeXml(trace.connectionTarget)}" />`;
240984
+ }
240985
+ function renderRequirements(requirements, indent) {
240986
+ if (typeof requirements.maxLengthMm === "number") {
240987
+ return [
240988
+ `${indent}<TraceRequirements>`,
240989
+ `${indent} <MaxLengthMm>${formatNumber3(requirements.maxLengthMm)}</MaxLengthMm>`,
240990
+ `${indent}</TraceRequirements>`
240991
+ ].join(`
240992
+ `);
240993
+ }
240994
+ return `${indent}<TraceRequirements none />`;
240995
+ }
240996
+ function portMatchesLabel(port, label) {
240997
+ const loweredLabel = label.toLowerCase();
240998
+ const portName = port.name?.toLowerCase();
240999
+ const pinNumber = String(port.pin_number ?? "").toLowerCase();
241000
+ const portHints2 = (port.port_hints ?? []).map((hint) => hint.toLowerCase());
241001
+ return portName === loweredLabel || pinNumber === loweredLabel || portHints2.includes(loweredLabel);
241002
+ }
241003
+ function prioritizeFocus(portIds, focusSourcePortId) {
241004
+ const remainder = portIds.filter((portId) => portId !== focusSourcePortId);
241005
+ return [focusSourcePortId, ...remainder];
241006
+ }
241007
+ function prioritizePin(focusPin, pins) {
241008
+ const remainder = pins.filter((pin) => pin.ref !== focusPin.ref);
241009
+ return [focusPin, ...remainder];
241010
+ }
241011
+ function compareTraceModels(left, right) {
241012
+ return left.label.localeCompare(right.label);
241013
+ }
241014
+ function normalizeLayers(layers) {
241015
+ if (!Array.isArray(layers)) {
241016
+ return ["top"];
241017
+ }
241018
+ const normalizedLayers = layers.filter((layer) => typeof layer === "string" && layer.length > 0);
241019
+ return normalizedLayers.length > 0 ? normalizedLayers : ["top"];
241020
+ }
241021
+ function primaryLayer(layers) {
241022
+ return layers[0] ?? "top";
241023
+ }
241024
+ function distanceBetween(left, right) {
241025
+ return Math.hypot(right.x - left.x, right.y - left.y);
241026
+ }
241027
+ function formatNumber3(value) {
241028
+ return value.toFixed(2);
241029
+ }
241030
+ function escapeXml(value) {
241031
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
241032
+ }
241033
+ // cli/check/trace-length/register.ts
241034
+ var checkTraceLength = async (pinOrNetRef, file) => {
241035
+ const resolvedInputFilePath = await resolveCheckInputFilePath(file);
241036
+ const circuitJson = await getCircuitJsonForCheck({
241037
+ filePath: resolvedInputFilePath,
241038
+ platformConfig: {
241039
+ pcbDisabled: false,
241040
+ routingDisabled: false
241041
+ },
241042
+ allowPrebuiltCircuitJson: true
241043
+ });
241044
+ const analysis = analyzeCircuitJsonTraceLength(circuitJson, {
241045
+ targetPinOrNet: pinOrNetRef
241046
+ });
241047
+ return analysis.toString();
241048
+ };
241049
+ var registerCheckTraceLength = (program2) => {
241050
+ program2.commands.find((c3) => c3.name() === "check").command("trace-length").description("Analyze trace length for a pin or net").argument("<pinOrNetRef>", "Pin or net target to analyze").argument("[file]", "Path to the entry file").action(async (pinOrNetRef, file) => {
241051
+ try {
241052
+ const output = await checkTraceLength(pinOrNetRef, file);
241053
+ console.log(output);
241054
+ } catch (error) {
241055
+ console.error(error instanceof Error ? error.message : String(error));
241056
+ process.exit(1);
241057
+ }
241058
+ });
241059
+ };
241060
+
239668
241061
  // cli/clone/register.ts
239669
241062
  import * as fs43 from "node:fs";
239670
241063
  import * as path45 from "node:path";
@@ -270695,7 +272088,7 @@ var registerSetup = (program3) => {
270695
272088
  };
270696
272089
 
270697
272090
  // lib/shared/result-to-table.ts
270698
- var formatNumber3 = (n3) => {
272091
+ var formatNumber5 = (n3) => {
270699
272092
  return n3.toExponential(6);
270700
272093
  };
270701
272094
  function formatRows(rows) {
@@ -270727,7 +272120,7 @@ var resultToTable = (result) => {
270727
272120
  for (let i2 = 0;i2 < result.numPoints; i2++) {
270728
272121
  const row = [
270729
272122
  i2.toString(),
270730
- ...uniqueData.map((d3) => formatNumber3(d3.values[i2]))
272123
+ ...uniqueData.map((d3) => formatNumber5(d3.values[i2]))
270731
272124
  ];
270732
272125
  dataRows2.push(row);
270733
272126
  }
@@ -270742,8 +272135,8 @@ var resultToTable = (result) => {
270742
272135
  const row = [
270743
272136
  i2.toString(),
270744
272137
  ...uniqueData.flatMap((d3) => [
270745
- formatNumber3(d3.values[i2].real),
270746
- formatNumber3(d3.values[i2].img)
272138
+ formatNumber5(d3.values[i2].real),
272139
+ formatNumber5(d3.values[i2].img)
270747
272140
  ])
270748
272141
  ];
270749
272142
  dataRows.push(row);
@@ -274159,6 +275552,7 @@ registerCheckPinSpecification(program2);
274159
275552
  registerCheckPlacement(program2);
274160
275553
  registerCheckRoutingDifficulty(program2);
274161
275554
  registerCheckRouting(program2);
275555
+ registerCheckTraceLength(program2);
274162
275556
  registerRegistry(program2);
274163
275557
  registerRegistryPackages(program2);
274164
275558
  registerRegistryPackagesCreate(program2);