@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.
- package/dist/cli/build/build.worker.js +3 -3
- package/dist/cli/main.js +705 -33
- package/dist/lib/index.js +2 -2
- package/package.json +2 -2
|
@@ -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.
|
|
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.
|
|
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: () =>
|
|
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
|
-
|
|
110788
|
-
|
|
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
|
|
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) =>
|
|
133615
|
-
var getOffsetFromBottomOfRegion = (componentBounds, regionBounds) =>
|
|
133616
|
-
var getOffsetFromLeftOfRegion = (componentBounds, regionBounds) =>
|
|
133617
|
-
var getOffsetFromRightOfRegion = (componentBounds, regionBounds) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
198649
|
-
const boundsB =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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) => `${
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|