@tscircuit/core 0.0.29 → 0.0.31

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.js CHANGED
@@ -10,6 +10,7 @@ __export(components_exports, {
10
10
  Board: () => Board,
11
11
  Capacitor: () => Capacitor,
12
12
  Chip: () => Chip,
13
+ Constraint: () => Constraint2,
13
14
  Footprint: () => Footprint,
14
15
  Group: () => Group,
15
16
  Jumper: () => Jumper,
@@ -36,13 +37,12 @@ import "react";
36
37
  import "react";
37
38
  var orderedRenderPhases = [
38
39
  "ReactSubtreesRender",
39
- // probably going to be removed b/c subtrees should render instantly
40
+ "InitializePortsFromChildren",
40
41
  "CreateNetsFromProps",
41
42
  "CreateTracesFromProps",
42
43
  "SourceRender",
43
44
  "SourceParentAttachment",
44
45
  "PortDiscovery",
45
- // probably going to be removed b/c port discovery can always be done on prop change
46
46
  "PortMatching",
47
47
  "SourceTraceRender",
48
48
  "SchematicComponentRender",
@@ -50,8 +50,9 @@ var orderedRenderPhases = [
50
50
  "SchematicPortRender",
51
51
  "SchematicTraceRender",
52
52
  "PcbComponentRender",
53
- "PcbPortRender",
54
53
  "PcbPrimitiveRender",
54
+ "PcbFootprintLayout",
55
+ "PcbPortRender",
55
56
  "PcbParentAttachment",
56
57
  "PcbLayout",
57
58
  "PcbTraceRender",
@@ -169,7 +170,6 @@ var PrimitiveComponent = class extends Renderable {
169
170
  zodProps: z.object({}).passthrough()
170
171
  };
171
172
  }
172
- project = null;
173
173
  props;
174
174
  _parsedProps;
175
175
  componentName = "";
@@ -183,8 +183,19 @@ var PrimitiveComponent = class extends Renderable {
183
183
  */
184
184
  get isSubcircuit() {
185
185
  return Boolean(this.props.subcircuit) || // Implied opaque group for top-level group
186
- this.lowercaseComponentName === "group" && this?.parent?.props?.name === "$root";
186
+ this.lowercaseComponentName === "group" && this?.parent?.isRoot;
187
187
  }
188
+ /**
189
+ * A primitive container is a component that contains one or more ports and
190
+ * primitive components that are designed to interact.
191
+ *
192
+ * For example a resistor contains ports and smtpads that interact, so the
193
+ * resistor is a primitive container. Inside a primitive container, the ports
194
+ * and pads are likely to reference each other and look for eachother during
195
+ * the port matching phase.
196
+ *
197
+ */
198
+ isPrimitiveContainer = false;
188
199
  source_group_id = null;
189
200
  source_component_id = null;
190
201
  schematic_component_id = null;
@@ -204,12 +215,6 @@ var PrimitiveComponent = class extends Renderable {
204
215
  this.lowercaseComponentName = this.componentName.toLowerCase();
205
216
  }
206
217
  }
207
- setProject(project) {
208
- this.project = project;
209
- for (const c of this.children) {
210
- c.setProject(project);
211
- }
212
- }
213
218
  setProps(props) {
214
219
  const newProps = this.config.zodProps.parse({
215
220
  ...this.props,
@@ -242,14 +247,14 @@ var PrimitiveComponent = class extends Renderable {
242
247
  * components, including this component's translation and rotation.
243
248
  *
244
249
  * This is used to compute this component's position as well as all children
245
- * components positions
250
+ * components positions before layout is applied
246
251
  */
247
- computePcbGlobalTransform() {
252
+ _computePcbGlobalTransformBeforeLayout() {
248
253
  const { _parsedProps: props } = this;
249
254
  const manualPlacement = this.getSubcircuit()._getManualPlacementForComponent(this);
250
255
  if (manualPlacement && this.props.pcbX === void 0 && this.props.pcbY === void 0) {
251
256
  return compose(
252
- this.parent?.computePcbGlobalTransform() ?? identity(),
257
+ this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
253
258
  compose(
254
259
  translate(manualPlacement.x, manualPlacement.y),
255
260
  rotate((props.pcbRotation ?? 0) * Math.PI / 180)
@@ -257,10 +262,37 @@ var PrimitiveComponent = class extends Renderable {
257
262
  );
258
263
  }
259
264
  return compose(
260
- this.parent?.computePcbGlobalTransform() ?? identity(),
265
+ this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
261
266
  this.computePcbPropsTransform()
262
267
  );
263
268
  }
269
+ getPrimitiveContainer() {
270
+ if (this.isPrimitiveContainer) return this;
271
+ return this.parent?.getPrimitiveContainer?.() ?? null;
272
+ }
273
+ /**
274
+ * Compute the bounds of this component the circuit json elements associated
275
+ * with it.
276
+ */
277
+ _getCircuitJsonBounds() {
278
+ return {
279
+ center: { x: 0, y: 0 },
280
+ bounds: { left: 0, top: 0, right: 0, bottom: 0 },
281
+ width: 0,
282
+ height: 0
283
+ };
284
+ }
285
+ /**
286
+ * Set the position of this component from the layout solver. This method
287
+ * should operate using CircuitJson associated with this component, like
288
+ * _getCircuitJsonBounds it can be called multiple times as different
289
+ * parents apply layout to their children.
290
+ */
291
+ _setPositionFromLayout(newCenter) {
292
+ throw new Error(
293
+ `_setPositionFromLayout not implemented for ${this.componentName}`
294
+ );
295
+ }
264
296
  /**
265
297
  * Computes a transformation matrix from the props of this component for
266
298
  * schematic components
@@ -301,7 +333,7 @@ var PrimitiveComponent = class extends Renderable {
301
333
  for (const position of placementConfig.positions) {
302
334
  if (isMatchingSelector(component, position.selector)) {
303
335
  const center = applyToPoint(
304
- this.computePcbGlobalTransform(),
336
+ this._computePcbGlobalTransformBeforeLayout(),
305
337
  position.center
306
338
  );
307
339
  return center;
@@ -309,15 +341,20 @@ var PrimitiveComponent = class extends Renderable {
309
341
  }
310
342
  return null;
311
343
  }
312
- getGlobalPcbPosition() {
313
- return applyToPoint(this.computePcbGlobalTransform(), { x: 0, y: 0 });
344
+ _getGlobalPcbPositionBeforeLayout() {
345
+ return applyToPoint(this._computePcbGlobalTransformBeforeLayout(), {
346
+ x: 0,
347
+ y: 0
348
+ });
314
349
  }
315
- getGlobalSchematicPosition() {
350
+ _getGlobalSchematicPositionBeforeLayout() {
316
351
  return applyToPoint(this.computeSchematicGlobalTransform(), { x: 0, y: 0 });
317
352
  }
353
+ get root() {
354
+ return this.parent?.root ?? null;
355
+ }
318
356
  onAddToParent(parent) {
319
357
  this.parent = parent;
320
- this.project = parent.project;
321
358
  }
322
359
  /**
323
360
  * Called whenever the props change
@@ -469,7 +506,125 @@ var PrimitiveComponent = class extends Renderable {
469
506
  };
470
507
 
471
508
  // lib/components/primitive-components/Footprint.ts
509
+ import * as kiwi from "@lume/kiwi";
510
+ import Debug from "debug";
511
+ var debug = Debug("tscircuit:core:footprint");
472
512
  var Footprint = class extends PrimitiveComponent {
513
+ /**
514
+ * A footprint is a constrainedlayout, the db elements are adjusted according
515
+ * to any constraints that are defined.
516
+ */
517
+ doInitialPcbFootprintLayout() {
518
+ const constraints = this.children.filter(
519
+ (child) => child.componentName === "Constraint"
520
+ );
521
+ if (constraints.length === 0) return;
522
+ const involvedComponents = constraints.flatMap(
523
+ (constraint) => constraint._getAllReferencedComponents().componentsWithSelectors
524
+ ).map(({ component, selector }) => ({
525
+ component,
526
+ selector,
527
+ bounds: component._getCircuitJsonBounds()
528
+ }));
529
+ function getComponentDetails(selector) {
530
+ return involvedComponents.find(({ selector: s }) => s === selector);
531
+ }
532
+ const solver = new kiwi.Solver();
533
+ const kVars = {};
534
+ function getKVar(name) {
535
+ if (!(name in kVars)) {
536
+ kVars[name] = new kiwi.Variable(name);
537
+ }
538
+ return kVars[name];
539
+ }
540
+ for (const { selector, bounds } of involvedComponents) {
541
+ const kvx = getKVar(`${selector}_x`);
542
+ const kvy = getKVar(`${selector}_y`);
543
+ solver.addEditVariable(kvx, kiwi.Strength.weak);
544
+ solver.addEditVariable(kvy, kiwi.Strength.weak);
545
+ solver.suggestValue(kvx, bounds.center.x);
546
+ solver.suggestValue(kvy, bounds.center.y);
547
+ }
548
+ for (const constraint of constraints) {
549
+ const props = constraint._parsedProps;
550
+ if ("xdist" in props) {
551
+ const { xdist, left, right, edgeToEdge, centerToCenter } = props;
552
+ const leftVar = getKVar(`${left}_x`);
553
+ const rightVar = getKVar(`${right}_x`);
554
+ const leftBounds = getComponentDetails(left)?.bounds;
555
+ const rightBounds = getComponentDetails(right)?.bounds;
556
+ if (centerToCenter) {
557
+ const expr = new kiwi.Expression(rightVar, [-1, leftVar]);
558
+ solver.addConstraint(
559
+ new kiwi.Constraint(
560
+ expr,
561
+ kiwi.Operator.Eq,
562
+ props.xdist,
563
+ kiwi.Strength.required
564
+ )
565
+ );
566
+ } else if (edgeToEdge) {
567
+ const expr = new kiwi.Expression(
568
+ rightVar,
569
+ rightBounds.width / 2,
570
+ [-1, leftVar],
571
+ -leftBounds.width / 2
572
+ );
573
+ solver.addConstraint(
574
+ new kiwi.Constraint(
575
+ expr,
576
+ kiwi.Operator.Eq,
577
+ props.xdist,
578
+ kiwi.Strength.required
579
+ )
580
+ );
581
+ }
582
+ }
583
+ solver.updateVariables();
584
+ if (debug.enabled) {
585
+ console.log("Solution to layout constraints:");
586
+ console.table(
587
+ Object.entries(kVars).map(([key, kvar]) => ({
588
+ var: key,
589
+ val: kvar.value()
590
+ }))
591
+ );
592
+ }
593
+ const bounds = {
594
+ left: Infinity,
595
+ right: -Infinity,
596
+ top: -Infinity,
597
+ bottom: Infinity
598
+ };
599
+ for (const {
600
+ selector,
601
+ bounds: { width, height }
602
+ } of involvedComponents) {
603
+ const kvx = getKVar(`${selector}_x`);
604
+ const kvy = getKVar(`${selector}_y`);
605
+ const newLeft = kvx.value() - width / 2;
606
+ const newRight = kvx.value() + width / 2;
607
+ const newTop = kvy.value() + height / 2;
608
+ const newBottom = kvy.value() - height / 2;
609
+ bounds.left = Math.min(bounds.left, newLeft);
610
+ bounds.right = Math.max(bounds.right, newRight);
611
+ bounds.top = Math.max(bounds.top, newTop);
612
+ bounds.bottom = Math.min(bounds.bottom, newBottom);
613
+ }
614
+ const globalOffset = {
615
+ x: -(bounds.right + bounds.left) / 2,
616
+ y: -(bounds.top + bounds.bottom) / 2
617
+ };
618
+ for (const { component, selector } of involvedComponents) {
619
+ const kvx = getKVar(`${selector}_x`);
620
+ const kvy = getKVar(`${selector}_y`);
621
+ component._setPositionFromLayout({
622
+ x: kvx.value() + globalOffset.x,
623
+ y: kvy.value() + globalOffset.y
624
+ });
625
+ }
626
+ }
627
+ }
473
628
  };
474
629
 
475
630
  // lib/components/base-components/NormalComponent.ts
@@ -511,16 +666,16 @@ var Port = class extends PrimitiveComponent {
511
666
  super(props);
512
667
  this.matchedComponents = [];
513
668
  }
514
- getGlobalPcbPosition() {
669
+ _getGlobalPcbPositionBeforeLayout() {
515
670
  const matchedPcbElm = this.matchedComponents.find((c) => c.isPcbPrimitive);
516
671
  if (!matchedPcbElm) {
517
672
  throw new Error(
518
673
  `Port ${this} has no matched pcb component, can't get global schematic position`
519
674
  );
520
675
  }
521
- return matchedPcbElm?.getGlobalPcbPosition() ?? { x: 0, y: 0 };
676
+ return matchedPcbElm?._getGlobalPcbPositionBeforeLayout() ?? { x: 0, y: 0 };
522
677
  }
523
- getGlobalSchematicPosition() {
678
+ _getGlobalSchematicPositionBeforeLayout() {
524
679
  if (!this.schematicSymbolPortDef) {
525
680
  return applyToPoint2(this.parent.computeSchematicGlobalTransform(), {
526
681
  x: 0,
@@ -581,7 +736,7 @@ var Port = class extends PrimitiveComponent {
581
736
  return connectedTraces;
582
737
  }
583
738
  doInitialSourceRender() {
584
- const { db } = this.project;
739
+ const { db } = this.root;
585
740
  const { _parsedProps: props } = this;
586
741
  const port_hints = this.getNameAndAliases();
587
742
  const source_port = db.source_port.insert({
@@ -593,7 +748,7 @@ var Port = class extends PrimitiveComponent {
593
748
  this.source_port_id = source_port.source_port_id;
594
749
  }
595
750
  doInitialSourceParentAttachment() {
596
- const { db } = this.project;
751
+ const { db } = this.root;
597
752
  if (!this.parent?.source_component_id) {
598
753
  throw new Error(
599
754
  `${this.getString()} has no parent source component (parent: ${this.parent?.getString()})`
@@ -604,13 +759,8 @@ var Port = class extends PrimitiveComponent {
604
759
  });
605
760
  this.source_component_id = this.parent?.source_component_id;
606
761
  }
607
- /**
608
- * For PcbPorts, we use the parent attachment phase to determine where to place
609
- * the pcb_port (prior to this phase, the smtpad/platedhole isn't guaranteed
610
- * to exist)
611
- */
612
762
  doInitialPcbPortRender() {
613
- const { db } = this.project;
763
+ const { db } = this.root;
614
764
  const { matchedComponents } = this;
615
765
  if (!this.parent?.pcb_component_id) {
616
766
  throw new Error(
@@ -625,26 +775,26 @@ var Port = class extends PrimitiveComponent {
625
775
  );
626
776
  }
627
777
  const pcbMatch = pcbMatches[0];
628
- if ("getGlobalPcbPosition" in pcbMatch) {
778
+ if ("_getCircuitJsonBounds" in pcbMatch) {
629
779
  const pcb_port = db.pcb_port.insert({
630
780
  pcb_component_id: this.parent?.pcb_component_id,
631
781
  layers: ["top"],
632
- ...pcbMatch.getGlobalPcbPosition(),
782
+ ...pcbMatch._getCircuitJsonBounds().center,
633
783
  source_port_id: this.source_port_id
634
784
  });
635
785
  this.pcb_port_id = pcb_port.pcb_port_id;
636
786
  } else {
637
787
  throw new Error(
638
- `${pcbMatch.getString()} does not have a getGlobalPcbPosition method (needed for pcb_port placement)`
788
+ `${pcbMatch.getString()} does not have a _getGlobalPcbPositionBeforeLayout method (needed for pcb_port placement)`
639
789
  );
640
790
  }
641
791
  }
642
792
  doInitialSchematicPortRender() {
643
- const { db } = this.project;
793
+ const { db } = this.root;
644
794
  const { _parsedProps: props } = this;
645
795
  if (!this.parent) return;
646
- const center = this.getGlobalSchematicPosition();
647
- const parentCenter = this.parent?.getGlobalSchematicPosition();
796
+ const center = this._getGlobalSchematicPositionBeforeLayout();
797
+ const parentCenter = this.parent?._getGlobalSchematicPositionBeforeLayout();
648
798
  this.facingDirection = getRelativeDirection(parentCenter, center);
649
799
  const schematic_port = db.schematic_port.insert({
650
800
  schematic_component_id: this.parent?.schematic_component_id,
@@ -856,8 +1006,8 @@ var SmtPad = class extends PrimitiveComponent {
856
1006
  );
857
1007
  }
858
1008
  doInitialPortMatching() {
859
- const parentPorts = (this.parent?.children ?? []).filter(
860
- (c) => c.componentName === "Port"
1009
+ const parentPorts = this.getPrimitiveContainer()?.selectAll(
1010
+ "port"
861
1011
  );
862
1012
  if (!this.props.portHints) {
863
1013
  return;
@@ -871,11 +1021,13 @@ var SmtPad = class extends PrimitiveComponent {
871
1021
  }
872
1022
  }
873
1023
  doInitialPcbPrimitiveRender() {
874
- const { db } = this.project;
1024
+ const { db } = this.root;
875
1025
  const { _parsedProps: props } = this;
876
1026
  if (!props.portHints) return;
877
- const position = this.getGlobalPcbPosition();
878
- const decomposedMat = decomposeTSR(this.computePcbGlobalTransform());
1027
+ const position = this._getGlobalPcbPositionBeforeLayout();
1028
+ const decomposedMat = decomposeTSR(
1029
+ this._computePcbGlobalTransformBeforeLayout()
1030
+ );
879
1031
  const isRotated90 = Math.abs(decomposedMat.rotation.angle * (180 / Math.PI) - 90) < 0.01;
880
1032
  let pcb_smtpad = null;
881
1033
  if (props.shape === "circle") {
@@ -906,6 +1058,46 @@ var SmtPad = class extends PrimitiveComponent {
906
1058
  this.pcb_smtpad_id = pcb_smtpad.pcb_smtpad_id;
907
1059
  }
908
1060
  }
1061
+ _getCircuitJsonBounds() {
1062
+ const { db } = this.root;
1063
+ const smtpad = db.pcb_smtpad.get(this.pcb_smtpad_id);
1064
+ if (smtpad.shape === "rect") {
1065
+ return {
1066
+ center: { x: smtpad.x, y: smtpad.y },
1067
+ bounds: {
1068
+ left: smtpad.x - smtpad.width / 2,
1069
+ top: smtpad.y - smtpad.height / 2,
1070
+ right: smtpad.x + smtpad.width / 2,
1071
+ bottom: smtpad.y + smtpad.height / 2
1072
+ },
1073
+ width: smtpad.width,
1074
+ height: smtpad.height
1075
+ };
1076
+ }
1077
+ if (smtpad.shape === "circle") {
1078
+ return {
1079
+ center: { x: smtpad.x, y: smtpad.y },
1080
+ bounds: {
1081
+ left: smtpad.x - smtpad.radius,
1082
+ top: smtpad.y - smtpad.radius,
1083
+ right: smtpad.x + smtpad.radius,
1084
+ bottom: smtpad.y + smtpad.radius
1085
+ },
1086
+ width: smtpad.radius * 2,
1087
+ height: smtpad.radius * 2
1088
+ };
1089
+ }
1090
+ throw new Error(
1091
+ `circuitJson bounds calculation not implemented for shape "${smtpad.shape}"`
1092
+ );
1093
+ }
1094
+ _setPositionFromLayout(newCenter) {
1095
+ const { db } = this.root;
1096
+ db.pcb_smtpad.update(this.pcb_smtpad_id, {
1097
+ x: newCenter.x,
1098
+ y: newCenter.y
1099
+ });
1100
+ }
909
1101
  };
910
1102
 
911
1103
  // lib/components/primitive-components/SilkscreenPath.ts
@@ -919,7 +1111,7 @@ var SilkscreenPath = class extends PrimitiveComponent {
919
1111
  };
920
1112
  }
921
1113
  doInitialPcbPrimitiveRender() {
922
- const { db } = this.project;
1114
+ const { db } = this.root;
923
1115
  const { _parsedProps: props } = this;
924
1116
  const layer = props.layer ?? "top";
925
1117
  if (layer !== "top" && layer !== "bottom") {
@@ -927,7 +1119,7 @@ var SilkscreenPath = class extends PrimitiveComponent {
927
1119
  `Invalid layer "${layer}" for SilkscreenPath. Must be "top" or "bottom".`
928
1120
  );
929
1121
  }
930
- const transform = this.computePcbGlobalTransform();
1122
+ const transform = this._computePcbGlobalTransformBeforeLayout();
931
1123
  const pcb_silkscreen_path = db.pcb_silkscreen_path.insert({
932
1124
  pcb_component_id: this.parent?.pcb_component_id,
933
1125
  layer,
@@ -987,10 +1179,10 @@ var PlatedHole = class extends PrimitiveComponent {
987
1179
  }
988
1180
  }
989
1181
  doInitialPcbPrimitiveRender() {
990
- const { db } = this.project;
1182
+ const { db } = this.root;
991
1183
  const { _parsedProps: props } = this;
992
1184
  if (!props.portHints) return;
993
- const position = this.getGlobalPcbPosition();
1185
+ const position = this._getGlobalPcbPositionBeforeLayout();
994
1186
  if (props.shape === "circle") {
995
1187
  const plated_hole_input = {
996
1188
  pcb_component_id: this.parent?.pcb_component_id,
@@ -1085,7 +1277,7 @@ var Net = class extends PrimitiveComponent {
1085
1277
  return `net.${this.props.name}`;
1086
1278
  }
1087
1279
  doInitialSourceComponentRender() {
1088
- const { db } = this.project;
1280
+ const { db } = this.root;
1089
1281
  const { _parsedProps: props } = this;
1090
1282
  const net = db.source_net.insert({
1091
1283
  name: props.name,
@@ -1134,7 +1326,7 @@ var Net = class extends PrimitiveComponent {
1134
1326
  * such that the nets are fully connected
1135
1327
  */
1136
1328
  doInitialPcbRouteNetIslands() {
1137
- const { db } = this.project;
1329
+ const { db } = this.root;
1138
1330
  const { _parsedProps: props } = this;
1139
1331
  const traces = this._getAllDirectlyConnectedTraces().filter(
1140
1332
  (trace) => (trace._portsRoutedOnPcb?.length ?? 0) > 0
@@ -1158,10 +1350,10 @@ var Net = class extends PrimitiveComponent {
1158
1350
  const islandPairs = pairs(islands);
1159
1351
  for (const [A, B] of islandPairs) {
1160
1352
  const Apositions = A.ports.map(
1161
- (port) => port.getGlobalPcbPosition()
1353
+ (port) => port._getGlobalPcbPositionBeforeLayout()
1162
1354
  );
1163
1355
  const Bpositions = B.ports.map(
1164
- (port) => port.getGlobalPcbPosition()
1356
+ (port) => port._getGlobalPcbPositionBeforeLayout()
1165
1357
  );
1166
1358
  let closestDist = Infinity;
1167
1359
  let closestPair = [-1, -1];
@@ -1219,6 +1411,7 @@ var createNetsFromProps = (component, props) => {
1219
1411
  // lib/components/base-components/NormalComponent.ts
1220
1412
  var NormalComponent = class extends PrimitiveComponent {
1221
1413
  reactSubtrees = [];
1414
+ isPrimitiveContainer = true;
1222
1415
  constructor(props) {
1223
1416
  super(props);
1224
1417
  this._addChildrenFromStringFootprint();
@@ -1240,6 +1433,7 @@ var NormalComponent = class extends PrimitiveComponent {
1240
1433
  */
1241
1434
  initPorts() {
1242
1435
  const { config } = this;
1436
+ const portsToCreate = [];
1243
1437
  if (config.schematicSymbolName) {
1244
1438
  const sym = symbols3[`${config.schematicSymbolName}_horz`];
1245
1439
  if (!sym) return;
@@ -1247,27 +1441,31 @@ var NormalComponent = class extends PrimitiveComponent {
1247
1441
  const port = getPortFromHints(symPort.labels);
1248
1442
  if (port) {
1249
1443
  port.schematicSymbolPortDef = symPort;
1250
- this.add(port);
1444
+ portsToCreate.push(port);
1251
1445
  }
1252
1446
  }
1447
+ this.addAll(portsToCreate);
1253
1448
  return;
1254
1449
  }
1255
1450
  const portsFromFootprint = this.getPortsFromFootprint();
1256
- this.addAll(portsFromFootprint);
1451
+ portsToCreate.push(...portsFromFootprint);
1257
1452
  const pinLabels = this._parsedProps.pinLabels;
1258
1453
  if (pinLabels) {
1259
1454
  for (let [pinNumber, label] of Object.entries(pinLabels)) {
1260
1455
  pinNumber = pinNumber.replace("pin", "");
1261
- const port = this.selectOne(`port[pinNumber='${pinNumber}']`);
1262
- if (!port) {
1456
+ const existingPort = portsToCreate.find(
1457
+ (p) => p._parsedProps.pinNumber === Number(pinNumber)
1458
+ );
1459
+ if (!existingPort) {
1263
1460
  throw new Error(
1264
1461
  `Could not find port for pin number ${pinNumber} in chip ${this.getString()}`
1265
1462
  );
1266
1463
  }
1267
- port.externallyAddedAliases.push(label);
1268
- port.props.name = label;
1464
+ existingPort.externallyAddedAliases.push(label);
1465
+ existingPort.props.name = label;
1269
1466
  }
1270
1467
  }
1468
+ this.addAll(portsToCreate);
1271
1469
  }
1272
1470
  _addChildrenFromStringFootprint() {
1273
1471
  const { footprint } = this.props;
@@ -1305,7 +1503,7 @@ var NormalComponent = class extends PrimitiveComponent {
1305
1503
  doInitialSourceRender() {
1306
1504
  const ftype = this.config.sourceFtype;
1307
1505
  if (!ftype) return;
1308
- const { db } = this.project;
1506
+ const { db } = this.root;
1309
1507
  const { _parsedProps: props } = this;
1310
1508
  const source_component = db.source_component.insert({
1311
1509
  ftype,
@@ -1322,7 +1520,7 @@ var NormalComponent = class extends PrimitiveComponent {
1322
1520
  * You can override this method to do more complicated things.
1323
1521
  */
1324
1522
  doInitialSchematicComponentRender() {
1325
- const { db } = this.project;
1523
+ const { db } = this.root;
1326
1524
  const { schematicSymbolName } = this.config;
1327
1525
  if (!schematicSymbolName) return;
1328
1526
  const symbol_name = `${this.config.schematicSymbolName}_horz`;
@@ -1341,10 +1539,10 @@ var NormalComponent = class extends PrimitiveComponent {
1341
1539
  this.schematic_component_id = schematic_component2.schematic_component_id;
1342
1540
  }
1343
1541
  doInitialPcbComponentRender() {
1344
- const { db } = this.project;
1542
+ const { db } = this.root;
1345
1543
  const { _parsedProps: props } = this;
1346
1544
  const pcb_component = db.pcb_component.insert({
1347
- center: this.getGlobalPcbPosition(),
1545
+ center: this._getGlobalPcbPositionBeforeLayout(),
1348
1546
  // width/height are computed in the PcbComponentSizeCalculation phase
1349
1547
  width: 0,
1350
1548
  height: 0,
@@ -1360,7 +1558,7 @@ var NormalComponent = class extends PrimitiveComponent {
1360
1558
  */
1361
1559
  doInitialPcbComponentSizeCalculation() {
1362
1560
  if (!this.pcb_component_id) return;
1363
- const { db } = this.project;
1561
+ const { db } = this.root;
1364
1562
  const { _parsedProps: props } = this;
1365
1563
  let minX = Infinity;
1366
1564
  let minY = Infinity;
@@ -1368,7 +1566,7 @@ var NormalComponent = class extends PrimitiveComponent {
1368
1566
  let maxY = -Infinity;
1369
1567
  for (const child of this.children) {
1370
1568
  if (child.isPcbPrimitive) {
1371
- const { x, y } = child.getGlobalPcbPosition();
1569
+ const { x, y } = child._getGlobalPcbPositionBeforeLayout();
1372
1570
  const { width: width2, height: height2 } = child.getPcbSize();
1373
1571
  minX = Math.min(minX, x - width2 / 2);
1374
1572
  minY = Math.min(minY, y - height2 / 2);
@@ -1389,6 +1587,9 @@ var NormalComponent = class extends PrimitiveComponent {
1389
1587
  component: createInstanceFromReactElement(element)
1390
1588
  };
1391
1589
  }
1590
+ doInitialInitializePortsFromChildren() {
1591
+ this.initPorts();
1592
+ }
1392
1593
  doInitialReactSubtreesRender() {
1393
1594
  if (isReactElement2(this.props.footprint)) {
1394
1595
  if (this.reactSubtrees.some((rs) => rs.element === this.props.footprint))
@@ -1398,6 +1599,16 @@ var NormalComponent = class extends PrimitiveComponent {
1398
1599
  this.add(subtree.component);
1399
1600
  }
1400
1601
  }
1602
+ _hasExistingPortExactly(port1) {
1603
+ const existingPorts = this.children.filter(
1604
+ (c) => c.componentName === "Port"
1605
+ );
1606
+ return existingPorts.some((port2) => {
1607
+ const aliases1 = port1.getNameAndAliases();
1608
+ const aliases2 = port2.getNameAndAliases();
1609
+ return aliases1.length === aliases2.length && aliases1.every((alias) => aliases2.includes(alias));
1610
+ });
1611
+ }
1401
1612
  add(componentOrElm) {
1402
1613
  let component;
1403
1614
  if (isReactElement2(componentOrElm)) {
@@ -1407,6 +1618,20 @@ var NormalComponent = class extends PrimitiveComponent {
1407
1618
  } else {
1408
1619
  component = componentOrElm;
1409
1620
  }
1621
+ if (component.componentName === "Port") {
1622
+ if (this._hasExistingPortExactly(component)) return;
1623
+ const existingPorts = this.children.filter(
1624
+ (c) => c.componentName === "Port"
1625
+ );
1626
+ const conflictingPort = existingPorts.find(
1627
+ (p) => p.isMatchingAnyOf(component.getNameAndAliases())
1628
+ );
1629
+ if (conflictingPort) {
1630
+ throw new Error(
1631
+ `Conflicting ports added. Port 1: ${conflictingPort}, Port 2: ${component}`
1632
+ );
1633
+ }
1634
+ }
1410
1635
  super.add(component);
1411
1636
  }
1412
1637
  getPortsFromFootprint() {
@@ -1434,6 +1659,11 @@ var NormalComponent = class extends PrimitiveComponent {
1434
1659
  if (!newPort) continue;
1435
1660
  newPorts2.push(newPort);
1436
1661
  }
1662
+ if (newPorts2.length === 0) {
1663
+ throw new Error(
1664
+ `No ports found in chip ${this} (define a schPortArrangement, a footprint or add portHints to elements of the footprint)`
1665
+ );
1666
+ }
1437
1667
  return newPorts2;
1438
1668
  }
1439
1669
  const newPorts = [];
@@ -1526,7 +1756,7 @@ var Board = class extends Group {
1526
1756
  };
1527
1757
  }
1528
1758
  doInitialPcbComponentRender() {
1529
- const { db } = this.project;
1759
+ const { db } = this.root;
1530
1760
  const { _parsedProps: props } = this;
1531
1761
  if (!props.width || !props.height) {
1532
1762
  throw new Error("Board width and height or an outline are required");
@@ -1539,12 +1769,12 @@ var Board = class extends Group {
1539
1769
  this.pcb_board_id = pcb_board.pcb_board_id;
1540
1770
  }
1541
1771
  removePcbComponentRender() {
1542
- const { db } = this.project;
1772
+ const { db } = this.root;
1543
1773
  if (!this.pcb_board_id) return;
1544
1774
  db.pcb_board.delete(this.pcb_board_id);
1545
1775
  this.pcb_board_id = null;
1546
1776
  }
1547
- computePcbGlobalTransform() {
1777
+ _computePcbGlobalTransformBeforeLayout() {
1548
1778
  return identity4();
1549
1779
  }
1550
1780
  };
@@ -1685,8 +1915,8 @@ var mergeRoutes = (routes) => {
1685
1915
 
1686
1916
  // lib/utils/getClosest.ts
1687
1917
  var getDistance = (a, b) => {
1688
- const aPos = "getGlobalPcbPosition" in a ? a.getGlobalPcbPosition() : a;
1689
- const bPos = "getGlobalPcbPosition" in b ? b.getGlobalPcbPosition() : b;
1918
+ const aPos = "_getGlobalPcbPositionBeforeLayout" in a ? a._getGlobalPcbPositionBeforeLayout() : a;
1919
+ const bPos = "_getGlobalPcbPositionBeforeLayout" in b ? b._getGlobalPcbPositionBeforeLayout() : b;
1690
1920
  return Math.sqrt((aPos.x - bPos.x) ** 2 + (aPos.y - bPos.y) ** 2);
1691
1921
  };
1692
1922
  function getClosest(point, candidates) {
@@ -1707,7 +1937,7 @@ function getClosest(point, candidates) {
1707
1937
  // lib/components/primitive-components/Trace.ts
1708
1938
  import "zod";
1709
1939
  var portToObjective = (port) => {
1710
- const portPosition = port.getGlobalPcbPosition();
1940
+ const portPosition = port._getGlobalPcbPositionBeforeLayout();
1711
1941
  return {
1712
1942
  ...portPosition,
1713
1943
  layers: port.getAvailablePcbLayers()
@@ -1752,7 +1982,7 @@ var Trace = class extends PrimitiveComponent {
1752
1982
  );
1753
1983
  }
1754
1984
  _findConnectedPorts() {
1755
- const { db } = this.project;
1985
+ const { db } = this.root;
1756
1986
  const { _parsedProps: props, parent } = this;
1757
1987
  if (!parent) throw new Error("Trace has no parent");
1758
1988
  const portSelectors = this.getTracePortPathSelectors();
@@ -1836,7 +2066,7 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
1836
2066
  createNetsFromProps(this, this.getTracePathNetSelectors());
1837
2067
  }
1838
2068
  doInitialSourceTraceRender() {
1839
- const { db } = this.project;
2069
+ const { db } = this.root;
1840
2070
  const { _parsedProps: props, parent } = this;
1841
2071
  if (!parent) {
1842
2072
  this.renderError("Trace has no parent");
@@ -1852,7 +2082,7 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
1852
2082
  this.source_trace_id = trace.source_trace_id;
1853
2083
  }
1854
2084
  doInitialPcbTraceRender() {
1855
- const { db } = this.project;
2085
+ const { db } = this.root;
1856
2086
  const { _parsedProps: props, parent } = this;
1857
2087
  if (!parent) throw new Error("Trace has no parent");
1858
2088
  const { allPortsFound, ports } = this._findConnectedPorts();
@@ -1933,7 +2163,7 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
1933
2163
  `Could not find a common layer (using hints) for trace ${this.getString()}`
1934
2164
  );
1935
2165
  }
1936
- const obstacles = getObstaclesFromSoup(this.project.db.toArray());
2166
+ const obstacles = getObstaclesFromSoup(this.root.db.toArray());
1937
2167
  markObstaclesAsConnected(
1938
2168
  obstacles,
1939
2169
  orderedRouteObjectives,
@@ -1999,7 +2229,7 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
1999
2229
  this.pcb_trace_id = pcb_trace.pcb_trace_id;
2000
2230
  }
2001
2231
  doInitialSchematicTraceRender() {
2002
- const { db } = this.project;
2232
+ const { db } = this.root;
2003
2233
  const { _parsedProps: props, parent } = this;
2004
2234
  if (!parent) throw new Error("Trace has no parent");
2005
2235
  const { allPortsFound, portsWithSelectors: ports } = this._findConnectedPorts();
@@ -2023,7 +2253,7 @@ searched component ${targetComponent.getString()}, which has ports: ${targetComp
2023
2253
  for (const { port } of ports) {
2024
2254
  connection.pointsToConnect.push(
2025
2255
  projectPointInDirection(
2026
- port.getGlobalSchematicPosition(),
2256
+ port._getGlobalSchematicPositionBeforeLayout(),
2027
2257
  port.facingDirection,
2028
2258
  0.1501
2029
2259
  )
@@ -2093,7 +2323,7 @@ var Resistor = class extends NormalComponent {
2093
2323
  }
2094
2324
  }
2095
2325
  doInitialSourceRender() {
2096
- const { db } = this.project;
2326
+ const { db } = this.root;
2097
2327
  const { _parsedProps: props } = this;
2098
2328
  const source_component = db.source_component.insert({
2099
2329
  ftype: "simple_resistor",
@@ -2171,7 +2401,7 @@ var Capacitor = class extends NormalComponent {
2171
2401
  }
2172
2402
  }
2173
2403
  doInitialSourceRender() {
2174
- const { db } = this.project;
2404
+ const { db } = this.root;
2175
2405
  const { _parsedProps: props } = this;
2176
2406
  const source_component = db.source_component.insert({
2177
2407
  ftype: "simple_capacitor",
@@ -2191,7 +2421,7 @@ import { applyToPoint as applyToPoint4 } from "transformation-matrix";
2191
2421
  var TraceHint = class extends PrimitiveComponent {
2192
2422
  matchedPort = null;
2193
2423
  doInitialPortMatching() {
2194
- const { db } = this.project;
2424
+ const { db } = this.root;
2195
2425
  const { _parsedProps: props, parent } = this;
2196
2426
  if (!parent) return;
2197
2427
  if (parent.componentName === "Trace") {
@@ -2218,7 +2448,7 @@ var TraceHint = class extends PrimitiveComponent {
2218
2448
  const { _parsedProps: props } = this;
2219
2449
  const offsets = props.offset ? [props.offset] : props.offsets;
2220
2450
  if (!offsets) return [];
2221
- const globalTransform = this.computePcbGlobalTransform();
2451
+ const globalTransform = this._computePcbGlobalTransformBeforeLayout();
2222
2452
  return offsets.map(
2223
2453
  (offset) => ({
2224
2454
  ...applyToPoint4(globalTransform, offset),
@@ -2522,7 +2752,7 @@ var Chip = class extends NormalComponent {
2522
2752
  };
2523
2753
  }
2524
2754
  doInitialSourceRender() {
2525
- const { db } = this.project;
2755
+ const { db } = this.root;
2526
2756
  const { _parsedProps: props } = this;
2527
2757
  const source_component = db.source_component.insert({
2528
2758
  ftype: "simple_chip",
@@ -2533,7 +2763,7 @@ var Chip = class extends NormalComponent {
2533
2763
  this.source_component_id = source_component.source_component_id;
2534
2764
  }
2535
2765
  doInitialSchematicComponentRender() {
2536
- const { db } = this.project;
2766
+ const { db } = this.root;
2537
2767
  const { _parsedProps: props } = this;
2538
2768
  const ports = this.children.filter((child) => child instanceof Port);
2539
2769
  const pinSpacing = props.schPinSpacing ?? 0.2;
@@ -2565,7 +2795,7 @@ var Chip = class extends NormalComponent {
2565
2795
  this.schematic_component_id = schematic_component2.schematic_component_id;
2566
2796
  }
2567
2797
  doInitialPcbComponentRender() {
2568
- const { db } = this.project;
2798
+ const { db } = this.root;
2569
2799
  const { _parsedProps: props } = this;
2570
2800
  const pcb_component = db.pcb_component.insert({
2571
2801
  center: { x: props.pcbX ?? 0, y: props.pcbY ?? 0 },
@@ -2591,7 +2821,7 @@ var Jumper = class extends NormalComponent {
2591
2821
  };
2592
2822
  }
2593
2823
  doInitialSourceRender() {
2594
- const { db } = this.project;
2824
+ const { db } = this.root;
2595
2825
  const { _parsedProps: props } = this;
2596
2826
  const source_component = db.source_component.insert({
2597
2827
  ftype: "simple_chip",
@@ -2603,7 +2833,7 @@ var Jumper = class extends NormalComponent {
2603
2833
  this.source_component_id = source_component.source_component_id;
2604
2834
  }
2605
2835
  doInitialSchematicComponentRender() {
2606
- const { db } = this.project;
2836
+ const { db } = this.root;
2607
2837
  const { _parsedProps: props } = this;
2608
2838
  const ports = this.children.filter((child) => child instanceof Port);
2609
2839
  const pinSpacing = props.schPinSpacing ?? 0.2;
@@ -2638,7 +2868,7 @@ var Jumper = class extends NormalComponent {
2638
2868
  this.schematic_component_id = schematic_component2.schematic_component_id;
2639
2869
  }
2640
2870
  doInitialPcbComponentRender() {
2641
- const { db } = this.project;
2871
+ const { db } = this.root;
2642
2872
  const { _parsedProps: props } = this;
2643
2873
  const pcb_component = db.pcb_component.insert({
2644
2874
  center: { x: props.pcbX ?? 0, y: props.pcbY ?? 0 },
@@ -2654,18 +2884,58 @@ var Jumper = class extends NormalComponent {
2654
2884
  }
2655
2885
  };
2656
2886
 
2887
+ // lib/components/primitive-components/Constraint.ts
2888
+ import { constraintProps } from "@tscircuit/props";
2889
+ import "zod";
2890
+ var Constraint2 = class extends PrimitiveComponent {
2891
+ get config() {
2892
+ return {
2893
+ zodProps: constraintProps
2894
+ };
2895
+ }
2896
+ constructor(props) {
2897
+ super(props);
2898
+ if ("xdist" in props || "ydist" in props) {
2899
+ if (!("edgeToEdge" in props) && !("centerToCenter" in props)) {
2900
+ throw new Error(
2901
+ "edgeToEdge, centerToCenter (or from*/to* props) must be set for xdist or ydist for <constraint />"
2902
+ );
2903
+ }
2904
+ }
2905
+ }
2906
+ _getAllReferencedComponents() {
2907
+ const componentsWithSelectors = [];
2908
+ for (const key of ["left", "right", "top", "bottom"]) {
2909
+ if (key in this._parsedProps) {
2910
+ const selector = this._parsedProps[key];
2911
+ const component = this.getSubcircuit().selectOne(selector);
2912
+ if (component) {
2913
+ componentsWithSelectors.push({
2914
+ selector,
2915
+ component
2916
+ });
2917
+ }
2918
+ }
2919
+ }
2920
+ return { componentsWithSelectors };
2921
+ }
2922
+ };
2923
+
2657
2924
  // lib/Project.ts
2658
2925
  import { su } from "@tscircuit/soup-util";
2659
2926
  import { isValidElement as isValidElement2 } from "react";
2660
2927
  import { identity as identity5 } from "transformation-matrix";
2661
2928
  var Circuit = class {
2662
- rootComponent = null;
2929
+ firstChild = null;
2663
2930
  children;
2664
2931
  db;
2932
+ root = null;
2933
+ isRoot = true;
2665
2934
  _hasRenderedAtleastOnce = false;
2666
2935
  constructor() {
2667
2936
  this.children = [];
2668
2937
  this.db = su([]);
2938
+ this.root = this;
2669
2939
  }
2670
2940
  add(componentOrElm) {
2671
2941
  let component;
@@ -2677,9 +2947,9 @@ var Circuit = class {
2677
2947
  this.children.push(component);
2678
2948
  }
2679
2949
  _guessRootComponent() {
2680
- if (this.rootComponent) return;
2950
+ if (this.firstChild) return;
2681
2951
  if (this.children.length === 1) {
2682
- this.rootComponent = this.children[0];
2952
+ this.firstChild = this.children[0];
2683
2953
  return;
2684
2954
  }
2685
2955
  if (this.children.length === 0) {
@@ -2690,7 +2960,7 @@ var Circuit = class {
2690
2960
  if (this.children.length > 0) {
2691
2961
  const board = this.children.find((c) => c.componentName === "Board") ?? null;
2692
2962
  if (board) {
2693
- this.rootComponent = board;
2963
+ this.firstChild = board;
2694
2964
  return;
2695
2965
  }
2696
2966
  }
@@ -2699,13 +2969,13 @@ var Circuit = class {
2699
2969
  );
2700
2970
  }
2701
2971
  render() {
2702
- if (!this.rootComponent) {
2972
+ if (!this.firstChild) {
2703
2973
  this._guessRootComponent();
2704
2974
  }
2705
- const { rootComponent, db } = this;
2706
- if (!rootComponent) throw new Error("Project has no root component");
2707
- rootComponent.setProject(this);
2708
- rootComponent.runRenderCycle();
2975
+ const { firstChild, db } = this;
2976
+ if (!firstChild) throw new Error("Project has no root component");
2977
+ firstChild.parent = this;
2978
+ firstChild.runRenderCycle();
2709
2979
  this._hasRenderedAtleastOnce = true;
2710
2980
  }
2711
2981
  getSoup() {
@@ -2729,17 +2999,17 @@ var Circuit = class {
2729
2999
  const previewOpts = typeof previewNameOrOpts === "object" ? previewNameOrOpts : { previewName: previewNameOrOpts };
2730
3000
  throw new Error("project.preview is not yet implemented");
2731
3001
  }
2732
- computeGlobalSchematicTransform() {
3002
+ computeSchematicGlobalTransform() {
2733
3003
  return identity5();
2734
3004
  }
2735
- computeGlobalPcbTransform() {
3005
+ _computePcbGlobalTransformBeforeLayout() {
2736
3006
  return identity5();
2737
3007
  }
2738
3008
  selectAll(selector) {
2739
- return this.rootComponent?.selectAll(selector) ?? [];
3009
+ return this.firstChild?.selectAll(selector) ?? [];
2740
3010
  }
2741
3011
  selectOne(selector, opts) {
2742
- return this.rootComponent?.selectOne(selector, opts) ?? null;
3012
+ return this.firstChild?.selectOne(selector, opts) ?? null;
2743
3013
  }
2744
3014
  };
2745
3015
  var Project = Circuit;
@@ -2754,6 +3024,7 @@ export {
2754
3024
  Capacitor,
2755
3025
  Chip,
2756
3026
  Circuit,
3027
+ Constraint2 as Constraint,
2757
3028
  Footprint,
2758
3029
  Group,
2759
3030
  Jumper,