@protoboardai/ext-tscircuit 0.0.0-stage → 0.2.1
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/LICENSE +202 -0
- package/NOTICE +59 -0
- package/README.md +493 -2
- package/evals/announce-pcb-fallback/case.ts +67 -0
- package/evals/catch-swapped-nets/case.ts +69 -0
- package/evals/generate-imu-board/case.ts +60 -0
- package/evals/locked-board-fix-upstream/case.ts +70 -0
- package/lib/checks.ts +464 -0
- package/lib/config.ts +384 -0
- package/lib/fingerprint.ts +144 -0
- package/lib/footprints.ts +370 -0
- package/lib/kicad/cli.ts +93 -0
- package/lib/kicad/drc.ts +95 -0
- package/lib/kicad/fields.ts +65 -0
- package/lib/kicad/handoff.ts +112 -0
- package/lib/kicad/netlists.ts +311 -0
- package/lib/kicad/probe.cjs +21 -0
- package/lib/kicad/project.ts +594 -0
- package/lib/kicad/refdes.ts +130 -0
- package/lib/kicad/schematic.ts +624 -0
- package/lib/kicad/sexpr.ts +117 -0
- package/lib/land-geometry.ts +177 -0
- package/lib/land-patterns.ts +706 -0
- package/lib/layout.ts +236 -0
- package/lib/models.ts +284 -0
- package/lib/native.ts +199 -0
- package/lib/outputs.ts +383 -0
- package/lib/paste.ts +69 -0
- package/lib/probe.cjs +31 -0
- package/lib/runtime.ts +185 -0
- package/lib/silk.ts +141 -0
- package/lib/source.ts +472 -0
- package/lib/stepfile.ts +210 -0
- package/lib/suppliers.ts +39 -0
- package/ops/circuit.ts +270 -0
- package/ops/kicad.ts +636 -0
- package/ops/package.ts +156 -0
- package/package.json +81 -4
- package/protoboard-extension.json +313 -0
- package/skills/circuit-board/SKILL.md +494 -0
- package/skills/circuit-board/references/pcb-workflow.md +87 -0
- package/skills/kicad-cli/SKILL.md +210 -0
- package/skills/kicad-layout-review/SKILL.md +275 -0
package/lib/probe.cjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Tool probe for `protoboard doctor` and capability operations: the tscircuit
|
|
2
|
+
// libraries this provider runs in-process are installed at the exact versions
|
|
3
|
+
// package.json pins, on Node 22 or newer. Prints one line; exits 1 otherwise.
|
|
4
|
+
const { existsSync, readFileSync } = require("node:fs");
|
|
5
|
+
const { dirname, join } = require("node:path");
|
|
6
|
+
|
|
7
|
+
const KEY = ["@tscircuit/core", "@tscircuit/checks", "circuit-json-to-gerber", "circuit-json-to-gltf", "circuit-json-to-step", "circuit-to-svg", "sucrase"];
|
|
8
|
+
const root = dirname(require.resolve("@protoboardai/ext-tscircuit/package.json", { paths: [process.cwd(), __dirname] }));
|
|
9
|
+
const pins = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dependencies;
|
|
10
|
+
|
|
11
|
+
function installed(name) {
|
|
12
|
+
for (let dir = root; ; dir = dirname(dir)) {
|
|
13
|
+
const f = join(dir, "node_modules", name, "package.json");
|
|
14
|
+
if (existsSync(f)) return JSON.parse(readFileSync(f, "utf8")).version;
|
|
15
|
+
if (dirname(dir) === dir) return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const problems = [];
|
|
20
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
21
|
+
if (major < 22) problems.push(`Node ${process.version} (needs 22 or newer)`);
|
|
22
|
+
for (const name of KEY) {
|
|
23
|
+
const v = installed(name);
|
|
24
|
+
if (!v) problems.push(`${name} not installed`);
|
|
25
|
+
else if (v !== pins[name]) problems.push(`${name} ${v} (pinned ${pins[name]})`);
|
|
26
|
+
}
|
|
27
|
+
if (problems.length) {
|
|
28
|
+
console.error(`tscircuit libraries not usable: ${problems.join("; ")}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
console.log(`tscircuit @tscircuit/core ${pins["@tscircuit/core"]}, converters pinned, Node ${process.version}`);
|
package/lib/runtime.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running tscircuit in Protoboard's own Node process, offline by
|
|
3
|
+
* construction (docs/research/tscircuit-spike.md §1, §7, §9):
|
|
4
|
+
*
|
|
5
|
+
* - the native source (TSX) is compiled with sucrase and evaluated here;
|
|
6
|
+
* `tsci` (which needs Bun) is never spawned
|
|
7
|
+
* - @tscircuit/core renders it with the parts engine disabled, the cloud
|
|
8
|
+
* autorouter off and a local autorouter preset
|
|
9
|
+
* - `fetch` is replaced for the duration by a guard that serves absolute
|
|
10
|
+
* local paths and file:// URLs from disk and refuses everything else, so
|
|
11
|
+
* a build or an export cannot reach the network even by accident
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { dirname, extname, join, posix, resolve, win32 } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { transform } from "sucrase";
|
|
18
|
+
|
|
19
|
+
const localRequire = createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
/** A circuit JSON element (the tscircuit interchange format). */
|
|
22
|
+
export type CircuitElement = { type: string; [key: string]: any };
|
|
23
|
+
|
|
24
|
+
/** Network requests refused while the guard was installed (for reports and tests). */
|
|
25
|
+
export const refusedRequests: string[] = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The local file a model URL names, or undefined when it names none: a
|
|
29
|
+
* file:// URL, or an absolute path. On Windows that is `C:\\…`, `C:/…` or a
|
|
30
|
+
* UNC path as well as `/…`, and a file:// URL is `file:///C:/…` (whose URL
|
|
31
|
+
* pathname `/C:/…` is not a path there). `platform` is for tests.
|
|
32
|
+
*/
|
|
33
|
+
export function localFile(url: string, platform: NodeJS.Platform = process.platform): string | undefined {
|
|
34
|
+
const windows = platform === "win32";
|
|
35
|
+
if (url.startsWith("file://")) {
|
|
36
|
+
try {
|
|
37
|
+
return fileURLToPath(url, { windows });
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return (windows ? win32 : posix).isAbsolute(url) ? url : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Local files the guard was asked for while installed: found and read, or missing. */
|
|
46
|
+
export interface FileRequests {
|
|
47
|
+
read: string[];
|
|
48
|
+
missing: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run `fn` with `fetch` restricted to local files. Absolute paths and
|
|
53
|
+
* file:// URLs are read from disk; data: and blob: URLs pass through;
|
|
54
|
+
* anything else throws. `files`, when given, collects the local files asked
|
|
55
|
+
* for (so a caller can say which bodies loaded).
|
|
56
|
+
*/
|
|
57
|
+
export async function offline<T>(fn: () => Promise<T>, options: { files?: FileRequests; platform?: NodeJS.Platform } = {}): Promise<T> {
|
|
58
|
+
const real = globalThis.fetch;
|
|
59
|
+
globalThis.fetch = (async (input: unknown, init?: unknown) => {
|
|
60
|
+
const url = typeof input === "string" ? input : ((input as { url?: string })?.url ?? String(input));
|
|
61
|
+
const file = localFile(url, options.platform);
|
|
62
|
+
if (file) {
|
|
63
|
+
if (!existsSync(file)) {
|
|
64
|
+
options.files?.missing.push(file);
|
|
65
|
+
return new Response(null, { status: 404 });
|
|
66
|
+
}
|
|
67
|
+
options.files?.read.push(file);
|
|
68
|
+
return new Response(readFileSync(file));
|
|
69
|
+
}
|
|
70
|
+
if (url.startsWith("data:") || url.startsWith("blob:")) return real(input as Parameters<typeof fetch>[0], init as Parameters<typeof fetch>[1]);
|
|
71
|
+
refusedRequests.push(url);
|
|
72
|
+
throw new Error(`@protoboardai/ext-tscircuit works offline: refused a network request to ${url.slice(0, 120)}`);
|
|
73
|
+
}) as typeof fetch;
|
|
74
|
+
try {
|
|
75
|
+
return await fn();
|
|
76
|
+
} finally {
|
|
77
|
+
globalThis.fetch = real;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SOURCE_EXT = [".tsx", ".ts", ".jsx", ".js"];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load a TSX module and its relative imports; bare imports resolve from this package (react, react/jsx-runtime).
|
|
85
|
+
* `loaded` collects the absolute path of every file read: the entry, each relative import (JSON included),
|
|
86
|
+
* wherever it lives (a hand file shared with another board's folder).
|
|
87
|
+
*/
|
|
88
|
+
export function loadTsx(entry: string, loaded?: Set<string>): Record<string, unknown> {
|
|
89
|
+
const cache = new Map<string, { exports: Record<string, unknown> }>();
|
|
90
|
+
const load = (file: string): Record<string, unknown> => {
|
|
91
|
+
const hit = cache.get(file);
|
|
92
|
+
if (hit) return hit.exports;
|
|
93
|
+
const mod = { exports: {} as Record<string, unknown> };
|
|
94
|
+
cache.set(file, mod);
|
|
95
|
+
loaded?.add(file);
|
|
96
|
+
if (extname(file) === ".json") {
|
|
97
|
+
mod.exports = JSON.parse(readFileSync(file, "utf8"));
|
|
98
|
+
return mod.exports;
|
|
99
|
+
}
|
|
100
|
+
const { code } = transform(readFileSync(file, "utf8"), { transforms: ["typescript", "jsx", "imports"], jsxRuntime: "automatic", production: true, filePath: file });
|
|
101
|
+
const req = (spec: string) => {
|
|
102
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
103
|
+
const base = resolve(dirname(file), spec);
|
|
104
|
+
const found = [base, ...SOURCE_EXT.map((e) => base + e), ...SOURCE_EXT.map((e) => join(base, "index" + e))].find((p) => existsSync(p) && !readdirSafe(p));
|
|
105
|
+
if (!found) throw new Error(`${file}: cannot resolve ${spec}`);
|
|
106
|
+
return load(found);
|
|
107
|
+
}
|
|
108
|
+
return localRequire(spec);
|
|
109
|
+
};
|
|
110
|
+
new Function("exports", "require", "module", "__filename", "__dirname", code)(mod.exports, req, mod, file, dirname(file));
|
|
111
|
+
return mod.exports;
|
|
112
|
+
};
|
|
113
|
+
return load(resolve(entry));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readdirSafe(p: string): boolean {
|
|
117
|
+
try {
|
|
118
|
+
readdirSync(p);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface BuildResult {
|
|
126
|
+
circuitJson: CircuitElement[];
|
|
127
|
+
ms: number;
|
|
128
|
+
/** @tscircuit/core version that rendered it. */
|
|
129
|
+
coreVersion: string;
|
|
130
|
+
/** What tscircuit printed while building (router progress, solver notes). */
|
|
131
|
+
log: string[];
|
|
132
|
+
/** Every source file the build read (absolute): the entry and its relative imports, JSON included. */
|
|
133
|
+
files: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Installed version of a dependency, read from its package.json (several hide it behind "exports"). */
|
|
137
|
+
export function packageVersion(name: string): string {
|
|
138
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
139
|
+
for (;;) {
|
|
140
|
+
const file = join(dir, "node_modules", name, "package.json");
|
|
141
|
+
if (existsSync(file)) return (JSON.parse(readFileSync(file, "utf8")) as { version: string }).version;
|
|
142
|
+
const up = dirname(dir);
|
|
143
|
+
if (up === dir) return "unknown";
|
|
144
|
+
dir = up;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const coreVersion = (): string => packageVersion("@tscircuit/core");
|
|
149
|
+
|
|
150
|
+
/** Run `fn` with console.log/info/warn collected instead of printed (the router is chatty); `only` keeps just the lines it accepts. */
|
|
151
|
+
export async function quietly<T>(fn: () => Promise<T>, log: string[], only?: (line: string) => boolean): Promise<T> {
|
|
152
|
+
const saved = { log: console.log, info: console.info, warn: console.warn, debug: console.debug, error: console.error };
|
|
153
|
+
const keep = (...args: unknown[]) => {
|
|
154
|
+
const line = args.map(String).join(" ");
|
|
155
|
+
if (log.length < 2000 && (!only || only(line))) log.push(line);
|
|
156
|
+
};
|
|
157
|
+
Object.assign(console, { log: keep, info: keep, warn: keep, debug: keep, error: keep });
|
|
158
|
+
try {
|
|
159
|
+
return await fn();
|
|
160
|
+
} finally {
|
|
161
|
+
Object.assign(console, saved);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Render the board exported by `entry` (default export: a component taking
|
|
167
|
+
* no props) to circuit JSON with @tscircuit/core, offline.
|
|
168
|
+
*/
|
|
169
|
+
export async function buildCircuit(entry: string, options: { routing?: boolean } = {}): Promise<BuildResult> {
|
|
170
|
+
const t0 = performance.now();
|
|
171
|
+
const loaded = new Set<string>();
|
|
172
|
+
const mod = loadTsx(entry, loaded);
|
|
173
|
+
const Board = mod.default as (() => unknown) | undefined;
|
|
174
|
+
if (typeof Board !== "function") throw new Error(`${entry} has no default export (the board component)`);
|
|
175
|
+
const log: string[] = [];
|
|
176
|
+
const circuitJson = await quietly(() => offline(async () => {
|
|
177
|
+
const { Circuit } = (await import("@tscircuit/core")) as unknown as { Circuit: new (o: unknown) => { add(e: unknown): void; renderUntilSettled(): Promise<void>; getCircuitJson(): CircuitElement[] } };
|
|
178
|
+
const { createElement } = localRequire("react") as { createElement: (c: unknown) => unknown };
|
|
179
|
+
const circuit = new Circuit({ platform: { partsEngineDisabled: true, useCloudAutorouter: false, ...(options.routing === false ? { routingDisabled: true } : {}) } });
|
|
180
|
+
circuit.add(createElement(Board));
|
|
181
|
+
await circuit.renderUntilSettled();
|
|
182
|
+
return circuit.getCircuitJson();
|
|
183
|
+
}), log);
|
|
184
|
+
return { circuitJson, ms: Math.round(performance.now() - t0), coreVersion: coreVersion(), log, files: [...loaded].sort() };
|
|
185
|
+
}
|
package/lib/silk.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The silkscreen for authored land patterns: tscircuit draws silkscreen
|
|
3
|
+
* (outline and reference) only for its built-in footprint strings, so a board
|
|
4
|
+
* whose parts all have authored land patterns would go to the fab with an
|
|
5
|
+
* empty silkscreen. After each build this pass adds, for every component
|
|
6
|
+
* with an authored land pattern:
|
|
7
|
+
*
|
|
8
|
+
* - its reference designator (R12, U3: tscircuit/refdes.json, which the
|
|
9
|
+
* KiCad handoff keeps; the UHD instance path only for a component that
|
|
10
|
+
* has none), 0.8 mm high (the usual fab
|
|
11
|
+
* minimum), set beside its courtyard on the first side where it clears
|
|
12
|
+
* every pad, hole, courtyard, the board edge and the references already
|
|
13
|
+
* set; at 0.6 mm when none of the four sides has room; left off (and
|
|
14
|
+
* reported) when neither size fits;
|
|
15
|
+
* - a pin-1 mark (a Ø0.3 mm dot) beside the pin-1 pad, on the clear side
|
|
16
|
+
* farthest from the part's centre, for parts with a pin 1 and three or
|
|
17
|
+
* more pins.
|
|
18
|
+
*
|
|
19
|
+
* Nothing is drawn over a pad or a hole, so the fab need not clip silkscreen
|
|
20
|
+
* from the solder-mask openings.
|
|
21
|
+
*/
|
|
22
|
+
import type { CircuitElement } from "./runtime.ts";
|
|
23
|
+
import type { NameMap } from "./source.ts";
|
|
24
|
+
|
|
25
|
+
type Box = [number, number, number, number]; // x0, y0, x1, y1
|
|
26
|
+
|
|
27
|
+
const r4 = (v: number) => Math.round(v * 1e4) / 1e4;
|
|
28
|
+
const overlaps = (a: Box, b: Box, gap = 0) => a[0] < b[2] + gap && b[0] < a[2] + gap && a[1] < b[3] + gap && b[1] < a[3] + gap;
|
|
29
|
+
|
|
30
|
+
/** Axis-aligned box of a copper or hole element (rotated pads by their corners), grown by `grow`. */
|
|
31
|
+
function boxOf(e: CircuitElement, grow = 0): Box | undefined {
|
|
32
|
+
if (typeof e.x !== "number" || typeof e.y !== "number") return undefined;
|
|
33
|
+
let w = e.width ?? e.outer_width ?? e.outer_diameter ?? e.hole_diameter ?? (e.radius ?? 0) * 2;
|
|
34
|
+
let h = e.height ?? e.outer_height ?? e.outer_diameter ?? e.hole_diameter ?? (e.radius ?? 0) * 2;
|
|
35
|
+
const a = (((e.ccw_rotation as number) ?? 0) * Math.PI) / 180;
|
|
36
|
+
if (a) [w, h] = [Math.abs(w * Math.cos(a)) + Math.abs(h * Math.sin(a)), Math.abs(w * Math.sin(a)) + Math.abs(h * Math.cos(a))];
|
|
37
|
+
const m = (e.soldermask_margin ?? 0) + grow;
|
|
38
|
+
return [e.x - w / 2 - m, e.y - h / 2 - m, e.x + w / 2 + m, e.y + h / 2 + m];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Point in polygon (even-odd). */
|
|
42
|
+
function inside([x, y]: [number, number], poly: { x: number; y: number }[]): boolean {
|
|
43
|
+
let c = false;
|
|
44
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
45
|
+
const [a, b] = [poly[i], poly[j]];
|
|
46
|
+
if (a.y > y !== b.y > y && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x) c = !c;
|
|
47
|
+
}
|
|
48
|
+
return c;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function addSilkscreen(cj: CircuitElement[], map: NameMap, designators: Record<string, string> = {}): { circuitJson: CircuitElement[]; references: number; marks: number; omitted: string[] } {
|
|
52
|
+
const board = cj.find((e) => e.type === "pcb_board");
|
|
53
|
+
const outline: { x: number; y: number }[] | undefined = board?.outline;
|
|
54
|
+
const onBoard = (b: Box, margin = 0.2) => {
|
|
55
|
+
if (!outline) return true;
|
|
56
|
+
const pts: [number, number][] = [[b[0] - margin, b[1] - margin], [b[2] + margin, b[1] - margin], [b[2] + margin, b[3] + margin], [b[0] - margin, b[3] + margin], [(b[0] + b[2]) / 2, b[1] - margin], [(b[0] + b[2]) / 2, b[3] + margin], [b[0] - margin, (b[1] + b[3]) / 2], [b[2] + margin, (b[1] + b[3]) / 2]];
|
|
57
|
+
return pts.every((p) => inside(p, outline));
|
|
58
|
+
};
|
|
59
|
+
const nameOf = new Map(cj.filter((e) => e.type === "source_component").map((s) => [s.source_component_id, s.name as string]));
|
|
60
|
+
// what silkscreen must not touch, per side: every pad and hole (mask openings) and every courtyard
|
|
61
|
+
const side = (layer: string) => (e: CircuitElement) => e.layer === layer || (Array.isArray(e.layers) && e.layers.includes(layer)) || e.type === "pcb_hole" || e.type === "pcb_plated_hole";
|
|
62
|
+
const copper = (layer: string) => cj.filter((e) => ["pcb_smtpad", "pcb_plated_hole", "pcb_hole", "pcb_via"].includes(e.type)).filter(side(layer)).map((e) => boxOf(e, 0.1)).filter(Boolean) as Box[];
|
|
63
|
+
const courtyards = (layer: string) =>
|
|
64
|
+
cj
|
|
65
|
+
.filter((e) => e.type === "pcb_courtyard_rect" && e.layer === layer)
|
|
66
|
+
.map((e) => {
|
|
67
|
+
const a = ((e.ccw_rotation ?? 0) * Math.PI) / 180;
|
|
68
|
+
const w = Math.abs(e.width * Math.cos(a)) + Math.abs(e.height * Math.sin(a));
|
|
69
|
+
const h = Math.abs(e.width * Math.sin(a)) + Math.abs(e.height * Math.cos(a));
|
|
70
|
+
return { id: e.pcb_component_id as string, box: [e.center.x - w / 2, e.center.y - h / 2, e.center.x + w / 2, e.center.y + h / 2] as Box };
|
|
71
|
+
});
|
|
72
|
+
const obstacles = { top: copper("top"), bottom: copper("bottom") };
|
|
73
|
+
const yards = { top: courtyards("top"), bottom: courtyards("bottom") };
|
|
74
|
+
const placed: Record<string, Box[]> = { top: [], bottom: [] };
|
|
75
|
+
const add: CircuitElement[] = [];
|
|
76
|
+
const omitted: string[] = [];
|
|
77
|
+
let references = 0;
|
|
78
|
+
let marks = 0;
|
|
79
|
+
let n = 0;
|
|
80
|
+
|
|
81
|
+
for (const pc of cj.filter((e) => e.type === "pcb_component")) {
|
|
82
|
+
const m = map.components[nameOf.get(pc.source_component_id) ?? ""];
|
|
83
|
+
if (!m?.paste) continue; // built-in footprint strings: tscircuit's own silkscreen
|
|
84
|
+
const layer: "top" | "bottom" = pc.layer === "bottom" ? "bottom" : "top";
|
|
85
|
+
const own = yards[layer].find((y) => y.id === pc.pcb_component_id)?.box;
|
|
86
|
+
if (!own) continue;
|
|
87
|
+
|
|
88
|
+
// the reference: beside the courtyard, on the first place that is clear (each side's middle, then its ends)
|
|
89
|
+
let set = false;
|
|
90
|
+
for (const size of [0.8, 0.6]) {
|
|
91
|
+
const w = (designators[m.instance] ?? m.instance).length * size * 0.75 + 0.15;
|
|
92
|
+
const h = size + 0.15;
|
|
93
|
+
const [cx, cy] = [(own[0] + own[2]) / 2, (own[1] + own[3]) / 2];
|
|
94
|
+
const [hw, hh] = [(own[2] - own[0]) / 2, (own[3] - own[1]) / 2];
|
|
95
|
+
const along = (half: number, len: number) => [0, -(half - len / 2), half - len / 2].filter((d, i) => i === 0 || half > len / 2);
|
|
96
|
+
const spots: [number, number][] = [
|
|
97
|
+
...along(hw, w).map((d) => [cx + d, own[3] + h / 2 + 0.05] as [number, number]),
|
|
98
|
+
...along(hw, w).map((d) => [cx + d, own[1] - h / 2 - 0.05] as [number, number]),
|
|
99
|
+
...along(hh, h).map((d) => [own[2] + w / 2 + 0.05, cy + d] as [number, number]),
|
|
100
|
+
...along(hh, h).map((d) => [own[0] - w / 2 - 0.05, cy + d] as [number, number]),
|
|
101
|
+
];
|
|
102
|
+
for (const [x, y] of spots) {
|
|
103
|
+
const b: Box = [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
|
|
104
|
+
if (!onBoard(b)) continue;
|
|
105
|
+
if (obstacles[layer].some((o) => overlaps(b, o))) continue;
|
|
106
|
+
if (yards[layer].some((c) => c.id !== pc.pcb_component_id && overlaps(b, c.box))) continue;
|
|
107
|
+
if (placed[layer].some((p) => overlaps(b, p, 0.1))) continue;
|
|
108
|
+
placed[layer].push(b);
|
|
109
|
+
add.push({ type: "pcb_silkscreen_text", pcb_silkscreen_text_id: `pcb_silkscreen_text_ref_${n++}`, pcb_component_id: pc.pcb_component_id, layer, text: designators[m.instance] ?? m.instance, font: "tscircuit2024", font_size: size, anchor_alignment: "center", anchor_position: { x: r4(x), y: r4(y) }, ccw_rotation: 0 });
|
|
110
|
+
references++;
|
|
111
|
+
set = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (set) break;
|
|
115
|
+
}
|
|
116
|
+
if (!set) omitted.push(m.instance);
|
|
117
|
+
|
|
118
|
+
// the pin-1 mark: beside the pin-1 pad, on the side away from the part's centre
|
|
119
|
+
const own_pads = cj.filter((e) => (e.type === "pcb_smtpad" || e.type === "pcb_plated_hole") && e.pcb_component_id === pc.pcb_component_id);
|
|
120
|
+
const pin1 = own_pads.find((e) => (e.port_hints ?? []).includes("pin1"));
|
|
121
|
+
const pins = new Set(own_pads.flatMap((e) => (e.port_hints ?? []).filter((h: string) => /^pin\d+$/.test(h))));
|
|
122
|
+
if (pin1 && pins.size >= 3) {
|
|
123
|
+
const [mx, my] = [own_pads.reduce((a, e) => a + e.x, 0) / own_pads.length, own_pads.reduce((a, e) => a + e.y, 0) / own_pads.length];
|
|
124
|
+
const pb = boxOf(pin1)!;
|
|
125
|
+
const [px, py] = [(pb[0] + pb[2]) / 2, (pb[1] + pb[3]) / 2];
|
|
126
|
+
const [hx, hy] = [(pb[2] - pb[0]) / 2 + 0.35, (pb[3] - pb[1]) / 2 + 0.35];
|
|
127
|
+
const spots: [number, number][] = [];
|
|
128
|
+
for (const dx of [-1, 0, 1]) for (const dy of [-1, 0, 1]) if (dx || dy) spots.push([px + dx * hx, py + dy * hy]);
|
|
129
|
+
spots.sort((a, b) => Math.hypot(b[0] - mx, b[1] - my) - Math.hypot(a[0] - mx, a[1] - my));
|
|
130
|
+
for (const [x, y] of spots) {
|
|
131
|
+
const dot: Box = [x - 0.15, y - 0.15, x + 0.15, y + 0.15];
|
|
132
|
+
if (obstacles[layer].some((o) => overlaps(dot, o)) || !onBoard(dot, 0) || placed[layer].some((p) => overlaps(dot, p))) continue;
|
|
133
|
+
add.push({ type: "pcb_silkscreen_circle", pcb_silkscreen_circle_id: `pcb_silkscreen_circle_pin1_${n++}`, pcb_component_id: pc.pcb_component_id, layer, center: { x: r4(x), y: r4(y) }, radius: 0.15, stroke_width: 0.15 });
|
|
134
|
+
placed[layer].push(dot);
|
|
135
|
+
marks++;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { circuitJson: [...cj, ...add], references, marks, omitted };
|
|
141
|
+
}
|