@tscircuit/core 0.0.133 → 0.0.135

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.d.ts CHANGED
@@ -13,6 +13,7 @@ type RenderPhase = (typeof orderedRenderPhases)[number];
13
13
  type RenderPhaseFn<K extends RenderPhase = RenderPhase> = `doInitial${K}` | `update${K}` | `remove${K}`;
14
14
  type RenderPhaseStates = Record<RenderPhase, {
15
15
  initialized: boolean;
16
+ dirty: boolean;
16
17
  }>;
17
18
  type RenderPhaseFunctions = {
18
19
  [T in RenderPhaseFn]?: () => void;
@@ -34,7 +35,12 @@ declare abstract class Renderable implements IRenderable {
34
35
  /** Schematic-only, lines, boxes, indicators etc. */
35
36
  isSchematicPrimitive: boolean;
36
37
  _renderId: string;
38
+ _currentRenderPhase: RenderPhase | null;
39
+ private _asyncEffects;
37
40
  constructor(props: any);
41
+ protected _markDirty(phase: RenderPhase): void;
42
+ protected _queueAsyncEffect(effect: () => Promise<void>): void;
43
+ _hasIncompleteAsyncEffects(): boolean;
38
44
  runRenderCycle(): void;
39
45
  /**
40
46
  * This runs all the render methods for a given phase, calling one of:
@@ -53,6 +59,7 @@ type ReactSubtree = {
53
59
  component: NormalComponent;
54
60
  };
55
61
 
62
+ type RootCircuitEventName = "asyncEffectComplete";
56
63
  declare class Circuit {
57
64
  firstChild: PrimitiveComponent | null;
58
65
  children: PrimitiveComponent[];
@@ -71,6 +78,8 @@ declare class Circuit {
71
78
  };
72
79
  _guessRootComponent(): void;
73
80
  render(): void;
81
+ renderUntilSettled(): Promise<void>;
82
+ private _hasIncompleteAsyncEffects;
74
83
  getSoup(): AnyCircuitElement[];
75
84
  getCircuitJson(): AnyCircuitElement[];
76
85
  toJson(): AnyCircuitElement[];
@@ -88,12 +97,19 @@ declare class Circuit {
88
97
  selectOne(selector: string, opts?: {
89
98
  type?: "component" | "port";
90
99
  }): PrimitiveComponent | null;
100
+ _eventListeners: Record<RootCircuitEventName, Array<(...args: any[]) => void>>;
101
+ emit(event: RootCircuitEventName, ...args: any[]): void;
102
+ on(event: RootCircuitEventName, listener: (...args: any[]) => void): void;
91
103
  }
92
104
  /**
93
105
  * @deprecated
94
106
  */
95
107
  declare const Project: typeof Circuit;
96
108
 
109
+ interface ISubcircuit extends PrimitiveComponent {
110
+ _shouldUseTraceByTraceRouting(): boolean;
111
+ }
112
+
97
113
  interface BaseComponentConfig {
98
114
  componentName: string;
99
115
  schematicSymbolName?: BaseSymbolName | null;
@@ -244,7 +260,7 @@ declare abstract class PrimitiveComponent<ZodProps extends ZodType = any> extend
244
260
  height: number;
245
261
  };
246
262
  doesSelectorMatch(selector: string): boolean;
247
- getSubcircuit(): PrimitiveComponent;
263
+ getSubcircuit(): ISubcircuit;
248
264
  selectAll(selector: string): PrimitiveComponent[];
249
265
  selectOne(selector: string, options?: {
250
266
  type?: string;
@@ -297,6 +313,10 @@ declare class Net extends PrimitiveComponent<typeof netProps> {
297
313
  * ports that are connected to each other. If a there are multiple net islands
298
314
  * that means that the net is not fully connected and we need to add traces
299
315
  * such that the nets are fully connected
316
+ *
317
+ * Sometimes this phase doesn't find any net islands if the autorouter did
318
+ * a good job and connected the islands. In some sense this is a "backup"
319
+ * routing phase for autorouters that don't care about connecting nets.
300
320
  */
301
321
  doInitialPcbRouteNetIslands(): void;
302
322
  }
@@ -718,13 +738,78 @@ declare class NormalComponent<ZodProps extends ZodType = any, PortNames extends
718
738
  doInitialCadModelRender(): void;
719
739
  }
720
740
 
721
- declare class Group<Props extends z.ZodType<any, any, any> = typeof groupProps> extends NormalComponent<Props> {
741
+ type SimplifiedPcbTrace = {
742
+ type: "pcb_trace";
743
+ pcb_trace_id: string;
744
+ route: Array<{
745
+ route_type: "wire";
746
+ x: number;
747
+ y: number;
748
+ width: number;
749
+ layer: string;
750
+ } | {
751
+ route_type: "via";
752
+ x: number;
753
+ y: number;
754
+ to_layer: string;
755
+ from_layer: string;
756
+ }>;
757
+ };
758
+ type Obstacle = {
759
+ type: "rect";
760
+ layers: string[];
761
+ center: {
762
+ x: number;
763
+ y: number;
764
+ };
765
+ width: number;
766
+ height: number;
767
+ connectedTo: string[];
768
+ };
769
+ interface SimpleRouteConnection {
770
+ name: string;
771
+ pointsToConnect: Array<{
772
+ x: number;
773
+ y: number;
774
+ layer: string;
775
+ }>;
776
+ }
777
+ interface SimpleRouteJson {
778
+ layerCount: number;
779
+ minTraceWidth: number;
780
+ obstacles: Obstacle[];
781
+ connections: Array<SimpleRouteConnection>;
782
+ bounds: {
783
+ minX: number;
784
+ maxX: number;
785
+ minY: number;
786
+ maxY: number;
787
+ };
788
+ traces?: SimplifiedPcbTrace[];
789
+ }
790
+
791
+ declare class Group<Props extends z.ZodType<any, any, any> = typeof groupProps> extends NormalComponent<Props> implements ISubcircuit {
792
+ _asyncAutoroutingResult: {
793
+ output_simple_route_json: SimpleRouteJson;
794
+ } | null;
722
795
  get config(): {
723
796
  zodProps: Props;
724
797
  componentName: string;
725
798
  };
726
799
  doInitialCreateTraceHintsFromProps(): void;
800
+ _getSimpleRouteJsonFromPcbTraces(): SimpleRouteJson;
801
+ doInitialPcbTraceRender(): void;
802
+ updatePcbTraceRender(): void;
727
803
  doInitialSchematicLayout(): void;
804
+ /**
805
+ * Trace-by-trace autorouting is where each trace routes itself in a well-known
806
+ * order. It's the most deterministic way to autoroute, because a new trace
807
+ * is generally ordered last.
808
+ *
809
+ * This method will return false if using an external service for autorouting
810
+ * or if using a "fullview" or "rip and replace" autorouting mode
811
+ */
812
+ _shouldUseTraceByTraceRouting(): boolean;
728
813
  }
729
814
 
730
815
  declare class Board extends Group<typeof boardProps> {
package/dist/index.js CHANGED
@@ -87,14 +87,61 @@ var Renderable = class {
87
87
  /** Schematic-only, lines, boxes, indicators etc. */
88
88
  isSchematicPrimitive = false;
89
89
  _renderId;
90
+ _currentRenderPhase = null;
91
+ _asyncEffects = [];
90
92
  constructor(props) {
91
93
  this._renderId = `${globalRenderCounter++}`;
92
94
  this.children = [];
93
95
  this.renderPhaseStates = {};
94
96
  for (const phase of orderedRenderPhases) {
95
- this.renderPhaseStates[phase] = { initialized: false };
97
+ this.renderPhaseStates[phase] = {
98
+ initialized: false,
99
+ dirty: false
100
+ };
101
+ }
102
+ }
103
+ _markDirty(phase) {
104
+ this.renderPhaseStates[phase].dirty = true;
105
+ const phaseIndex = orderedRenderPhases.indexOf(phase);
106
+ for (let i = phaseIndex + 1; i < orderedRenderPhases.length; i++) {
107
+ this.renderPhaseStates[orderedRenderPhases[i]].dirty = true;
96
108
  }
97
109
  }
110
+ _queueAsyncEffect(effect) {
111
+ const asyncEffect = {
112
+ promise: effect(),
113
+ // TODO don't start effects until end of render cycle
114
+ phase: this._currentRenderPhase,
115
+ complete: false
116
+ };
117
+ this._asyncEffects.push(asyncEffect);
118
+ asyncEffect.promise.then(() => {
119
+ asyncEffect.complete = true;
120
+ if ("root" in this && this.root) {
121
+ ;
122
+ this.root.emit("asyncEffectComplete", {
123
+ component: this,
124
+ asyncEffect
125
+ });
126
+ }
127
+ }).catch((error) => {
128
+ console.error(
129
+ `Async effect error in ${this._currentRenderPhase}:`,
130
+ error
131
+ );
132
+ asyncEffect.complete = true;
133
+ if ("root" in this && this.root) {
134
+ ;
135
+ this.root.emit("asyncEffectComplete", {
136
+ component: this,
137
+ asyncEffect
138
+ });
139
+ }
140
+ });
141
+ }
142
+ _hasIncompleteAsyncEffects() {
143
+ return this._asyncEffects.some((effect) => !effect.complete);
144
+ }
98
145
  runRenderCycle() {
99
146
  for (const renderPhase of orderedRenderPhases) {
100
147
  this.runRenderPhaseForChildren(renderPhase);
@@ -109,22 +156,35 @@ var Renderable = class {
109
156
  * ...depending on the current state of the component.
110
157
  */
111
158
  runRenderPhase(phase) {
112
- const isInitialized = this.renderPhaseStates[phase].initialized;
159
+ this._currentRenderPhase = phase;
160
+ const phaseState = this.renderPhaseStates[phase];
161
+ const isInitialized = phaseState.initialized;
162
+ const isDirty = phaseState.dirty;
113
163
  if (!isInitialized && this.shouldBeRemoved) return;
114
164
  if (this.shouldBeRemoved && isInitialized) {
115
165
  ;
116
166
  this?.[`remove${phase}`]?.();
117
- this.renderPhaseStates[phase].initialized = false;
167
+ phaseState.initialized = false;
168
+ phaseState.dirty = false;
118
169
  return;
119
170
  }
171
+ const prevPhaseIndex = orderedRenderPhases.indexOf(phase) - 1;
172
+ if (prevPhaseIndex >= 0) {
173
+ const prevPhase = orderedRenderPhases[prevPhaseIndex];
174
+ const hasIncompleteEffects = this._asyncEffects.filter((e) => e.phase === prevPhase).some((e) => !e.complete);
175
+ if (hasIncompleteEffects) return;
176
+ }
120
177
  if (isInitialized) {
121
- ;
122
- this?.[`update${phase}`]?.();
178
+ if (isDirty) {
179
+ ;
180
+ this?.[`update${phase}`]?.();
181
+ phaseState.dirty = false;
182
+ }
123
183
  return;
124
184
  }
125
- ;
185
+ phaseState.dirty = false;
126
186
  this?.[`doInitial${phase}`]?.();
127
- this.renderPhaseStates[phase].initialized = true;
187
+ phaseState.initialized = true;
128
188
  }
129
189
  runRenderPhaseForChildren(phase) {
130
190
  for (const child of this.children) {
@@ -843,6 +903,10 @@ var Net = class extends PrimitiveComponent {
843
903
  * ports that are connected to each other. If a there are multiple net islands
844
904
  * that means that the net is not fully connected and we need to add traces
845
905
  * such that the nets are fully connected
906
+ *
907
+ * Sometimes this phase doesn't find any net islands if the autorouter did
908
+ * a good job and connected the islands. In some sense this is a "backup"
909
+ * routing phase for autorouters that don't care about connecting nets.
846
910
  */
847
911
  doInitialPcbRouteNetIslands() {
848
912
  const { db } = this.root;
@@ -2373,7 +2437,7 @@ var NormalComponent = class extends PrimitiveComponent {
2373
2437
  import { boardProps } from "@tscircuit/props";
2374
2438
  import { identity as identity4 } from "transformation-matrix";
2375
2439
 
2376
- // lib/components/primitive-components/Group.ts
2440
+ // lib/components/primitive-components/Group/Group.ts
2377
2441
  import {
2378
2442
  groupProps
2379
2443
  } from "@tscircuit/props";
@@ -2440,9 +2504,11 @@ var TraceHint = class extends PrimitiveComponent {
2440
2504
  }
2441
2505
  };
2442
2506
 
2443
- // lib/components/primitive-components/Group.ts
2507
+ // lib/components/primitive-components/Group/Group.ts
2444
2508
  import * as SAL from "@tscircuit/schematic-autolayout";
2509
+ import { getObstaclesFromSoup } from "@tscircuit/infgrid-ijump-astar";
2445
2510
  var Group = class extends NormalComponent {
2511
+ _asyncAutoroutingResult = null;
2446
2512
  get config() {
2447
2513
  return {
2448
2514
  zodProps: groupProps,
@@ -2465,6 +2531,87 @@ var Group = class extends NormalComponent {
2465
2531
  );
2466
2532
  }
2467
2533
  }
2534
+ _getSimpleRouteJsonFromPcbTraces() {
2535
+ const traces = this.selectAll("trace");
2536
+ const { db } = this.root;
2537
+ const obstacles = getObstaclesFromSoup([
2538
+ ...db.pcb_component.list(),
2539
+ ...db.pcb_smtpad.list(),
2540
+ ...db.pcb_plated_hole.list()
2541
+ ]);
2542
+ const allPoints = obstacles.flatMap((o) => [
2543
+ {
2544
+ x: o.center.x - o.width / 2,
2545
+ y: o.center.y - o.height / 2
2546
+ },
2547
+ {
2548
+ x: o.center.x + o.width / 2,
2549
+ y: o.center.y + o.height / 2
2550
+ }
2551
+ ]);
2552
+ const bounds = {
2553
+ minX: Math.min(...allPoints.map((p) => p.x)) - 1,
2554
+ maxX: Math.max(...allPoints.map((p) => p.x)) + 1,
2555
+ minY: Math.min(...allPoints.map((p) => p.y)) - 1,
2556
+ maxY: Math.max(...allPoints.map((p) => p.y)) + 1
2557
+ };
2558
+ const connections = traces.map((trace) => {
2559
+ const connectedPorts = trace._findConnectedPorts();
2560
+ if (!connectedPorts.allPortsFound || connectedPorts.ports.length < 2)
2561
+ return null;
2562
+ return {
2563
+ name: trace.source_trace_id ?? "",
2564
+ pointsToConnect: connectedPorts.ports.map((port) => {
2565
+ const pos = port._getGlobalPcbPositionBeforeLayout();
2566
+ return {
2567
+ x: pos.x,
2568
+ y: pos.y,
2569
+ layer: port.getAvailablePcbLayers()[0] ?? "top"
2570
+ };
2571
+ })
2572
+ };
2573
+ }).filter((c) => c !== null);
2574
+ return {
2575
+ bounds,
2576
+ obstacles: [],
2577
+ connections,
2578
+ layerCount: 2,
2579
+ minTraceWidth: this._parsedProps.minTraceWidth ?? 0.1
2580
+ };
2581
+ }
2582
+ doInitialPcbTraceRender() {
2583
+ if (this._shouldUseTraceByTraceRouting()) return;
2584
+ if (this.props.autorouter?.serverUrl) {
2585
+ this._queueAsyncEffect(async () => {
2586
+ const { autorouting_result } = await fetch(
2587
+ this.props.autorouter.serverUrl,
2588
+ {
2589
+ method: "POST",
2590
+ body: JSON.stringify({
2591
+ input_simple_route_json: this._getSimpleRouteJsonFromPcbTraces()
2592
+ })
2593
+ }
2594
+ ).then((r) => r.json());
2595
+ const { output_simple_route_json } = autorouting_result;
2596
+ this._asyncAutoroutingResult = autorouting_result;
2597
+ this._markDirty("PcbTraceRender");
2598
+ });
2599
+ }
2600
+ }
2601
+ updatePcbTraceRender() {
2602
+ if (!this._asyncAutoroutingResult) return;
2603
+ if (this._shouldUseTraceByTraceRouting()) return;
2604
+ const { db } = this.root;
2605
+ const { traces: routedTraces } = this._asyncAutoroutingResult.output_simple_route_json;
2606
+ if (!routedTraces) return;
2607
+ const circuitTraces = this.selectAll("trace");
2608
+ for (const routedTrace of routedTraces) {
2609
+ const pcb_trace = db.pcb_trace.insert({
2610
+ route: routedTrace.route
2611
+ // source_trace_id: circuitTrace.source_trace_id!,
2612
+ });
2613
+ }
2614
+ }
2468
2615
  doInitialSchematicLayout() {
2469
2616
  if (!this.isSubcircuit) return;
2470
2617
  const props = this._parsedProps;
@@ -2491,6 +2638,18 @@ var Group = class extends NormalComponent {
2491
2638
  const laidOutScene = SAL.ascendingCentralLrBug1(scene);
2492
2639
  SAL.mutateSoupForScene(db.toArray(), laidOutScene);
2493
2640
  }
2641
+ /**
2642
+ * Trace-by-trace autorouting is where each trace routes itself in a well-known
2643
+ * order. It's the most deterministic way to autoroute, because a new trace
2644
+ * is generally ordered last.
2645
+ *
2646
+ * This method will return false if using an external service for autorouting
2647
+ * or if using a "fullview" or "rip and replace" autorouting mode
2648
+ */
2649
+ _shouldUseTraceByTraceRouting() {
2650
+ if (this.props.autorouter) return false;
2651
+ return true;
2652
+ }
2494
2653
  };
2495
2654
 
2496
2655
  // lib/components/normal-components/Board.ts
@@ -2557,7 +2716,7 @@ var FTYPE = stringProxy;
2557
2716
  // lib/components/primitive-components/Trace.ts
2558
2717
  import {
2559
2718
  MultilayerIjump,
2560
- getObstaclesFromSoup
2719
+ getObstaclesFromSoup as getObstaclesFromSoup2
2561
2720
  } from "@tscircuit/infgrid-ijump-astar";
2562
2721
  import { traceProps } from "@tscircuit/props";
2563
2722
  import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map";
@@ -2938,6 +3097,9 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
2938
3097
  if (this.getSubcircuit()._parsedProps.routingDisabled) {
2939
3098
  return;
2940
3099
  }
3100
+ if (!this.getSubcircuit()._shouldUseTraceByTraceRouting()) {
3101
+ return;
3102
+ }
2941
3103
  const { allPortsFound, ports } = this._findConnectedPorts();
2942
3104
  const portsConnectedOnPcbViaNet = [];
2943
3105
  if (!allPortsFound) return;
@@ -3034,7 +3196,7 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
3034
3196
  this.root.db.toArray()
3035
3197
  );
3036
3198
  const [obstacles, errGettingObstacles] = tryNow(
3037
- () => getObstaclesFromSoup(this.root.db.toArray())
3199
+ () => getObstaclesFromSoup2(this.root.db.toArray())
3038
3200
  // Remove as any when autorouting-dataset gets updated
3039
3201
  );
3040
3202
  if (errGettingObstacles) {
@@ -4396,6 +4558,21 @@ var Circuit = class {
4396
4558
  firstChild.runRenderCycle();
4397
4559
  this._hasRenderedAtleastOnce = true;
4398
4560
  }
4561
+ async renderUntilSettled() {
4562
+ this.render();
4563
+ while (this._hasIncompleteAsyncEffects()) {
4564
+ await new Promise((resolve) => setTimeout(resolve, 100));
4565
+ this.render();
4566
+ }
4567
+ }
4568
+ _hasIncompleteAsyncEffects() {
4569
+ return this.children.some((child) => {
4570
+ if (child._hasIncompleteAsyncEffects()) return true;
4571
+ return child.children.some(
4572
+ (grandchild) => grandchild._hasIncompleteAsyncEffects()
4573
+ );
4574
+ });
4575
+ }
4399
4576
  getSoup() {
4400
4577
  if (!this._hasRenderedAtleastOnce) this.render();
4401
4578
  return this.db.toArray();
@@ -4432,6 +4609,19 @@ var Circuit = class {
4432
4609
  selectOne(selector, opts) {
4433
4610
  return this.firstChild?.selectOne(selector, opts) ?? null;
4434
4611
  }
4612
+ _eventListeners = { asyncEffectComplete: [] };
4613
+ emit(event, ...args) {
4614
+ if (!this._eventListeners[event]) return;
4615
+ for (const listener of this._eventListeners[event]) {
4616
+ listener(...args);
4617
+ }
4618
+ }
4619
+ on(event, listener) {
4620
+ if (!this._eventListeners[event]) {
4621
+ this._eventListeners[event] = [];
4622
+ }
4623
+ this._eventListeners[event].push(listener);
4624
+ }
4435
4625
  };
4436
4626
  var Project = Circuit;
4437
4627
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/core",
3
3
  "type": "module",
4
- "version": "0.0.133",
4
+ "version": "0.0.135",
5
5
  "types": "dist/index.d.ts",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",