@tscircuit/cli 0.1.1174 → 0.1.1176

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.
@@ -11877,7 +11877,7 @@ var EMPTY_IMAGE_FORMAT_SELECTION = {
11877
11877
  };
11878
11878
  var hasAnyImageFormatSelected = (selection) => selection.threeDPngs || selection.pcbPngs || selection.pcbSvgs || selection.schematicSvgs;
11879
11879
  var hasNewOutputFlags = (options) => Boolean(options?.pngs || options?.pcbPng || options?.svgs || options?.pcbSvgs || options?.schematicSvgs);
11880
- var hasEstablishedOutputFlags = (options) => Boolean(options?.["3d"] || options?.pcbOnly || options?.schematicOnly);
11880
+ var hasEstablishedOutputFlags = (options) => Boolean(options?.["3d"] || options?.["3dPng"] || options?.pcbOnly || options?.schematicOnly);
11881
11881
  var resolveImageFormatSelection = (options) => {
11882
11882
  const hasNewFlags = hasNewOutputFlags(options);
11883
11883
  const hasEstablishedFlags = hasEstablishedOutputFlags(options);
@@ -11890,7 +11890,7 @@ var resolveImageFormatSelection = (options) => {
11890
11890
  }
11891
11891
  if (!hasNewFlags && hasEstablishedFlags) {
11892
11892
  const selection2 = {
11893
- threeDPngs: Boolean(options?.["3d"]),
11893
+ threeDPngs: Boolean(options?.["3d"] || options?.["3dPng"]),
11894
11894
  pcbPngs: false,
11895
11895
  pcbSvgs: true,
11896
11896
  schematicSvgs: true
@@ -11919,7 +11919,7 @@ var resolveImageFormatSelection = (options) => {
11919
11919
  if (options?.schematicSvgs) {
11920
11920
  selection.schematicSvgs = true;
11921
11921
  }
11922
- if (options?.pngs || options?.["3d"]) {
11922
+ if (options?.pngs || options?.["3d"] || options?.["3dPng"]) {
11923
11923
  selection.threeDPngs = true;
11924
11924
  }
11925
11925
  if (options?.pcbOnly && !options?.schematicOnly) {
package/dist/cli/main.js CHANGED
@@ -98409,7 +98409,7 @@ var import_perfect_cli = __toESM2(require_dist2(), 1);
98409
98409
  // lib/getVersion.ts
98410
98410
  import { createRequire as createRequire2 } from "node:module";
98411
98411
  // package.json
98412
- var version = "0.1.1172";
98412
+ var version = "0.1.1175";
98413
98413
  var package_default = {
98414
98414
  name: "@tscircuit/cli",
98415
98415
  version,
@@ -98421,7 +98421,7 @@ var package_default = {
98421
98421
  devDependencies: {
98422
98422
  "@babel/standalone": "^7.26.9",
98423
98423
  "@biomejs/biome": "^1.9.4",
98424
- "@tscircuit/circuit-json-placement-analysis": "^0.0.4",
98424
+ "@tscircuit/circuit-json-placement-analysis": "^0.0.5",
98425
98425
  "@tscircuit/circuit-json-routing-analysis": "^0.0.1",
98426
98426
  "@tscircuit/fake-snippets": "^0.0.182",
98427
98427
  "@tscircuit/file-server": "^0.0.32",
@@ -108576,7 +108576,7 @@ var EMPTY_IMAGE_FORMAT_SELECTION = {
108576
108576
  };
108577
108577
  var hasAnyImageFormatSelected = (selection) => selection.threeDPngs || selection.pcbPngs || selection.pcbSvgs || selection.schematicSvgs;
108578
108578
  var hasNewOutputFlags = (options) => Boolean(options?.pngs || options?.pcbPng || options?.svgs || options?.pcbSvgs || options?.schematicSvgs);
108579
- var hasEstablishedOutputFlags = (options) => Boolean(options?.["3d"] || options?.pcbOnly || options?.schematicOnly);
108579
+ var hasEstablishedOutputFlags = (options) => Boolean(options?.["3d"] || options?.["3dPng"] || options?.pcbOnly || options?.schematicOnly);
108580
108580
  var resolveImageFormatSelection = (options) => {
108581
108581
  const hasNewFlags = hasNewOutputFlags(options);
108582
108582
  const hasEstablishedFlags = hasEstablishedOutputFlags(options);
@@ -108589,7 +108589,7 @@ var resolveImageFormatSelection = (options) => {
108589
108589
  }
108590
108590
  if (!hasNewFlags && hasEstablishedFlags) {
108591
108591
  const selection2 = {
108592
- threeDPngs: Boolean(options?.["3d"]),
108592
+ threeDPngs: Boolean(options?.["3d"] || options?.["3dPng"]),
108593
108593
  pcbPngs: false,
108594
108594
  pcbSvgs: true,
108595
108595
  schematicSvgs: true
@@ -108618,7 +108618,7 @@ var resolveImageFormatSelection = (options) => {
108618
108618
  if (options?.schematicSvgs) {
108619
108619
  selection.schematicSvgs = true;
108620
108620
  }
108621
- if (options?.pngs || options?.["3d"]) {
108621
+ if (options?.pngs || options?.["3d"] || options?.["3dPng"]) {
108622
108622
  selection.threeDPngs = true;
108623
108623
  }
108624
108624
  if (options?.pcbOnly && !options?.schematicOnly) {
@@ -109376,7 +109376,7 @@ var getOutputDirName = (relativePath) => {
109376
109376
  return relativePath.replace(/(\.board|\.circuit)?\.tsx$/, "").replace(/\.circuit\.json$/, "");
109377
109377
  };
109378
109378
  var registerBuild = (program2) => {
109379
- program2.command("build").description("Run tscircuit eval and output circuit json").argument("[file]", "Path to the entry file").option("--ci", "Run install and optional prebuild/build commands (or default CI build)").option("--ignore-errors", "Do not exit with code 1 on errors").option("--ignore-warnings", "Do not log warnings").option("--ignore-netlist-drc", "Ignore netlist DRC errors/warnings").option("--ignore-pin-specification-drc", "Ignore pin-specification DRC errors/warnings").option("--ignore-placement-drc", "Ignore placement DRC errors/warnings").option("--ignore-routing-drc", "Ignore routing DRC errors/warnings").option("--ignore-config", "Ignore options from tscircuit.config.json").option("--disable-pcb", "Disable PCB outputs").option("--routing-disabled", "Disable routing during circuit generation").option("--disable-parts-engine", "Disable the parts engine").option("--site", "Generate a static site in the dist directory").option("--transpile", "Transpile the entry file to JavaScript").option("--preview-images", "Generate preview images in the dist directory").option("--all-images", "Generate preview images for every successful build output").option("--pngs", "Generate PNG outputs during build generation").option("--pcb-png", "Generate PCB PNG outputs during build generation").option("--svgs", "Generate SVG outputs during build generation").option("--pcb-svgs", "Generate PCB SVG outputs during build generation").option("--schematic-svgs", "Generate schematic SVG outputs during build generation").option("--3d", "Generate 3D PNG outputs during build generation").option("--pcb-only", "Generate only PCB SVG outputs during build generation").option("--schematic-only", "Generate only schematic SVG outputs during build generation").option("--kicad-project", "Generate KiCad project directories for each successful build output").option("--kicad-project-zip", "Generate a zipped KiCad project for each successful build output").option("--kicad-library", "Generate KiCad library in dist/kicad-library").option("--kicad-library-name <name>", "Specify the name of the KiCad library").option("--preview-gltf", "Generate a GLTF file from the preview entrypoint").option("--glbs", "Generate GLB 3D model files for every successful build").option("--profile", "Log per-circuit circuit.json generation time during build").option("--kicad-pcm", "Generate KiCad PCM (Plugin and Content Manager) assets in dist/pcm").option("--use-cdn-javascript", "Use CDN-hosted JavaScript instead of bundled standalone file for --site").option("--concurrency <number>", "Number of files to build in parallel (default: 1)", "1").option("--inject-props <json>", "Inject JSON props into the built file's default export").option("--inject-props-file <path>", "Inject JSON props from a file into the built file's default export").action(async (file, options) => {
109379
+ program2.command("build").description("Run tscircuit eval and output circuit json").argument("[file]", "Path to the entry file").option("--ci", "Run install and optional prebuild/build commands (or default CI build)").option("--ignore-errors", "Do not exit with code 1 on errors").option("--ignore-warnings", "Do not log warnings").option("--ignore-netlist-drc", "Ignore netlist DRC errors/warnings").option("--ignore-pin-specification-drc", "Ignore pin-specification DRC errors/warnings").option("--ignore-placement-drc", "Ignore placement DRC errors/warnings").option("--ignore-routing-drc", "Ignore routing DRC errors/warnings").option("--ignore-config", "Ignore options from tscircuit.config.json").option("--disable-pcb", "Disable PCB outputs").option("--routing-disabled", "Disable routing during circuit generation").option("--disable-parts-engine", "Disable the parts engine").option("--site", "Generate a static site in the dist directory").option("--transpile", "Transpile the entry file to JavaScript").option("--preview-images", "Generate preview images in the dist directory").option("--all-images", "Generate preview images for every successful build output").option("--pngs", "Generate PNG outputs during build generation").option("--pcb-png", "Generate PCB PNG outputs during build generation").option("--svgs", "Generate SVG outputs during build generation").option("--pcb-svgs", "Generate PCB SVG outputs during build generation").option("--schematic-svgs", "Generate schematic SVG outputs during build generation").option("--3d-png", "Generate 3D PNG outputs during build generation").option("--3d", "Generate 3D PNG outputs during build generation").option("--pcb-only", "Generate only PCB SVG outputs during build generation").option("--schematic-only", "Generate only schematic SVG outputs during build generation").option("--kicad-project", "Generate KiCad project directories for each successful build output").option("--kicad-project-zip", "Generate a zipped KiCad project for each successful build output").option("--kicad-library", "Generate KiCad library in dist/kicad-library").option("--kicad-library-name <name>", "Specify the name of the KiCad library").option("--preview-gltf", "Generate a GLTF file from the preview entrypoint").option("--glbs", "Generate GLB 3D model files for every successful build").option("--profile", "Log per-circuit circuit.json generation time during build").option("--kicad-pcm", "Generate KiCad PCM (Plugin and Content Manager) assets in dist/pcm").option("--use-cdn-javascript", "Use CDN-hosted JavaScript instead of bundled standalone file for --site").option("--concurrency <number>", "Number of files to build in parallel (default: 1)", "1").option("--inject-props <json>", "Inject JSON props into the built file's default export").option("--inject-props-file <path>", "Inject JSON props from a file into the built file's default export").action(async (file, options) => {
109380
109380
  try {
109381
109381
  const transpileExplicitlyRequested = options?.transpile === true;
109382
109382
  const resolvedRoot = path39.resolve(process.cwd());
@@ -109830,6 +109830,7 @@ var registerBuild = (program2) => {
109830
109830
  resolvedOptions?.svgs && "svgs",
109831
109831
  resolvedOptions?.pcbSvgs && "pcb-svgs",
109832
109832
  resolvedOptions?.schematicSvgs && "schematic-svgs",
109833
+ resolvedOptions?.["3dPng"] && "3d-png",
109833
109834
  resolvedOptions?.["3d"] && "3d",
109834
109835
  resolvedOptions?.pcbOnly && "pcb-only",
109835
109836
  resolvedOptions?.schematicOnly && "schematic-only",
@@ -110750,6 +110751,673 @@ var analyzeComponentPlacement = (circuitJson, componentName) => {
110750
110751
  `)
110751
110752
  };
110752
110753
  };
110754
+ var TOP_ISSUE_LIMIT = 5;
110755
+ var CENTER_ANCHOR2 = "center";
110756
+ var ISSUE_TYPE_ORDER = [
110757
+ "pad_overlap",
110758
+ "off_board",
110759
+ "courtyard_collision",
110760
+ "connector_body_intrusion",
110761
+ "footprint_intrusion"
110762
+ ];
110763
+ var toNumber22 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
110764
+ var fmtNumber2 = (value) => {
110765
+ if (Number.isInteger(value))
110766
+ return String(value);
110767
+ return value.toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
110768
+ };
110769
+ var fmtMm2 = (value) => `${fmtNumber2(value)}mm`;
110770
+ var getBoundsFromCenterAndSize = (centerX, centerY, width, height) => ({
110771
+ min_x: centerX - width / 2,
110772
+ max_x: centerX + width / 2,
110773
+ min_y: centerY - height / 2,
110774
+ max_y: centerY + height / 2,
110775
+ width,
110776
+ height
110777
+ });
110778
+ var getBoundsFromPoints = (points) => {
110779
+ const xs = [];
110780
+ const ys = [];
110781
+ for (const point of points) {
110782
+ const x = toNumber22(point.x);
110783
+ const y = toNumber22(point.y);
110784
+ if (x === null || y === null)
110785
+ continue;
110786
+ xs.push(x);
110787
+ ys.push(y);
110788
+ }
110789
+ if (xs.length === 0 || ys.length === 0)
110790
+ return null;
110791
+ const min_x = Math.min(...xs);
110792
+ const max_x = Math.max(...xs);
110793
+ const min_y = Math.min(...ys);
110794
+ const max_y = Math.max(...ys);
110795
+ return {
110796
+ min_x,
110797
+ max_x,
110798
+ min_y,
110799
+ max_y,
110800
+ width: max_x - min_x,
110801
+ height: max_y - min_y
110802
+ };
110803
+ };
110804
+ var getOverlap = (a, b) => {
110805
+ const overlapX = Math.min(a.max_x, b.max_x) - Math.max(a.min_x, b.min_x);
110806
+ const overlapY = Math.min(a.max_y, b.max_y) - Math.max(a.min_y, b.min_y);
110807
+ if (overlapX <= 0 || overlapY <= 0)
110808
+ return null;
110809
+ return {
110810
+ overlapX,
110811
+ overlapY,
110812
+ clearance: -Math.min(overlapX, overlapY)
110813
+ };
110814
+ };
110815
+ var layersIntersect = (a, b) => {
110816
+ const bSet = new Set(b);
110817
+ return a.some((layer) => bSet.has(layer));
110818
+ };
110819
+ var getArea = (bounds) => {
110820
+ if (!bounds)
110821
+ return Number.POSITIVE_INFINITY;
110822
+ return bounds.width * bounds.height;
110823
+ };
110824
+ var stripNumericSuffix = (name) => {
110825
+ const stripped = name.replace(/\d+$/, "");
110826
+ return stripped.length >= 2 ? stripped : name;
110827
+ };
110828
+ var getSummaryLabel = (type, count) => {
110829
+ if (count === 0)
110830
+ return null;
110831
+ switch (type) {
110832
+ case "pad_overlap":
110833
+ return `${count} pad overlap${count === 1 ? "" : "s"}`;
110834
+ case "off_board":
110835
+ return `${count} off-board`;
110836
+ case "courtyard_collision":
110837
+ return `${count} courtyard collision${count === 1 ? "" : "s"}`;
110838
+ case "connector_body_intrusion":
110839
+ return `${count} connector-body intrusion${count === 1 ? "" : "s"}`;
110840
+ case "footprint_intrusion":
110841
+ return `${count} footprint intrusion${count === 1 ? "" : "s"}`;
110842
+ }
110843
+ };
110844
+ var getMoveDirectionForEdge = (edge) => {
110845
+ switch (edge) {
110846
+ case "left":
110847
+ return "right";
110848
+ case "right":
110849
+ return "left";
110850
+ case "top":
110851
+ return "down";
110852
+ case "bottom":
110853
+ return "up";
110854
+ }
110855
+ };
110856
+ var getBoardEdgeStatus = (bounds, boardBounds, componentName) => {
110857
+ if (!bounds || !boardBounds)
110858
+ return null;
110859
+ const clearances = [
110860
+ { edge: "left", clearance: bounds.min_x - boardBounds.min_x },
110861
+ { edge: "right", clearance: boardBounds.max_x - bounds.max_x },
110862
+ { edge: "top", clearance: bounds.min_y - boardBounds.min_y },
110863
+ {
110864
+ edge: "bottom",
110865
+ clearance: boardBounds.max_y - bounds.max_y
110866
+ }
110867
+ ];
110868
+ const outside = clearances.filter((entry) => entry.clearance < 0).sort((a, b) => a.clearance - b.clearance);
110869
+ const selected = outside[0] ?? [...clearances].sort((a, b) => a.clearance - b.clearance)[0] ?? null;
110870
+ if (!selected)
110871
+ return null;
110872
+ return {
110873
+ component_name: componentName,
110874
+ edge: selected.edge,
110875
+ status: selected.clearance < 0 ? "outside" : "inside",
110876
+ distance: Math.abs(selected.clearance)
110877
+ };
110878
+ };
110879
+ var formatBoardEdgeStatus = (status) => `${fmtMm2(status.distance)} ${status.status} ${status.edge} edge`;
110880
+ var chooseMover = (a, b, preferredMover) => {
110881
+ if (preferredMover === a.name)
110882
+ return { mover: a, anchor: b };
110883
+ if (preferredMover === b.name)
110884
+ return { mover: b, anchor: a };
110885
+ if (a.isConnectorLike && !b.isConnectorLike)
110886
+ return { mover: b, anchor: a };
110887
+ if (b.isConnectorLike && !a.isConnectorLike)
110888
+ return { mover: a, anchor: b };
110889
+ const areaA = getArea(a.bounds);
110890
+ const areaB = getArea(b.bounds);
110891
+ if (areaA < areaB)
110892
+ return { mover: a, anchor: b };
110893
+ if (areaB < areaA)
110894
+ return { mover: b, anchor: a };
110895
+ return a.order > b.order ? { mover: a, anchor: b } : { mover: b, anchor: a };
110896
+ };
110897
+ var getMoveDirectionBetweenComponents = (mover, anchor, axis) => {
110898
+ if (axis === "x") {
110899
+ if ((mover.centerX ?? 0) >= (anchor.centerX ?? 0))
110900
+ return "right";
110901
+ return "left";
110902
+ }
110903
+ if ((mover.centerY ?? 0) >= (anchor.centerY ?? 0))
110904
+ return "down";
110905
+ return "up";
110906
+ };
110907
+ var getSeparationSuggestion = (a, b, overlapX, overlapY, preferredMover) => {
110908
+ if (a.centerX === null || a.centerY === null || b.centerX === null || b.centerY === null) {
110909
+ return;
110910
+ }
110911
+ const { mover, anchor } = chooseMover(a, b, preferredMover);
110912
+ const axis = overlapX <= overlapY ? "x" : "y";
110913
+ const distance = axis === "x" ? overlapX : overlapY;
110914
+ const direction = getMoveDirectionBetweenComponents(mover, anchor, axis);
110915
+ return `move ${mover.name} ${fmtMm2(distance)} ${direction}`;
110916
+ };
110917
+ var getCountainmentBonus = (a, b) => {
110918
+ if (!a.bounds || !b.bounds || a.centerX === null || a.centerY === null)
110919
+ return 0;
110920
+ const centerInsideB = a.centerX >= b.bounds.min_x && a.centerX <= b.bounds.max_x && a.centerY >= b.bounds.min_y && a.centerY <= b.bounds.max_y;
110921
+ return centerInsideB ? 25 : 0;
110922
+ };
110923
+ var createIssue = (issue) => issue;
110924
+ var getComponentByPcbId = (componentsByPcbId, pcbComponentId) => {
110925
+ if (typeof pcbComponentId !== "string")
110926
+ return null;
110927
+ return componentsByPcbId.get(pcbComponentId) ?? null;
110928
+ };
110929
+ var getLayers = (value) => {
110930
+ if (!Array.isArray(value))
110931
+ return [];
110932
+ return value.filter((layer) => typeof layer === "string");
110933
+ };
110934
+ var buildComponentContexts = (circuitJson) => {
110935
+ const components = [];
110936
+ const sourceComponentsById = /* @__PURE__ */ new Map;
110937
+ const componentByName = /* @__PURE__ */ new Map;
110938
+ const componentsByPcbId = /* @__PURE__ */ new Map;
110939
+ let order = 0;
110940
+ for (const el of circuitJson) {
110941
+ if (el.type !== "source_component" || typeof el.source_component_id !== "string" || typeof el.name !== "string") {
110942
+ continue;
110943
+ }
110944
+ if (componentByName.has(el.name))
110945
+ continue;
110946
+ const context = {
110947
+ name: el.name,
110948
+ sourceComponent: el,
110949
+ sourceComponentId: el.source_component_id,
110950
+ pcbComponent: null,
110951
+ pcbComponentId: null,
110952
+ centerX: null,
110953
+ centerY: null,
110954
+ layer: null,
110955
+ width: null,
110956
+ height: null,
110957
+ bounds: null,
110958
+ anchorAlignment: CENTER_ANCHOR2,
110959
+ placementMode: "none",
110960
+ pads: [],
110961
+ courtyards: [],
110962
+ isConnectorLike: false,
110963
+ order: order++
110964
+ };
110965
+ const xDefinitionRaw = el.pcbX ?? el.pcb_x ?? el.x;
110966
+ const yDefinitionRaw = el.pcbY ?? el.pcb_y ?? el.y;
110967
+ context.xDefinition = xDefinitionRaw === undefined ? undefined : String(xDefinitionRaw);
110968
+ context.yDefinition = yDefinitionRaw === undefined ? undefined : String(yDefinitionRaw);
110969
+ if (context.xDefinition !== undefined || context.yDefinition !== undefined) {
110970
+ context.placementMode = "props_set";
110971
+ } else if (el.placement_mode === "auto") {
110972
+ context.placementMode = "auto";
110973
+ }
110974
+ components.push(context);
110975
+ sourceComponentsById.set(context.sourceComponentId, context);
110976
+ componentByName.set(context.name, context);
110977
+ }
110978
+ for (const el of circuitJson) {
110979
+ if (el.type !== "pcb_component" || typeof el.source_component_id !== "string" || typeof el.pcb_component_id !== "string") {
110980
+ continue;
110981
+ }
110982
+ const context = sourceComponentsById.get(el.source_component_id);
110983
+ if (!context)
110984
+ continue;
110985
+ context.pcbComponent = el;
110986
+ context.pcbComponentId = el.pcb_component_id;
110987
+ componentsByPcbId.set(el.pcb_component_id, context);
110988
+ const center = typeof el.center === "object" && el.center ? el.center : null;
110989
+ context.centerX = center ? toNumber22(center.x) : null;
110990
+ context.centerY = center ? toNumber22(center.y) : null;
110991
+ context.layer = typeof el.layer === "string" ? el.layer : null;
110992
+ context.width = toNumber22(el.width);
110993
+ context.height = toNumber22(el.height);
110994
+ if (context.centerX !== null && context.centerY !== null && context.width !== null && context.height !== null) {
110995
+ context.bounds = getBoundsFromCenterAndSize(context.centerX, context.centerY, context.width, context.height);
110996
+ }
110997
+ if (context.placementMode === "none" && el.position_mode === "auto") {
110998
+ context.placementMode = "auto";
110999
+ }
111000
+ if (context.sourceComponent.ftype === "simple_pin_header" && context.width !== null && context.height !== null) {
111001
+ context.orientation = context.width >= context.height ? "horizontal" : "vertical";
111002
+ }
111003
+ }
111004
+ for (const el of circuitJson) {
111005
+ if (el.type === "pcb_silkscreen_text" && typeof el.anchor_alignment === "string") {
111006
+ const context = getComponentByPcbId(componentsByPcbId, el.pcb_component_id);
111007
+ if (!context)
111008
+ continue;
111009
+ context.anchorAlignment = el.anchor_alignment;
111010
+ continue;
111011
+ }
111012
+ if (el.type === "pcb_smtpad") {
111013
+ const context = getComponentByPcbId(componentsByPcbId, el.pcb_component_id);
111014
+ if (!context)
111015
+ continue;
111016
+ const x = toNumber22(el.x);
111017
+ const y = toNumber22(el.y);
111018
+ const width = toNumber22(el.width);
111019
+ const height = toNumber22(el.height);
111020
+ const layer = typeof el.layer === "string" ? el.layer : null;
111021
+ if (x === null || y === null || width === null || height === null || !layer)
111022
+ continue;
111023
+ context.pads.push({
111024
+ bounds: getBoundsFromCenterAndSize(x, y, width, height),
111025
+ layers: [layer]
111026
+ });
111027
+ continue;
111028
+ }
111029
+ if (el.type === "pcb_plated_hole") {
111030
+ const context = getComponentByPcbId(componentsByPcbId, el.pcb_component_id);
111031
+ if (!context)
111032
+ continue;
111033
+ const x = toNumber22(el.x);
111034
+ const y = toNumber22(el.y);
111035
+ const layers = getLayers(el.layers);
111036
+ if (x === null || y === null || layers.length === 0)
111037
+ continue;
111038
+ let width = null;
111039
+ let height = null;
111040
+ if (el.shape === "circle") {
111041
+ const diameter = toNumber22(el.outer_diameter) ?? toNumber22(el.hole_diameter);
111042
+ width = diameter;
111043
+ height = diameter;
111044
+ } else if (el.shape === "circular_hole_with_rect_pad" || el.shape === "pill_hole_with_rect_pad" || el.shape === "rotated_pill_hole_with_rect_pad") {
111045
+ width = toNumber22(el.rect_pad_width);
111046
+ height = toNumber22(el.rect_pad_height);
111047
+ } else if (el.shape === "hole_with_polygon_pad") {
111048
+ const padOutline = Array.isArray(el.pad_outline) ? el.pad_outline : [];
111049
+ const padBounds = getBoundsFromPoints(padOutline.map((point) => ({
111050
+ x: (toNumber22(point.x) ?? 0) + x,
111051
+ y: (toNumber22(point.y) ?? 0) + y
111052
+ })));
111053
+ if (padBounds) {
111054
+ context.pads.push({
111055
+ bounds: padBounds,
111056
+ layers
111057
+ });
111058
+ }
111059
+ continue;
111060
+ }
111061
+ if (width === null || height === null)
111062
+ continue;
111063
+ context.pads.push({
111064
+ bounds: getBoundsFromCenterAndSize(x, y, width, height),
111065
+ layers
111066
+ });
111067
+ continue;
111068
+ }
111069
+ if (el.type === "pcb_courtyard_rect") {
111070
+ const context = getComponentByPcbId(componentsByPcbId, el.pcb_component_id);
111071
+ if (!context)
111072
+ continue;
111073
+ const center = typeof el.center === "object" && el.center ? el.center : null;
111074
+ const centerX = center ? toNumber22(center.x) : null;
111075
+ const centerY = center ? toNumber22(center.y) : null;
111076
+ const width = toNumber22(el.width);
111077
+ const height = toNumber22(el.height);
111078
+ if (centerX === null || centerY === null || width === null || height === null)
111079
+ continue;
111080
+ context.courtyards.push(getBoundsFromCenterAndSize(centerX, centerY, width, height));
111081
+ continue;
111082
+ }
111083
+ if (el.type === "pcb_courtyard_outline") {
111084
+ const context = getComponentByPcbId(componentsByPcbId, el.pcb_component_id);
111085
+ if (!context)
111086
+ continue;
111087
+ const outline = Array.isArray(el.outline) ? el.outline : [];
111088
+ const bounds = getBoundsFromPoints(outline);
111089
+ if (bounds)
111090
+ context.courtyards.push(bounds);
111091
+ continue;
111092
+ }
111093
+ if (el.type === "pcb_courtyard_polygon") {
111094
+ const context = getComponentByPcbId(componentsByPcbId, el.pcb_component_id);
111095
+ if (!context)
111096
+ continue;
111097
+ const points = Array.isArray(el.points) ? el.points : [];
111098
+ const bounds = getBoundsFromPoints(points);
111099
+ if (bounds)
111100
+ context.courtyards.push(bounds);
111101
+ }
111102
+ }
111103
+ for (const context of components) {
111104
+ const nameUpper = context.name.toUpperCase();
111105
+ const ftype = String(context.sourceComponent.ftype ?? "").toUpperCase();
111106
+ const manufacturerPartNumber = String(context.sourceComponent.manufacturer_part_number ?? context.sourceComponent.manufacturerPartNumber ?? "").toUpperCase();
111107
+ const platedHoleCount = context.pads.filter((pad) => pad.layers.includes("bottom")).length;
111108
+ context.isConnectorLike = ftype === "SIMPLE_PIN_HEADER" || nameUpper.startsWith("USB") || /^J[A-Z0-9_]*\d*$/.test(nameUpper) || /USB|CONN|CONNECTOR|HEADER|SOCKET|JST|HDMI|RJ|BARREL/.test(manufacturerPartNumber) || platedHoleCount >= 4 && context.bounds !== null && Math.max(context.bounds.width, context.bounds.height) >= 4;
111109
+ }
111110
+ const pcbBoard = circuitJson.find((el) => el.type === "pcb_board");
111111
+ const boardCenter = pcbBoard && typeof pcbBoard.center === "object" && pcbBoard.center ? pcbBoard.center : null;
111112
+ const boardCenterX = boardCenter ? toNumber22(boardCenter.x) : null;
111113
+ const boardCenterY = boardCenter ? toNumber22(boardCenter.y) : null;
111114
+ const boardWidth = toNumber22(pcbBoard?.width);
111115
+ const boardHeight = toNumber22(pcbBoard?.height);
111116
+ const boardBounds = boardCenterX !== null && boardCenterY !== null && boardWidth !== null && boardHeight !== null ? getBoundsFromCenterAndSize(boardCenterX, boardCenterY, boardWidth, boardHeight) : null;
111117
+ return {
111118
+ components,
111119
+ componentByName,
111120
+ boardBounds
111121
+ };
111122
+ };
111123
+ var buildIssues = (components, boardBounds) => {
111124
+ const issues = [];
111125
+ for (const component of components) {
111126
+ const boardEdgeStatus = getBoardEdgeStatus(component.bounds, boardBounds, component.name);
111127
+ if (!boardEdgeStatus || boardEdgeStatus.status !== "outside")
111128
+ continue;
111129
+ const moveDirection = getMoveDirectionForEdge(boardEdgeStatus.edge);
111130
+ issues.push(createIssue({
111131
+ type: "off_board",
111132
+ componentA: component.name,
111133
+ clearance: -boardEdgeStatus.distance,
111134
+ severity: 220 + boardEdgeStatus.distance * 100,
111135
+ summary: `${component.name} is ${fmtMm2(boardEdgeStatus.distance)} outside ${boardEdgeStatus.edge} edge`,
111136
+ suggested_move: `move ${component.name} ${fmtMm2(boardEdgeStatus.distance)} ${moveDirection} to clear ${boardEdgeStatus.edge} edge`
111137
+ }));
111138
+ }
111139
+ for (let i = 0;i < components.length; i += 1) {
111140
+ const a = components[i];
111141
+ if (!a)
111142
+ continue;
111143
+ for (let j = i + 1;j < components.length; j += 1) {
111144
+ const b = components[j];
111145
+ if (!b)
111146
+ continue;
111147
+ if (!a.bounds || !b.bounds)
111148
+ continue;
111149
+ if (a.layer !== null && b.layer !== null && a.layer !== b.layer)
111150
+ continue;
111151
+ let strongestPadOverlap = null;
111152
+ for (const padA of a.pads) {
111153
+ for (const padB of b.pads) {
111154
+ if (!layersIntersect(padA.layers, padB.layers))
111155
+ continue;
111156
+ const overlap = getOverlap(padA.bounds, padB.bounds);
111157
+ if (!overlap)
111158
+ continue;
111159
+ if (!strongestPadOverlap || overlap.clearance < strongestPadOverlap.clearance) {
111160
+ strongestPadOverlap = overlap;
111161
+ }
111162
+ }
111163
+ }
111164
+ const bodyOverlap = getOverlap(a.bounds, b.bounds);
111165
+ let strongestCourtyardOverlap = null;
111166
+ if (!strongestPadOverlap) {
111167
+ for (const courtyardA of a.courtyards) {
111168
+ for (const courtyardB of b.courtyards) {
111169
+ const overlap = getOverlap(courtyardA, courtyardB);
111170
+ if (!overlap)
111171
+ continue;
111172
+ if (!strongestCourtyardOverlap || overlap.clearance < strongestCourtyardOverlap.clearance) {
111173
+ strongestCourtyardOverlap = overlap;
111174
+ }
111175
+ }
111176
+ }
111177
+ }
111178
+ if (strongestPadOverlap) {
111179
+ issues.push(createIssue({
111180
+ type: "pad_overlap",
111181
+ componentA: a.name,
111182
+ componentB: b.name,
111183
+ clearance: strongestPadOverlap.clearance,
111184
+ severity: 300 + Math.abs(strongestPadOverlap.clearance) * 120,
111185
+ summary: `${a.name} and ${b.name} pad overlap by ${fmtMm2(Math.abs(strongestPadOverlap.clearance))}`,
111186
+ suggested_move: getSeparationSuggestion(a, b, strongestPadOverlap.overlapX, strongestPadOverlap.overlapY)
111187
+ }));
111188
+ }
111189
+ if (bodyOverlap) {
111190
+ if (a.isConnectorLike || b.isConnectorLike) {
111191
+ const connector = a.isConnectorLike ? a : b;
111192
+ const intruder = connector === a ? b : a;
111193
+ const overlapX = bodyOverlap.overlapX;
111194
+ const overlapY = bodyOverlap.overlapY;
111195
+ const containmentBonus = getCountainmentBonus(intruder, connector);
111196
+ issues.push(createIssue({
111197
+ type: "connector_body_intrusion",
111198
+ componentA: intruder.name,
111199
+ componentB: connector.name,
111200
+ clearance: bodyOverlap.clearance,
111201
+ severity: 260 + Math.abs(bodyOverlap.clearance) * 100 + containmentBonus,
111202
+ summary: `${intruder.name} intrudes ${fmtMm2(Math.abs(bodyOverlap.clearance))} into ${connector.name} connector body`,
111203
+ suggested_move: getSeparationSuggestion(a, b, overlapX, overlapY, intruder.name)
111204
+ }));
111205
+ } else if (!strongestPadOverlap) {
111206
+ const containmentBonus = getCountainmentBonus(a, b) + getCountainmentBonus(b, a);
111207
+ issues.push(createIssue({
111208
+ type: "footprint_intrusion",
111209
+ componentA: a.name,
111210
+ componentB: b.name,
111211
+ clearance: bodyOverlap.clearance,
111212
+ severity: 180 + Math.abs(bodyOverlap.clearance) * 80 + containmentBonus,
111213
+ summary: `${a.name} and ${b.name} footprint intrusion by ${fmtMm2(Math.abs(bodyOverlap.clearance))}`,
111214
+ suggested_move: getSeparationSuggestion(a, b, bodyOverlap.overlapX, bodyOverlap.overlapY)
111215
+ }));
111216
+ }
111217
+ } else if (strongestCourtyardOverlap) {
111218
+ issues.push(createIssue({
111219
+ type: "courtyard_collision",
111220
+ componentA: a.name,
111221
+ componentB: b.name,
111222
+ clearance: strongestCourtyardOverlap.clearance,
111223
+ severity: 120 + Math.abs(strongestCourtyardOverlap.clearance) * 80,
111224
+ summary: `${a.name} and ${b.name} courtyard collision by ${fmtMm2(Math.abs(strongestCourtyardOverlap.clearance))}`,
111225
+ suggested_move: getSeparationSuggestion(a, b, strongestCourtyardOverlap.overlapX, strongestCourtyardOverlap.overlapY)
111226
+ }));
111227
+ }
111228
+ }
111229
+ }
111230
+ return issues.sort((a, b) => b.severity - a.severity);
111231
+ };
111232
+ var buildClusters = (components, issues) => {
111233
+ const componentsByName = new Map(components.map((component) => [component.name, component]));
111234
+ const adjacency = /* @__PURE__ */ new Map;
111235
+ const incidentSeverity = /* @__PURE__ */ new Map;
111236
+ for (const component of components) {
111237
+ adjacency.set(component.name, /* @__PURE__ */ new Set);
111238
+ incidentSeverity.set(component.name, 0);
111239
+ }
111240
+ for (const issue of issues) {
111241
+ if (!issue.componentB)
111242
+ continue;
111243
+ adjacency.get(issue.componentA)?.add(issue.componentB);
111244
+ adjacency.get(issue.componentB)?.add(issue.componentA);
111245
+ incidentSeverity.set(issue.componentA, (incidentSeverity.get(issue.componentA) ?? 0) + issue.severity);
111246
+ incidentSeverity.set(issue.componentB, (incidentSeverity.get(issue.componentB) ?? 0) + issue.severity);
111247
+ }
111248
+ const visited = /* @__PURE__ */ new Set;
111249
+ const clusters = [];
111250
+ for (const component of components) {
111251
+ if (visited.has(component.name))
111252
+ continue;
111253
+ const queue = [component.name];
111254
+ const members = [];
111255
+ while (queue.length > 0) {
111256
+ const current = queue.shift();
111257
+ if (visited.has(current))
111258
+ continue;
111259
+ visited.add(current);
111260
+ members.push(current);
111261
+ for (const neighbor of adjacency.get(current) ?? []) {
111262
+ if (!visited.has(neighbor))
111263
+ queue.push(neighbor);
111264
+ }
111265
+ }
111266
+ if (members.length < 3)
111267
+ continue;
111268
+ const memberSet = new Set(members);
111269
+ const severity = issues.filter((issue) => issue.componentB && memberSet.has(issue.componentA) && memberSet.has(issue.componentB)).reduce((sum, issue) => sum + issue.severity, 0);
111270
+ if (severity === 0)
111271
+ continue;
111272
+ const sortedMembers = [...members].sort((a, b) => {
111273
+ const severityA = incidentSeverity.get(a) ?? 0;
111274
+ const severityB = incidentSeverity.get(b) ?? 0;
111275
+ if (severityA !== severityB)
111276
+ return severityB - severityA;
111277
+ const orderA = componentsByName.get(a)?.order ?? 0;
111278
+ const orderB = componentsByName.get(b)?.order ?? 0;
111279
+ return orderA - orderB;
111280
+ });
111281
+ const labelComponent = sortedMembers.find((name) => componentsByName.get(name)?.isConnectorLike) ?? sortedMembers[0];
111282
+ if (!labelComponent)
111283
+ continue;
111284
+ clusters.push({
111285
+ clusterName: `${stripNumericSuffix(labelComponent)} cluster`,
111286
+ componentNames: sortedMembers,
111287
+ severity
111288
+ });
111289
+ }
111290
+ return clusters.sort((a, b) => b.severity - a.severity);
111291
+ };
111292
+ var buildCountsByType = (issues) => {
111293
+ const counts = {};
111294
+ for (const issue of issues) {
111295
+ counts[issue.type] = (counts[issue.type] ?? 0) + 1;
111296
+ }
111297
+ return counts;
111298
+ };
111299
+ var buildComponentStatuses = (components, boardBounds, issues) => components.map((component) => {
111300
+ const componentIssues = issues.filter((issue) => issue.componentA === component.name || issue.componentB === component.name);
111301
+ return {
111302
+ componentName: component.name,
111303
+ placementMode: component.placementMode,
111304
+ sourcePlacement: {
111305
+ xDefinition: component.xDefinition,
111306
+ yDefinition: component.yDefinition
111307
+ },
111308
+ resolvedPlacement: {
111309
+ center: component.centerX !== null && component.centerY !== null && component.layer !== null ? {
111310
+ x: component.centerX,
111311
+ y: component.centerY,
111312
+ layer: component.layer
111313
+ } : undefined,
111314
+ bounds: component.bounds ? {
111315
+ width: component.bounds.width,
111316
+ height: component.bounds.height,
111317
+ min_x: component.bounds.min_x,
111318
+ max_x: component.bounds.max_x,
111319
+ min_y: component.bounds.min_y,
111320
+ max_y: component.bounds.max_y
111321
+ } : undefined,
111322
+ anchorAlignment: component.anchorAlignment,
111323
+ orientation: component.orientation
111324
+ },
111325
+ boardEdgeStatus: getBoardEdgeStatus(component.bounds, boardBounds, component.name),
111326
+ issues: componentIssues
111327
+ };
111328
+ });
111329
+ var formatSourcePlacement = (component) => {
111330
+ const bits = [`placement_mode=${component.placementMode}`];
111331
+ if (component.sourcePlacement.xDefinition !== undefined) {
111332
+ bits.push(`x=${component.sourcePlacement.xDefinition}`);
111333
+ }
111334
+ if (component.sourcePlacement.yDefinition !== undefined) {
111335
+ bits.push(`y=${component.sourcePlacement.yDefinition}`);
111336
+ }
111337
+ return bits.join(", ");
111338
+ };
111339
+ var formatResolvedPlacement = (component) => {
111340
+ const bits = [];
111341
+ const center = component.resolvedPlacement.center;
111342
+ const bounds = component.resolvedPlacement.bounds;
111343
+ if (center) {
111344
+ bits.push(`center=(${fmtMm2(center.x)}, ${fmtMm2(center.y)}) on ${center.layer}`);
111345
+ }
111346
+ if (bounds) {
111347
+ bits.push(`bounds=(minX=${fmtMm2(bounds.min_x)}, maxX=${fmtMm2(bounds.max_x)}, minY=${fmtMm2(bounds.min_y)}, maxY=${fmtMm2(bounds.max_y)})`);
111348
+ bits.push(`size=(width=${fmtMm2(bounds.width)}, height=${fmtMm2(bounds.height)})`);
111349
+ }
111350
+ bits.push(`anchor_alignment="${component.resolvedPlacement.anchorAlignment}"`);
111351
+ if (component.resolvedPlacement.orientation) {
111352
+ bits.push(`orientation=${component.resolvedPlacement.orientation}`);
111353
+ }
111354
+ return bits.join("; ");
111355
+ };
111356
+ var formatIssue = (issue) => {
111357
+ const suffix = issue.suggested_move ? ` Suggested move: ${issue.suggested_move}.` : "";
111358
+ return `${issue.summary}.${suffix}`;
111359
+ };
111360
+ var formatPlacementAnalysisReport = (report) => {
111361
+ const lines = [];
111362
+ const summaryBits = ISSUE_TYPE_ORDER.map((type) => getSummaryLabel(type, report.summary.countsByType[type] ?? 0)).filter((entry) => Boolean(entry));
111363
+ lines.push(summaryBits.length > 0 ? `placement summary: ${summaryBits.join(", ")}` : "placement summary: no placement issues");
111364
+ if (report.summary.topIssues.length > 0) {
111365
+ lines.push("");
111366
+ lines.push("worst issues:");
111367
+ report.summary.topIssues.forEach((issue, index) => {
111368
+ lines.push(`${index + 1}. ${formatIssue(issue)}`);
111369
+ });
111370
+ }
111371
+ if (report.summary.likelyBadClusters.length > 0) {
111372
+ lines.push("");
111373
+ lines.push("likely bad clusters:");
111374
+ for (const cluster of report.summary.likelyBadClusters) {
111375
+ lines.push(`- ${cluster.clusterName}: ${cluster.componentNames.join(", ")}`);
111376
+ }
111377
+ }
111378
+ lines.push("");
111379
+ lines.push("board-edge status:");
111380
+ for (const component of report.components) {
111381
+ if (!component.boardEdgeStatus)
111382
+ continue;
111383
+ lines.push(`- ${component.componentName}: ${formatBoardEdgeStatus(component.boardEdgeStatus)}`);
111384
+ }
111385
+ const flaggedComponents = report.components.filter((component) => component.issues.length > 0);
111386
+ if (flaggedComponents.length > 0) {
111387
+ lines.push("");
111388
+ lines.push("flagged components:");
111389
+ for (const component of flaggedComponents) {
111390
+ lines.push(`- ${component.componentName}`);
111391
+ lines.push(` source placement: ${formatSourcePlacement(component)}`);
111392
+ lines.push(` resolved placement: ${formatResolvedPlacement(component)}`);
111393
+ if (component.boardEdgeStatus) {
111394
+ lines.push(` board edge status: ${formatBoardEdgeStatus(component.boardEdgeStatus)}`);
111395
+ }
111396
+ lines.push(" issues:");
111397
+ for (const issue of component.issues) {
111398
+ lines.push(` - ${formatIssue(issue)}`);
111399
+ }
111400
+ }
111401
+ }
111402
+ return lines.join(`
111403
+ `);
111404
+ };
111405
+ var buildPlacementAnalysisReport = (circuitJson) => {
111406
+ const { components, boardBounds } = buildComponentContexts(circuitJson);
111407
+ const issues = buildIssues(components, boardBounds);
111408
+ const clusters = buildClusters(components, issues);
111409
+ const countsByType = buildCountsByType(issues);
111410
+ return {
111411
+ summary: {
111412
+ totalIssueCount: issues.length,
111413
+ countsByType,
111414
+ topIssues: issues.slice(0, TOP_ISSUE_LIMIT),
111415
+ likelyBadClusters: clusters
111416
+ },
111417
+ components: buildComponentStatuses(components, boardBounds, issues),
111418
+ issues
111419
+ };
111420
+ };
110753
111421
  var analyzeAllPlacements = (circuitJson) => {
110754
111422
  const componentNames = [];
110755
111423
  const seenNames = /* @__PURE__ */ new Set;
@@ -110766,10 +111434,12 @@ var analyzeAllPlacements = (circuitJson) => {
110766
111434
  analysis: analyzeComponentPlacement(circuitJson, componentName)
110767
111435
  }));
110768
111436
  const lineItems = analyses.flatMap(({ analysis }) => analysis.getLineItems());
111437
+ const report = buildPlacementAnalysisReport(circuitJson);
110769
111438
  return {
110770
111439
  getLineItems: () => lineItems,
110771
- getString: () => analyses.map(({ analysis }) => analysis.getString()).join(`
110772
- `)
111440
+ getString: () => formatPlacementAnalysisReport(report),
111441
+ getIssues: () => report.issues,
111442
+ getReport: () => report
110773
111443
  };
110774
111444
  };
110775
111445
 
@@ -110784,8 +111454,10 @@ var checkPlacement = async (file, refdes) => {
110784
111454
  },
110785
111455
  allowPrebuiltCircuitJson: true
110786
111456
  });
110787
- const analysis = refdes ? analyzeComponentPlacement(circuitJson, refdes) : analyzeAllPlacements(circuitJson);
110788
- return analysis.getString();
111457
+ if (refdes) {
111458
+ return analyzeComponentPlacement(circuitJson, refdes).getString();
111459
+ }
111460
+ return analyzeAllPlacements(circuitJson).getString();
110789
111461
  };
110790
111462
  var registerCheckPlacement = (program2) => {
110791
111463
  program2.commands.find((c) => c.name() === "check").command("placement").description("Partially build and validate the placement").argument("[file]", "Path to the entry file").argument("[refdes]", "Optional refdes to scope the check").action(async (file, refdes) => {
@@ -133567,7 +134239,7 @@ var getDirectionalFreeSpace = (spatialIndex, componentIndex, directions) => {
133567
134239
  }
133568
134240
  return freeSpaceByDirection;
133569
134241
  };
133570
- var fmtMm2 = (value) => `${value.toFixed(1)}mm`;
134242
+ var fmtMm3 = (value) => `${value.toFixed(1)}mm`;
133571
134243
  var roundsToZeroMm = (value) => Math.abs(value) < 0.05;
133572
134244
  var toNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
133573
134245
  var getPcbComponentBounds = (element) => {
@@ -133611,10 +134283,10 @@ var getPlacedComponents = (circuitJson) => {
133611
134283
  }
133612
134284
  return placedComponents;
133613
134285
  };
133614
- var getOffsetFromTopOfRegion = (componentBounds, regionBounds) => fmtMm2(regionBounds.maxY - componentBounds.maxY);
133615
- var getOffsetFromBottomOfRegion = (componentBounds, regionBounds) => fmtMm2(componentBounds.minY - regionBounds.minY);
133616
- var getOffsetFromLeftOfRegion = (componentBounds, regionBounds) => fmtMm2(componentBounds.minX - regionBounds.minX);
133617
- var getOffsetFromRightOfRegion = (componentBounds, regionBounds) => fmtMm2(regionBounds.maxX - componentBounds.maxX);
134286
+ var getOffsetFromTopOfRegion = (componentBounds, regionBounds) => fmtMm3(regionBounds.maxY - componentBounds.maxY);
134287
+ var getOffsetFromBottomOfRegion = (componentBounds, regionBounds) => fmtMm3(componentBounds.minY - regionBounds.minY);
134288
+ var getOffsetFromLeftOfRegion = (componentBounds, regionBounds) => fmtMm3(componentBounds.minX - regionBounds.minX);
134289
+ var getOffsetFromRightOfRegion = (componentBounds, regionBounds) => fmtMm3(regionBounds.maxX - componentBounds.maxX);
133618
134290
  var addEdgeAlignedOffsets = (nearbyComponent, componentBounds, regionBounds) => {
133619
134291
  if (nearbyComponent.onLeftEdgeOfRegion || nearbyComponent.onRightEdgeOfRegion) {
133620
134292
  nearbyComponent.distToTopOfRegion = getOffsetFromTopOfRegion(componentBounds, regionBounds);
@@ -133746,28 +134418,28 @@ var getOutsideComponentCandidate = (component, componentIndex, regionBounds) =>
133746
134418
  if (roundsToZeroMm(candidate.distance)) {
133747
134419
  nearbyComponentCandidate.onLeftEdgeOfRegion = true;
133748
134420
  } else {
133749
- nearbyComponentCandidate.distToLeftEdgeOfRegion = fmtMm2(candidate.distance);
134421
+ nearbyComponentCandidate.distToLeftEdgeOfRegion = fmtMm3(candidate.distance);
133750
134422
  }
133751
134423
  break;
133752
134424
  case "right":
133753
134425
  if (roundsToZeroMm(candidate.distance)) {
133754
134426
  nearbyComponentCandidate.onRightEdgeOfRegion = true;
133755
134427
  } else {
133756
- nearbyComponentCandidate.distToRightEdgeOfRegion = fmtMm2(candidate.distance);
134428
+ nearbyComponentCandidate.distToRightEdgeOfRegion = fmtMm3(candidate.distance);
133757
134429
  }
133758
134430
  break;
133759
134431
  case "top":
133760
134432
  if (roundsToZeroMm(candidate.distance)) {
133761
134433
  nearbyComponentCandidate.onTopEdgeOfRegion = true;
133762
134434
  } else {
133763
- nearbyComponentCandidate.distToTopOfRegion = fmtMm2(candidate.distance);
134435
+ nearbyComponentCandidate.distToTopOfRegion = fmtMm3(candidate.distance);
133764
134436
  }
133765
134437
  break;
133766
134438
  case "bottom":
133767
134439
  if (roundsToZeroMm(candidate.distance)) {
133768
134440
  nearbyComponentCandidate.onBottomEdgeOfRegion = true;
133769
134441
  } else {
133770
- nearbyComponentCandidate.distToBottomOfRegion = fmtMm2(candidate.distance);
134442
+ nearbyComponentCandidate.distToBottomOfRegion = fmtMm3(candidate.distance);
133771
134443
  }
133772
134444
  break;
133773
134445
  }
@@ -168820,7 +169492,7 @@ function normalizeDegrees(angle) {
168820
169492
  var doBoundsOverlap = (bounds1, bounds2) => {
168821
169493
  return !(bounds1.maxX < bounds2.minX || bounds2.maxX < bounds1.minX || bounds1.maxY < bounds2.minY || bounds2.maxY < bounds1.minY);
168822
169494
  };
168823
- var getBoundsFromPoints = (points) => {
169495
+ var getBoundsFromPoints2 = (points) => {
168824
169496
  if (points.length === 0) {
168825
169497
  return null;
168826
169498
  }
@@ -198645,8 +199317,8 @@ function checkCourtyardOverlap(circuitJson) {
198645
199317
  for (const b22 of byComponent.get(idB)) {
198646
199318
  const polyA = getCourtyardPolygon(a2);
198647
199319
  const polyB = getCourtyardPolygon(b22);
198648
- const boundsA = getBoundsFromPoints(polyA);
198649
- const boundsB = getBoundsFromPoints(polyB);
199320
+ const boundsA = getBoundsFromPoints2(polyA);
199321
+ const boundsB = getBoundsFromPoints2(polyB);
198650
199322
  if (!boundsA || !boundsB)
198651
199323
  continue;
198652
199324
  if (areBoundsOverlappingPolygon(boundsA, polyB) || areBoundsOverlappingPolygon(boundsB, polyA)) {
@@ -219678,7 +220350,7 @@ var CourtyardOutline = class extends PrimitiveComponent2 {
219678
220350
  if (!props.outline || props.outline.length === 0) {
219679
220351
  return { width: 0, height: 0 };
219680
220352
  }
219681
- const bounds = getBoundsFromPoints(props.outline);
220353
+ const bounds = getBoundsFromPoints2(props.outline);
219682
220354
  if (!bounds) {
219683
220355
  return { width: 0, height: 0 };
219684
220356
  }
@@ -223587,7 +224259,7 @@ var NormalComponent__getMinimumFlexContainerSize = (component) => {
223587
224259
  if (!pcbGroup)
223588
224260
  return null;
223589
224261
  if (pcbGroup.outline && pcbGroup.outline.length > 0) {
223590
- const bounds = getBoundsFromPoints(pcbGroup.outline);
224262
+ const bounds = getBoundsFromPoints2(pcbGroup.outline);
223591
224263
  if (!bounds)
223592
224264
  return null;
223593
224265
  return {
@@ -226856,7 +227528,7 @@ function Group_doInitialPcbComponentAnchorAlignment(group) {
226856
227528
  let height = pcbGroup.height;
226857
227529
  const { center: center2 } = pcbGroup;
226858
227530
  if (pcbGroup.outline && pcbGroup.outline.length > 0) {
226859
- const bounds2 = getBoundsFromPoints(pcbGroup.outline);
227531
+ const bounds2 = getBoundsFromPoints2(pcbGroup.outline);
226860
227532
  if (bounds2) {
226861
227533
  width = bounds2.maxX - bounds2.minX;
226862
227534
  height = bounds2.maxY - bounds2.minY;
@@ -230166,7 +230838,7 @@ var Group6 = class extends NormalComponent3 {
230166
230838
  x: distance.parse(point6.x),
230167
230839
  y: distance.parse(point6.y)
230168
230840
  }));
230169
- const outlineBounds = getBoundsFromPoints(numericOutline);
230841
+ const outlineBounds = getBoundsFromPoints2(numericOutline);
230170
230842
  if (!outlineBounds)
230171
230843
  return;
230172
230844
  const centerX2 = (outlineBounds.minX + outlineBounds.maxX) / 2;
@@ -232098,7 +232770,7 @@ var Board = class extends Group6 {
232098
232770
  let width = dbBoard?.width ?? props.width;
232099
232771
  let height = dbBoard?.height ?? props.height;
232100
232772
  if ((width == null || height == null) && props.outline?.length) {
232101
- const outlineBounds = getBoundsFromPoints(props.outline);
232773
+ const outlineBounds = getBoundsFromPoints2(props.outline);
232102
232774
  if (outlineBounds) {
232103
232775
  width ??= outlineBounds.maxX - outlineBounds.minX;
232104
232776
  height ??= outlineBounds.maxY - outlineBounds.minY;
@@ -232154,7 +232826,7 @@ var Board = class extends Group6 {
232154
232826
  let width = pcbGroup.width ?? 0;
232155
232827
  let height = pcbGroup.height ?? 0;
232156
232828
  if (pcbGroup.outline && pcbGroup.outline.length > 0) {
232157
- const bounds = getBoundsFromPoints(pcbGroup.outline);
232829
+ const bounds = getBoundsFromPoints2(pcbGroup.outline);
232158
232830
  if (bounds) {
232159
232831
  width = bounds.maxX - bounds.minX;
232160
232832
  height = bounds.maxY - bounds.minY;
@@ -232305,7 +232977,7 @@ var Board = class extends Group6 {
232305
232977
  }
232306
232978
  let outlineTranslation = { x: 0, y: 0 };
232307
232979
  if (outline && outline.length > 0 && (this.parent?.lowercaseComponentName === "panel" || this.parent?.lowercaseComponentName === "subpanel")) {
232308
- const outlineBounds = getBoundsFromPoints(outline);
232980
+ const outlineBounds = getBoundsFromPoints2(outline);
232309
232981
  if (outlineBounds) {
232310
232982
  const outlineCenterX = (outlineBounds.minX + outlineBounds.maxX) / 2;
232311
232983
  const outlineCenterY = (outlineBounds.minY + outlineBounds.maxY) / 2;
@@ -232420,7 +233092,7 @@ var Board = class extends Group6 {
232420
233092
  if (this.pcb_board_id) {
232421
233093
  db.pcb_board.update(this.pcb_board_id, { center: position2 });
232422
233094
  if (pcbBoard?.outline) {
232423
- const outlineBounds = getBoundsFromPoints(pcbBoard.outline);
233095
+ const outlineBounds = getBoundsFromPoints2(pcbBoard.outline);
232424
233096
  if (outlineBounds) {
232425
233097
  const oldOutlineCenter = {
232426
233098
  x: (outlineBounds.minX + outlineBounds.maxX) / 2,
@@ -232723,7 +233395,7 @@ var getBoardDimensionsFromProps = (board) => {
232723
233395
  let width = props.width != null ? distance.parse(props.width) : undefined;
232724
233396
  let height = props.height != null ? distance.parse(props.height) : undefined;
232725
233397
  if ((width === undefined || height === undefined) && props.outline?.length) {
232726
- const outlineBounds = getBoundsFromPoints(props.outline);
233398
+ const outlineBounds = getBoundsFromPoints2(props.outline);
232727
233399
  if (outlineBounds) {
232728
233400
  width ??= outlineBounds.maxX - outlineBounds.minX;
232729
233401
  height ??= outlineBounds.maxY - outlineBounds.minY;
@@ -236889,13 +237561,13 @@ var solveForGlobalCapacityNodes = async (circuitJson) => {
236889
237561
  await solver.solveUntilPhase("highDensityRouteSolver");
236890
237562
  return solver.uniformPortDistributionSolver?.getOutput() ?? [];
236891
237563
  };
236892
- var fmtNumber2 = (value) => {
237564
+ var fmtNumber3 = (value) => {
236893
237565
  if (Number.isInteger(value))
236894
237566
  return String(value);
236895
237567
  return value.toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
236896
237568
  };
236897
237569
  var fmtMm22 = (value) => `${value.toFixed(1)}mm`;
236898
- var fmtPercent = (value) => `${fmtNumber2(value * 100)}%`;
237570
+ var fmtPercent = (value) => `${fmtNumber3(value * 100)}%`;
236899
237571
  var isCrampedPortPoint = (portPointId) => portPointId?.includes("_cramped") ?? false;
236900
237572
  var clamp012 = (value) => Math.max(0, Math.min(1, value));
236901
237573
  var roundProbability = (value) => Number.parseFloat(value.toFixed(3));
package/dist/lib/index.js CHANGED
@@ -60678,7 +60678,7 @@ var getNodeHandler = (winterSpec, { port, middleware = [] }) => {
60678
60678
  }));
60679
60679
  };
60680
60680
  // package.json
60681
- var version = "0.1.1172";
60681
+ var version = "0.1.1175";
60682
60682
  var package_default = {
60683
60683
  name: "@tscircuit/cli",
60684
60684
  version,
@@ -60690,7 +60690,7 @@ var package_default = {
60690
60690
  devDependencies: {
60691
60691
  "@babel/standalone": "^7.26.9",
60692
60692
  "@biomejs/biome": "^1.9.4",
60693
- "@tscircuit/circuit-json-placement-analysis": "^0.0.4",
60693
+ "@tscircuit/circuit-json-placement-analysis": "^0.0.5",
60694
60694
  "@tscircuit/circuit-json-routing-analysis": "^0.0.1",
60695
60695
  "@tscircuit/fake-snippets": "^0.0.182",
60696
60696
  "@tscircuit/file-server": "^0.0.32",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/cli",
3
- "version": "0.1.1174",
3
+ "version": "0.1.1176",
4
4
  "main": "dist/cli/main.js",
5
5
  "exports": {
6
6
  ".": "./dist/cli/main.js",
@@ -9,7 +9,7 @@
9
9
  "devDependencies": {
10
10
  "@babel/standalone": "^7.26.9",
11
11
  "@biomejs/biome": "^1.9.4",
12
- "@tscircuit/circuit-json-placement-analysis": "^0.0.4",
12
+ "@tscircuit/circuit-json-placement-analysis": "^0.0.5",
13
13
  "@tscircuit/circuit-json-routing-analysis": "^0.0.1",
14
14
  "@tscircuit/fake-snippets": "^0.0.182",
15
15
  "@tscircuit/file-server": "^0.0.32",