@tscircuit/core 0.0.1 → 0.0.3

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/index.cjs CHANGED
@@ -484,9 +484,9 @@ var ParseStatus = class _ParseStatus {
484
484
  }
485
485
  return { status: status.value, value: arrayValue };
486
486
  }
487
- static async mergeObjectAsync(status, pairs) {
487
+ static async mergeObjectAsync(status, pairs2) {
488
488
  const syncPairs = [];
489
- for (const pair of pairs) {
489
+ for (const pair of pairs2) {
490
490
  const key = await pair.key;
491
491
  const value = await pair.value;
492
492
  syncPairs.push({
@@ -496,9 +496,9 @@ var ParseStatus = class _ParseStatus {
496
496
  }
497
497
  return _ParseStatus.mergeObjectSync(status, syncPairs);
498
498
  }
499
- static mergeObjectSync(status, pairs) {
499
+ static mergeObjectSync(status, pairs2) {
500
500
  const finalObject = {};
501
- for (const pair of pairs) {
501
+ for (const pair of pairs2) {
502
502
  const { key, value } = pair;
503
503
  if (key.status === "aborted")
504
504
  return INVALID;
@@ -2224,11 +2224,11 @@ var ZodObject = class _ZodObject extends ZodType {
2224
2224
  }
2225
2225
  }
2226
2226
  }
2227
- const pairs = [];
2227
+ const pairs2 = [];
2228
2228
  for (const key of shapeKeys) {
2229
2229
  const keyValidator = shape[key];
2230
2230
  const value = ctx.data[key];
2231
- pairs.push({
2231
+ pairs2.push({
2232
2232
  key: { status: "valid", value: key },
2233
2233
  value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)),
2234
2234
  alwaysSet: key in ctx.data
@@ -2238,7 +2238,7 @@ var ZodObject = class _ZodObject extends ZodType {
2238
2238
  const unknownKeys = this._def.unknownKeys;
2239
2239
  if (unknownKeys === "passthrough") {
2240
2240
  for (const key of extraKeys) {
2241
- pairs.push({
2241
+ pairs2.push({
2242
2242
  key: { status: "valid", value: key },
2243
2243
  value: { status: "valid", value: ctx.data[key] }
2244
2244
  });
@@ -2259,7 +2259,7 @@ var ZodObject = class _ZodObject extends ZodType {
2259
2259
  const catchall = this._def.catchall;
2260
2260
  for (const key of extraKeys) {
2261
2261
  const value = ctx.data[key];
2262
- pairs.push({
2262
+ pairs2.push({
2263
2263
  key: { status: "valid", value: key },
2264
2264
  value: catchall._parse(
2265
2265
  new ParseInputLazyPath(ctx, value, ctx.path, key)
@@ -2272,7 +2272,7 @@ var ZodObject = class _ZodObject extends ZodType {
2272
2272
  if (ctx.common.async) {
2273
2273
  return Promise.resolve().then(async () => {
2274
2274
  const syncPairs = [];
2275
- for (const pair of pairs) {
2275
+ for (const pair of pairs2) {
2276
2276
  const key = await pair.key;
2277
2277
  const value = await pair.value;
2278
2278
  syncPairs.push({
@@ -2286,7 +2286,7 @@ var ZodObject = class _ZodObject extends ZodType {
2286
2286
  return ParseStatus.mergeObjectSync(status, syncPairs);
2287
2287
  });
2288
2288
  } else {
2289
- return ParseStatus.mergeObjectSync(status, pairs);
2289
+ return ParseStatus.mergeObjectSync(status, pairs2);
2290
2290
  }
2291
2291
  }
2292
2292
  get shape() {
@@ -2900,20 +2900,20 @@ var ZodRecord = class _ZodRecord extends ZodType {
2900
2900
  });
2901
2901
  return INVALID;
2902
2902
  }
2903
- const pairs = [];
2903
+ const pairs2 = [];
2904
2904
  const keyType = this._def.keyType;
2905
2905
  const valueType = this._def.valueType;
2906
2906
  for (const key in ctx.data) {
2907
- pairs.push({
2907
+ pairs2.push({
2908
2908
  key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)),
2909
2909
  value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)),
2910
2910
  alwaysSet: key in ctx.data
2911
2911
  });
2912
2912
  }
2913
2913
  if (ctx.common.async) {
2914
- return ParseStatus.mergeObjectAsync(status, pairs);
2914
+ return ParseStatus.mergeObjectAsync(status, pairs2);
2915
2915
  } else {
2916
- return ParseStatus.mergeObjectSync(status, pairs);
2916
+ return ParseStatus.mergeObjectSync(status, pairs2);
2917
2917
  }
2918
2918
  }
2919
2919
  get element() {
@@ -2955,7 +2955,7 @@ var ZodMap = class extends ZodType {
2955
2955
  }
2956
2956
  const keyType = this._def.keyType;
2957
2957
  const valueType = this._def.valueType;
2958
- const pairs = [...ctx.data.entries()].map(([key, value], index) => {
2958
+ const pairs2 = [...ctx.data.entries()].map(([key, value], index) => {
2959
2959
  return {
2960
2960
  key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])),
2961
2961
  value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"]))
@@ -2964,7 +2964,7 @@ var ZodMap = class extends ZodType {
2964
2964
  if (ctx.common.async) {
2965
2965
  const finalMap = /* @__PURE__ */ new Map();
2966
2966
  return Promise.resolve().then(async () => {
2967
- for (const pair of pairs) {
2967
+ for (const pair of pairs2) {
2968
2968
  const key = await pair.key;
2969
2969
  const value = await pair.value;
2970
2970
  if (key.status === "aborted" || value.status === "aborted") {
@@ -2979,7 +2979,7 @@ var ZodMap = class extends ZodType {
2979
2979
  });
2980
2980
  } else {
2981
2981
  const finalMap = /* @__PURE__ */ new Map();
2982
- for (const pair of pairs) {
2982
+ for (const pair of pairs2) {
2983
2983
  const key = pair.key;
2984
2984
  const value = pair.value;
2985
2985
  if (key.status === "aborted" || value.status === "aborted") {
@@ -4161,7 +4161,7 @@ var PrimitiveComponent = class extends Renderable {
4161
4161
  * components
4162
4162
  */
4163
4163
  computePcbPropsTransform() {
4164
- return (0, import_transformation_matrix.compose)((0, import_transformation_matrix.translate)(this.props.pcbX, this.props.pcbY));
4164
+ return (0, import_transformation_matrix.compose)((0, import_transformation_matrix.translate)(this.props.pcbX ?? 0, this.props.pcbY ?? 0));
4165
4165
  }
4166
4166
  /**
4167
4167
  * Compute a transformation matrix combining all parent transforms for PCB
@@ -4293,6 +4293,16 @@ var PrimitiveComponent = class extends Renderable {
4293
4293
  }
4294
4294
  return this.selectAll(selector)[0] ?? null;
4295
4295
  }
4296
+ getAvailablePcbLayers() {
4297
+ if (this.isPcbPrimitive) {
4298
+ if (this.props.layer) return [this.props.layer];
4299
+ if (this.componentName === "PlatedHole") {
4300
+ return ["top", "bottom"];
4301
+ }
4302
+ return [];
4303
+ }
4304
+ return [];
4305
+ }
4296
4306
  getDescendants() {
4297
4307
  const descendants = [];
4298
4308
  for (const child of this.children) {
@@ -4406,6 +4416,11 @@ var Port = class extends PrimitiveComponent {
4406
4416
  getPortSelector() {
4407
4417
  return `.${this.parent?.props.name} > port.${this.props.name}`;
4408
4418
  }
4419
+ getAvailablePcbLayers() {
4420
+ return Array.from(
4421
+ new Set(this.matchedComponents.flatMap((c) => c.getAvailablePcbLayers()))
4422
+ );
4423
+ }
4409
4424
  doInitialSourceRender() {
4410
4425
  const { db } = this.project;
4411
4426
  const { _parsedProps: props } = this;
@@ -5104,7 +5119,119 @@ var projectPointInDirection = (point, direction, distance) => {
5104
5119
  }
5105
5120
  };
5106
5121
 
5122
+ // lib/utils/autorouting/findPossibleTraceLayerCombinations.ts
5123
+ var LAYER_SELECTION_PREFERENCE = ["top", "bottom", "inner1", "inner2"];
5124
+ var findPossibleTraceLayerCombinations = (hints, layer_path = []) => {
5125
+ const candidates = [];
5126
+ if (layer_path.length === 0) {
5127
+ const starting_layers = hints[0].layers;
5128
+ for (const layer of starting_layers) {
5129
+ candidates.push(
5130
+ ...findPossibleTraceLayerCombinations(hints.slice(1), [layer])
5131
+ );
5132
+ }
5133
+ return candidates;
5134
+ }
5135
+ if (hints.length === 0) return [];
5136
+ const current_hint = hints[0];
5137
+ const is_possibly_via = current_hint.via || current_hint.optional_via;
5138
+ const last_layer = layer_path[layer_path.length - 1];
5139
+ if (hints.length === 1) {
5140
+ const last_hint = current_hint;
5141
+ if (last_hint.layers && is_possibly_via) {
5142
+ return last_hint.layers.map((layer) => ({
5143
+ layer_path: [...layer_path, layer]
5144
+ }));
5145
+ }
5146
+ if (last_hint.layers?.includes(last_layer)) {
5147
+ return [{ layer_path: [...layer_path, last_layer] }];
5148
+ }
5149
+ return [];
5150
+ }
5151
+ if (!is_possibly_via) {
5152
+ if (current_hint.layers) {
5153
+ if (!current_hint.layers.includes(last_layer)) {
5154
+ return [];
5155
+ }
5156
+ }
5157
+ return findPossibleTraceLayerCombinations(
5158
+ hints.slice(1),
5159
+ layer_path.concat([last_layer])
5160
+ );
5161
+ }
5162
+ const candidate_next_layers = (current_hint.optional_via ? LAYER_SELECTION_PREFERENCE : LAYER_SELECTION_PREFERENCE.filter((layer) => layer !== last_layer)).filter(
5163
+ (layer) => !current_hint.layers || current_hint.layers?.includes(layer)
5164
+ );
5165
+ for (const candidate_next_layer of candidate_next_layers) {
5166
+ candidates.push(
5167
+ ...findPossibleTraceLayerCombinations(
5168
+ hints.slice(1),
5169
+ layer_path.concat(candidate_next_layer)
5170
+ )
5171
+ );
5172
+ }
5173
+ return candidates;
5174
+ };
5175
+
5176
+ // lib/utils/pairs.ts
5177
+ function pairs(arr) {
5178
+ const result = [];
5179
+ for (let i = 0; i < arr.length - 1; i++) {
5180
+ result.push([arr[i], arr[i + 1]]);
5181
+ }
5182
+ return result;
5183
+ }
5184
+
5185
+ // lib/utils/autorouting/mergeRoutes.ts
5186
+ function pdist(a, b) {
5187
+ return Math.hypot(a.x - b.x, a.y - b.y);
5188
+ }
5189
+ var mergeRoutes = (routes) => {
5190
+ if (routes.some((r) => r.length === 0)) {
5191
+ throw new Error("Cannot merge routes with zero length");
5192
+ }
5193
+ const merged = [];
5194
+ const first_route_fp = routes[0][0];
5195
+ const first_route_lp = routes[0][routes[0].length - 1];
5196
+ const second_route_fp = routes[1][0];
5197
+ const second_route_lp = routes[1][routes[1].length - 1];
5198
+ const best_reverse_dist = Math.min(
5199
+ pdist(first_route_fp, second_route_fp),
5200
+ pdist(first_route_fp, second_route_lp)
5201
+ );
5202
+ const best_normal_dist = Math.min(
5203
+ pdist(first_route_lp, second_route_fp),
5204
+ pdist(first_route_lp, second_route_lp)
5205
+ );
5206
+ if (best_reverse_dist < best_normal_dist) {
5207
+ merged.push(...routes[0].reverse());
5208
+ } else {
5209
+ merged.push(...routes[0]);
5210
+ }
5211
+ for (let i = 1; i < routes.length; i++) {
5212
+ const last_merged_point = merged[merged.length - 1];
5213
+ const next_route = routes[i];
5214
+ const next_first_point = next_route[0];
5215
+ const next_last_point = next_route[next_route.length - 1];
5216
+ const distance_to_first = pdist(last_merged_point, next_first_point);
5217
+ const distance_to_last = pdist(last_merged_point, next_last_point);
5218
+ if (distance_to_first < distance_to_last) {
5219
+ merged.push(...next_route);
5220
+ } else {
5221
+ merged.push(...next_route.reverse());
5222
+ }
5223
+ }
5224
+ return merged;
5225
+ };
5226
+
5107
5227
  // lib/components/primitive-components/Trace.ts
5228
+ var portToObjective = (port) => {
5229
+ const portPosition = port.getGlobalPcbPosition();
5230
+ return {
5231
+ ...portPosition,
5232
+ layers: port.getAvailablePcbLayers()
5233
+ };
5234
+ };
5108
5235
  var Trace = class extends PrimitiveComponent {
5109
5236
  source_trace_id = null;
5110
5237
  pcb_trace_id = null;
@@ -5181,9 +5308,91 @@ searched component ${targetComponent.getString()}, which has ports:${targetCompo
5181
5308
  (elm) => elm.type === "pcb_smtpad" || elm.type === "pcb_trace" || elm.type === "pcb_plated_hole" || elm.type === "pcb_hole" || elm.type === "source_port" || elm.type === "pcb_port"
5182
5309
  );
5183
5310
  const source_trace = db.source_trace.get(this.source_trace_id);
5184
- const { solution } = (0, import_infgrid_ijump_astar.autoroute)(pcbElements.concat([source_trace]));
5185
- const pcb_trace = solution[0];
5186
- db.pcb_trace.insert(pcb_trace);
5311
+ const hints = ports.flatMap(
5312
+ ({ port }) => port.matchedComponents.filter((c) => c.componentName === "TraceHint")
5313
+ );
5314
+ const pcbRouteHints = (this._parsedProps.pcbRouteHints ?? []).concat(
5315
+ hints.flatMap((h) => h.getPcbRouteHints())
5316
+ );
5317
+ if (ports.length > 2) {
5318
+ this.renderError(
5319
+ `Trace has more than two ports (${ports.map((p) => p.port.getString()).join(
5320
+ ", "
5321
+ )}), routing between more than two ports for a single trace is not implemented`
5322
+ );
5323
+ return;
5324
+ }
5325
+ if (pcbRouteHints.length === 0) {
5326
+ const { solution } = (0, import_infgrid_ijump_astar.autoroute)(pcbElements.concat([source_trace]));
5327
+ const pcb_trace2 = solution[0];
5328
+ db.pcb_trace.insert(pcb_trace2);
5329
+ this.pcb_trace_id = pcb_trace2.pcb_trace_id;
5330
+ return;
5331
+ }
5332
+ const orderedRouteObjectives = [
5333
+ portToObjective(ports[0].port),
5334
+ ...pcbRouteHints,
5335
+ portToObjective(ports[1].port)
5336
+ ];
5337
+ const candidateLayerCombinations = findPossibleTraceLayerCombinations(
5338
+ orderedRouteObjectives
5339
+ );
5340
+ if (candidateLayerCombinations.length === 0) {
5341
+ this.renderError(
5342
+ `Could not find a common layer (using hints) for trace ${this.getString()}`
5343
+ );
5344
+ }
5345
+ const obstacles = (0, import_infgrid_ijump_astar.getObstaclesFromSoup)(this.project.db.toArray());
5346
+ (0, import_infgrid_ijump_astar.markObstaclesAsConnected)(
5347
+ obstacles,
5348
+ orderedRouteObjectives,
5349
+ this.source_trace_id
5350
+ );
5351
+ const candidateLayerSelections = candidateLayerCombinations[0].layer_path;
5352
+ const orderedRoutePoints = orderedRouteObjectives.map((t, idx) => {
5353
+ if (t.via) {
5354
+ return {
5355
+ ...t,
5356
+ via_to_layer: candidateLayerSelections[idx]
5357
+ };
5358
+ }
5359
+ return { ...t, layers: [candidateLayerSelections[idx]] };
5360
+ });
5361
+ const routes = [];
5362
+ for (const [a, b] of pairs(orderedRoutePoints)) {
5363
+ const BOUNDS_MARGIN = 2;
5364
+ const ijump = new import_infgrid_ijump_astar.IJumpAutorouter({
5365
+ input: {
5366
+ obstacles,
5367
+ connections: [
5368
+ {
5369
+ name: this.source_trace_id,
5370
+ pointsToConnect: [a, b]
5371
+ }
5372
+ ],
5373
+ layerCount: 1,
5374
+ bounds: {
5375
+ minX: Math.min(a.x, b.x) - BOUNDS_MARGIN,
5376
+ maxX: Math.max(a.x, b.x) + BOUNDS_MARGIN,
5377
+ minY: Math.min(a.y, b.y) - BOUNDS_MARGIN,
5378
+ maxY: Math.max(a.y, b.y) + BOUNDS_MARGIN
5379
+ }
5380
+ }
5381
+ });
5382
+ const traces = ijump.solveAndMapToTraces();
5383
+ if (traces.length === 0) {
5384
+ this.renderError(
5385
+ `Could not find a route between ${a.x}, ${a.y} and ${b.x}, ${b.y}`
5386
+ );
5387
+ return;
5388
+ }
5389
+ const [trace] = traces;
5390
+ routes.push(trace.route);
5391
+ }
5392
+ const pcb_trace = db.pcb_trace.insert({
5393
+ route: mergeRoutes(routes),
5394
+ source_trace_id: this.source_trace_id
5395
+ });
5187
5396
  this.pcb_trace_id = pcb_trace.pcb_trace_id;
5188
5397
  }
5189
5398
  doInitialSchematicTraceRender() {
@@ -5252,7 +5461,47 @@ searched component ${targetComponent.getString()}, which has ports:${targetCompo
5252
5461
 
5253
5462
  // lib/components/primitive-components/TraceHint.ts
5254
5463
  var import_props7 = require("@tscircuit/props");
5464
+ var import_transformation_matrix4 = require("transformation-matrix");
5255
5465
  var TraceHint = class extends PrimitiveComponent {
5466
+ matchedPort = null;
5467
+ doInitialPortMatching() {
5468
+ const { db } = this.project;
5469
+ const { _parsedProps: props, parent } = this;
5470
+ if (!parent) return;
5471
+ if (parent.componentName === "Trace") {
5472
+ this.renderError(
5473
+ `Port inference inside trace is not yet supported (${this})`
5474
+ );
5475
+ return;
5476
+ }
5477
+ if (!parent) throw new Error("TraceHint has no parent");
5478
+ if (!props.for) {
5479
+ this.renderError(`TraceHint has no for property (${this})`);
5480
+ return;
5481
+ }
5482
+ const port = parent.selectOne(props.for, { type: "port" });
5483
+ if (!port) {
5484
+ this.renderError(
5485
+ `${this} could not find port for selector "${props.for}"`
5486
+ );
5487
+ }
5488
+ this.matchedPort = port;
5489
+ port.registerMatch(this);
5490
+ }
5491
+ getPcbRouteHints() {
5492
+ const { _parsedProps: props } = this;
5493
+ const offsets = props.offset ? [props.offset] : props.offsets;
5494
+ if (!offsets) return [];
5495
+ const globalTransform = this.computePcbGlobalTransform();
5496
+ return offsets.map(
5497
+ (offset) => ({
5498
+ ...(0, import_transformation_matrix4.applyToPoint)(globalTransform, offset),
5499
+ via: offset.via,
5500
+ to_layer: offset.to_layer,
5501
+ trace_width: offset.trace_width
5502
+ })
5503
+ );
5504
+ }
5256
5505
  };
5257
5506
 
5258
5507
  // lib/components/primitive-components/Group.ts
@@ -5268,7 +5517,7 @@ var Group = class extends PrimitiveComponent {
5268
5517
  // lib/Project.ts
5269
5518
  var import_soup_util = require("@tscircuit/soup-util");
5270
5519
  var import_react4 = require("react");
5271
- var import_transformation_matrix4 = require("transformation-matrix");
5520
+ var import_transformation_matrix5 = require("transformation-matrix");
5272
5521
  var Project = class {
5273
5522
  rootComponent = null;
5274
5523
  children;
@@ -5324,10 +5573,16 @@ var Project = class {
5324
5573
  return this.getSoup();
5325
5574
  }
5326
5575
  computeGlobalSchematicTransform() {
5327
- return (0, import_transformation_matrix4.identity)();
5576
+ return (0, import_transformation_matrix5.identity)();
5328
5577
  }
5329
5578
  computeGlobalPcbTransform() {
5330
- return (0, import_transformation_matrix4.identity)();
5579
+ return (0, import_transformation_matrix5.identity)();
5580
+ }
5581
+ selectAll(selector) {
5582
+ return this.rootComponent?.selectAll(selector) ?? [];
5583
+ }
5584
+ selectOne(selector) {
5585
+ return this.rootComponent?.selectOne(selector) ?? null;
5331
5586
  }
5332
5587
  };
5333
5588
 
package/lib/Project.ts CHANGED
@@ -81,4 +81,12 @@ export class Project {
81
81
  computeGlobalPcbTransform(): Matrix {
82
82
  return identity()
83
83
  }
84
+
85
+ selectAll(selector: string): PrimitiveComponent[] {
86
+ return this.rootComponent?.selectAll(selector) ?? []
87
+ }
88
+
89
+ selectOne(selector: string): PrimitiveComponent | null {
90
+ return this.rootComponent?.selectOne(selector) ?? null
91
+ }
84
92
  }
@@ -95,7 +95,7 @@ export abstract class PrimitiveComponent<
95
95
  */
96
96
  computePcbPropsTransform(): Matrix {
97
97
  // TODO rotations
98
- return compose(translate(this.props.pcbX, this.props.pcbY))
98
+ return compose(translate(this.props.pcbX ?? 0, this.props.pcbY ?? 0))
99
99
  }
100
100
 
101
101
  /**
@@ -268,6 +268,17 @@ export abstract class PrimitiveComponent<
268
268
  return this.selectAll(selector)[0] ?? null
269
269
  }
270
270
 
271
+ getAvailablePcbLayers(): string[] {
272
+ if (this.isPcbPrimitive) {
273
+ if (this.props.layer) return [this.props.layer]
274
+ if (this.componentName === "PlatedHole") {
275
+ return ["top", "bottom"] // TODO derive layers from parent
276
+ }
277
+ return []
278
+ }
279
+ return []
280
+ }
281
+
271
282
  getDescendants(): PrimitiveComponent[] {
272
283
  const descendants: PrimitiveComponent[] = []
273
284
  for (const child of this.children) {
@@ -88,6 +88,11 @@ export class Port extends PrimitiveComponent<typeof portProps> {
88
88
  return `.${this.parent?.props.name} > port.${this.props.name}`
89
89
  // return `#${this.props.id}`
90
90
  }
91
+ getAvailablePcbLayers(): string[] {
92
+ return Array.from(
93
+ new Set(this.matchedComponents.flatMap((c) => c.getAvailablePcbLayers())),
94
+ )
95
+ }
91
96
 
92
97
  doInitialSourceRender(): void {
93
98
  const { db } = this.project!
@@ -1,8 +1,18 @@
1
1
  import { traceProps } from "@tscircuit/props"
2
2
  import { PrimitiveComponent } from "../base-components/PrimitiveComponent"
3
3
  import type { Port } from "./Port"
4
- import { IJumpAutorouter, autoroute } from "@tscircuit/infgrid-ijump-astar"
5
- import type { AnySoupElement, SchematicTrace } from "@tscircuit/soup"
4
+ import {
5
+ IJumpAutorouter,
6
+ autoroute,
7
+ getObstaclesFromSoup,
8
+ markObstaclesAsConnected,
9
+ } from "@tscircuit/infgrid-ijump-astar"
10
+ import type {
11
+ AnySoupElement,
12
+ PCBTrace,
13
+ RouteHintPoint,
14
+ SchematicTrace,
15
+ } from "@tscircuit/soup"
6
16
  import type {
7
17
  Obstacle,
8
18
  SimpleRouteConnection,
@@ -10,6 +20,22 @@ import type {
10
20
  } from "lib/utils/autorouting/SimpleRouteJson"
11
21
  import { computeObstacleBounds } from "lib/utils/autorouting/computeObstacleBounds"
12
22
  import { projectPointInDirection } from "lib/utils/projectPointInDirection"
23
+ import type { TraceHint } from "./TraceHint"
24
+ import { findPossibleTraceLayerCombinations } from "lib/utils/autorouting/findPossibleTraceLayerCombinations"
25
+ import { pairs } from "lib/utils/pairs"
26
+ import { mergeRoutes } from "lib/utils/autorouting/mergeRoutes"
27
+
28
+ type PcbRouteObjective =
29
+ | RouteHintPoint
30
+ | { layers: string[]; x: number; y: number; via?: boolean }
31
+
32
+ const portToObjective = (port: Port): PcbRouteObjective => {
33
+ const portPosition = port.getGlobalPcbPosition()
34
+ return {
35
+ ...portPosition,
36
+ layers: port.getAvailablePcbLayers(),
37
+ }
38
+ }
13
39
 
14
40
  export class Trace extends PrimitiveComponent<typeof traceProps> {
15
41
  source_trace_id: string | null = null
@@ -124,14 +150,119 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
124
150
 
125
151
  const source_trace = db.source_trace.get(this.source_trace_id!)!
126
152
 
127
- const { solution } = autoroute(pcbElements.concat([source_trace]))
153
+ const hints = ports.flatMap(({ port }) =>
154
+ port.matchedComponents.filter((c) => c.componentName === "TraceHint"),
155
+ ) as TraceHint[]
156
+
157
+ const pcbRouteHints = (this._parsedProps.pcbRouteHints ?? []).concat(
158
+ hints.flatMap((h) => h.getPcbRouteHints()),
159
+ )
160
+
161
+ if (ports.length > 2) {
162
+ this.renderError(
163
+ `Trace has more than two ports (${ports
164
+ .map((p) => p.port.getString())
165
+ .join(
166
+ ", ",
167
+ )}), routing between more than two ports for a single trace is not implemented`,
168
+ )
169
+ return
170
+ }
128
171
 
129
- // TODO for some reason, the solution gets duplicated. Seems to be an issue
130
- // with the ijump-astar function
131
- const pcb_trace = solution[0]
172
+ if (pcbRouteHints.length === 0) {
173
+ const { solution } = autoroute(pcbElements.concat([source_trace]))
174
+ // TODO for some reason, the solution gets duplicated inside ijump-astar
175
+ const pcb_trace = solution[0]
176
+ db.pcb_trace.insert(pcb_trace)
177
+ this.pcb_trace_id = pcb_trace.pcb_trace_id
178
+ return
179
+ }
132
180
 
133
- db.pcb_trace.insert(pcb_trace)
181
+ // When we have hints, we have to order the hints then route between each
182
+ // terminal of the trace and the hints
183
+ // TODO order based on proximity to ports
184
+ const orderedRouteObjectives: PcbRouteObjective[] = [
185
+ portToObjective(ports[0].port),
186
+ ...pcbRouteHints,
187
+ portToObjective(ports[1].port),
188
+ ]
189
+
190
+ // Hints can indicate where there should be a via, but the layer is allowed
191
+ // to be unspecified, therefore we need to find possible layer combinations
192
+ // to go to each hint and still route to the start and end points
193
+ const candidateLayerCombinations = findPossibleTraceLayerCombinations(
194
+ orderedRouteObjectives,
195
+ )
196
+
197
+ if (candidateLayerCombinations.length === 0) {
198
+ this.renderError(
199
+ `Could not find a common layer (using hints) for trace ${this.getString()}`,
200
+ )
201
+ }
134
202
 
203
+ // Cache the PCB obstacles, they'll be needed for each segment between
204
+ // ports/hints
205
+ const obstacles = getObstaclesFromSoup(this.project!.db.toArray())
206
+ markObstaclesAsConnected(
207
+ obstacles,
208
+ orderedRouteObjectives,
209
+ this.source_trace_id!,
210
+ )
211
+
212
+ // TODO explore all candidate layer combinations if one fails
213
+ const candidateLayerSelections = candidateLayerCombinations[0].layer_path
214
+
215
+ /**
216
+ * Apply the candidate layer selections to the route objectives, now we
217
+ * have a set of points that have definite layers
218
+ */
219
+ const orderedRoutePoints = orderedRouteObjectives.map((t, idx) => {
220
+ if (t.via) {
221
+ return {
222
+ ...t,
223
+ via_to_layer: candidateLayerSelections[idx],
224
+ }
225
+ }
226
+ return { ...t, layers: [candidateLayerSelections[idx]] }
227
+ })
228
+
229
+ const routes: PCBTrace["route"][] = []
230
+ for (const [a, b] of pairs(orderedRoutePoints)) {
231
+ const BOUNDS_MARGIN = 2 //mm
232
+ const ijump = new IJumpAutorouter({
233
+ input: {
234
+ obstacles,
235
+ connections: [
236
+ {
237
+ name: this.source_trace_id!,
238
+ pointsToConnect: [a, b],
239
+ },
240
+ ],
241
+ layerCount: 1,
242
+ bounds: {
243
+ minX: Math.min(a.x, b.x) - BOUNDS_MARGIN,
244
+ maxX: Math.max(a.x, b.x) + BOUNDS_MARGIN,
245
+ minY: Math.min(a.y, b.y) - BOUNDS_MARGIN,
246
+ maxY: Math.max(a.y, b.y) + BOUNDS_MARGIN,
247
+ },
248
+ },
249
+ })
250
+ const traces = ijump.solveAndMapToTraces()
251
+ if (traces.length === 0) {
252
+ this.renderError(
253
+ `Could not find a route between ${a.x}, ${a.y} and ${b.x}, ${b.y}`,
254
+ )
255
+ return
256
+ }
257
+ // TODO ijump returns multiple traces for some reason
258
+ const [trace] = traces as PCBTrace[]
259
+ routes.push(trace.route)
260
+ }
261
+
262
+ const pcb_trace = db.pcb_trace.insert({
263
+ route: mergeRoutes(routes),
264
+ source_trace_id: this.source_trace_id!,
265
+ })
135
266
  this.pcb_trace_id = pcb_trace.pcb_trace_id
136
267
  }
137
268
 
@@ -1,4 +1,60 @@
1
1
  import { PrimitiveComponent } from "lib/components/base-components/PrimitiveComponent"
2
- import { traceProps } from "@tscircuit/props"
2
+ import { traceHintProps } from "@tscircuit/props"
3
+ import type { Port } from "./Port"
4
+ import type { RouteHintPoint } from "@tscircuit/soup"
5
+ import { applyToPoint } from "transformation-matrix"
3
6
 
4
- export class TraceHint extends PrimitiveComponent<typeof traceProps> {}
7
+ export class TraceHint extends PrimitiveComponent<typeof traceHintProps> {
8
+ matchedPort: Port | null = null
9
+
10
+ doInitialPortMatching(): void {
11
+ const { db } = this.project!
12
+ const { _parsedProps: props, parent } = this
13
+
14
+ if (!parent) return
15
+
16
+ if (parent.componentName === "Trace") {
17
+ this.renderError(
18
+ `Port inference inside trace is not yet supported (${this})`,
19
+ )
20
+ return
21
+ }
22
+
23
+ if (!parent) throw new Error("TraceHint has no parent")
24
+
25
+ if (!props.for) {
26
+ this.renderError(`TraceHint has no for property (${this})`)
27
+ return
28
+ }
29
+
30
+ const port = parent.selectOne(props.for, { type: "port" }) as Port
31
+
32
+ if (!port) {
33
+ this.renderError(
34
+ `${this} could not find port for selector "${props.for}"`,
35
+ )
36
+ }
37
+
38
+ this.matchedPort = port
39
+ port.registerMatch(this)
40
+ }
41
+
42
+ getPcbRouteHints(): Array<RouteHintPoint> {
43
+ const { _parsedProps: props } = this
44
+
45
+ const offsets = props.offset ? [props.offset] : props.offsets
46
+
47
+ if (!offsets) return []
48
+
49
+ const globalTransform = this.computePcbGlobalTransform()
50
+
51
+ return offsets.map(
52
+ (offset): RouteHintPoint => ({
53
+ ...applyToPoint(globalTransform, offset),
54
+ via: offset.via,
55
+ to_layer: offset.to_layer,
56
+ trace_width: offset.trace_width,
57
+ }),
58
+ )
59
+ }
60
+ }
@@ -0,0 +1,102 @@
1
+ interface Hint {
2
+ via?: boolean
3
+ optional_via?: boolean
4
+ layers?: Array<string>
5
+ }
6
+
7
+ const LAYER_SELECTION_PREFERENCE = ["top", "bottom", "inner1", "inner2"]
8
+
9
+ /**
10
+ * ORDERING OF CANDIDATES: Example:
11
+ * top -> top -> bottom -> bottom
12
+ * bottom -> bottom -> top -> top
13
+ * top -> bottom -> bottom -> top
14
+ * bottom -> top -> top -> bottom
15
+ */
16
+ interface CandidateTraceLayerCombination {
17
+ layer_path: string[]
18
+ }
19
+
20
+ /**
21
+ * Given a set of hints that contain layers or indication there should be a via,
22
+ * this function returns all combinations of layers that could be traversed.
23
+ *
24
+ * EXAMPLE 1:
25
+ * INPUT:
26
+ * [top,bottom] -> unspecified -> unspecified/via -> [top, bottom]
27
+ * OUTPUT:
28
+ * top -> top -> bottom -> bottom
29
+ * bottom -> bottom -> top -> top *
30
+ * EXAMPLE 2:
31
+ * INPUT:
32
+ * [top,bottom] -> unspecified -> unspecified/via -> unspecified/via -> [top, bottom]
33
+ * OUTPUT:
34
+ * top -> top -> bottom -> top -> top
35
+ * bottom -> bottom -> top-> bottom -> bottom
36
+ * bottom -> bottom -> inner-1 -> inner-1 -> bottom
37
+ */
38
+ export const findPossibleTraceLayerCombinations = (
39
+ hints: Hint[],
40
+ layer_path: string[] = [],
41
+ ): CandidateTraceLayerCombination[] => {
42
+ const candidates: CandidateTraceLayerCombination[] = []
43
+ if (layer_path.length === 0) {
44
+ const starting_layers = hints[0].layers!
45
+ for (const layer of starting_layers) {
46
+ candidates.push(
47
+ ...findPossibleTraceLayerCombinations(hints.slice(1), [layer]),
48
+ )
49
+ }
50
+ return candidates
51
+ }
52
+
53
+ if (hints.length === 0) return []
54
+ const current_hint = hints[0]
55
+ const is_possibly_via = current_hint.via || current_hint.optional_via
56
+ const last_layer = layer_path[layer_path.length - 1]
57
+
58
+ if (hints.length === 1) {
59
+ const last_hint = current_hint
60
+ if (last_hint.layers && is_possibly_via) {
61
+ return last_hint.layers.map((layer) => ({
62
+ layer_path: [...layer_path, layer],
63
+ }))
64
+ }
65
+ if (last_hint.layers?.includes(last_layer)) {
66
+ return [{ layer_path: [...layer_path, last_layer] }]
67
+ }
68
+ return []
69
+ }
70
+
71
+ if (!is_possibly_via) {
72
+ if (current_hint.layers) {
73
+ if (!current_hint.layers.includes(last_layer)) {
74
+ return []
75
+ }
76
+ }
77
+
78
+ return findPossibleTraceLayerCombinations(
79
+ hints.slice(1),
80
+ layer_path.concat([last_layer]),
81
+ )
82
+ }
83
+
84
+ const candidate_next_layers = (
85
+ current_hint.optional_via
86
+ ? LAYER_SELECTION_PREFERENCE
87
+ : LAYER_SELECTION_PREFERENCE.filter((layer) => layer !== last_layer)
88
+ ).filter(
89
+ (layer) => !current_hint.layers || current_hint.layers?.includes(layer),
90
+ )
91
+
92
+ for (const candidate_next_layer of candidate_next_layers) {
93
+ candidates.push(
94
+ ...findPossibleTraceLayerCombinations(
95
+ hints.slice(1),
96
+ layer_path.concat(candidate_next_layer),
97
+ ),
98
+ )
99
+ }
100
+
101
+ return candidates
102
+ }
@@ -0,0 +1,70 @@
1
+ import type { PCBTrace } from "@tscircuit/soup"
2
+
3
+ function pdist(a: any, b: any) {
4
+ return Math.hypot(a.x - b.x, a.y - b.y)
5
+ }
6
+
7
+ /**
8
+ * Merge multiple routes into a single route.
9
+ *
10
+ * If the end of the next route is closer to the end of the previous route,
11
+ * reverse the next route and append it to the previous route.
12
+ */
13
+ export const mergeRoutes = (routes: PCBTrace["route"][]) => {
14
+ // routes = routes.filter((route) => route.length > 0)
15
+ if (routes.some((r) => r.length === 0)) {
16
+ throw new Error("Cannot merge routes with zero length")
17
+ }
18
+ // for (const route of routes) {
19
+ // console.table(route)
20
+ // }
21
+ const merged: PCBTrace["route"] = []
22
+ // const reverse_log: boolean[] = []
23
+
24
+ // Determine if the first route should be reversed
25
+ const first_route_fp = routes[0][0]
26
+ const first_route_lp = routes[0][routes[0].length - 1]
27
+
28
+ const second_route_fp = routes[1][0]
29
+ const second_route_lp = routes[1][routes[1].length - 1]
30
+
31
+ const best_reverse_dist = Math.min(
32
+ pdist(first_route_fp, second_route_fp),
33
+ pdist(first_route_fp, second_route_lp),
34
+ )
35
+
36
+ const best_normal_dist = Math.min(
37
+ pdist(first_route_lp, second_route_fp),
38
+ pdist(first_route_lp, second_route_lp),
39
+ )
40
+
41
+ if (best_reverse_dist < best_normal_dist) {
42
+ merged.push(...routes[0].reverse())
43
+ // reverse_log.push(true)
44
+ } else {
45
+ merged.push(...routes[0])
46
+ // reverse_log.push(false)
47
+ }
48
+
49
+ for (let i = 1; i < routes.length; i++) {
50
+ const last_merged_point = merged[merged.length - 1]
51
+ const next_route = routes[i]
52
+
53
+ const next_first_point = next_route[0]
54
+ const next_last_point = next_route[next_route.length - 1]
55
+
56
+ const distance_to_first = pdist(last_merged_point, next_first_point)
57
+ const distance_to_last = pdist(last_merged_point, next_last_point)
58
+
59
+ if (distance_to_first < distance_to_last) {
60
+ // reverse_log.push(false)
61
+ merged.push(...next_route)
62
+ } else {
63
+ // reverse_log.push(true)
64
+ merged.push(...next_route.reverse())
65
+ }
66
+ }
67
+ // console.log(reverse_log)
68
+ // console.table(merged)
69
+ return merged
70
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Return pairs of adjacent elements in an array.
3
+ */
4
+ export function pairs<T>(arr: Array<T>): Array<[T, T]> {
5
+ const result: Array<[T, T]> = []
6
+ for (let i = 0; i < arr.length - 1; i++) {
7
+ result.push([arr[i], arr[i + 1]])
8
+ }
9
+ return result
10
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
5
  "types": "index.ts",
6
- "version": "0.0.1",
6
+ "version": "0.0.3",
7
7
  "main": "dist/index.cjs",
8
8
  "files": [
9
9
  "index.ts",
@@ -27,8 +27,8 @@
27
27
  "typescript": "^5.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@tscircuit/infgrid-ijump-astar": "0.0.2",
31
- "@tscircuit/props": "^0.0.36",
30
+ "@tscircuit/infgrid-ijump-astar": "0.0.4",
31
+ "@tscircuit/props": "^0.0.37",
32
32
  "@tscircuit/soup": "^0.0.52",
33
33
  "@tscircuit/soup-util": "0.0.18",
34
34
  "footprinter": "^0.0.44",