@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/lib/Project.ts CHANGED
@@ -7,15 +7,18 @@ import { createInstanceFromReactElement } from "./fiber/create-instance-from-rea
7
7
  import { identity, type Matrix } from "transformation-matrix"
8
8
 
9
9
  export class Circuit {
10
- rootComponent: PrimitiveComponent | null = null
10
+ firstChild: PrimitiveComponent | null = null
11
11
  children: PrimitiveComponent[]
12
12
  db: SoupUtilObjects
13
+ root: Circuit | null = null
14
+ isRoot = true
13
15
 
14
16
  _hasRenderedAtleastOnce = false
15
17
 
16
18
  constructor() {
17
19
  this.children = []
18
20
  this.db = su([])
21
+ this.root = this
19
22
  }
20
23
 
21
24
  add(componentOrElm: PrimitiveComponent | ReactElement) {
@@ -30,9 +33,9 @@ export class Circuit {
30
33
  }
31
34
 
32
35
  _guessRootComponent() {
33
- if (this.rootComponent) return
36
+ if (this.firstChild) return
34
37
  if (this.children.length === 1) {
35
- this.rootComponent = this.children[0]
38
+ this.firstChild = this.children[0]
36
39
  return
37
40
  }
38
41
  if (this.children.length === 0) {
@@ -46,7 +49,7 @@ export class Circuit {
46
49
  this.children.find((c) => c.componentName === "Board") ?? null
47
50
 
48
51
  if (board) {
49
- this.rootComponent = board
52
+ this.firstChild = board
50
53
  return
51
54
  }
52
55
  }
@@ -56,16 +59,14 @@ export class Circuit {
56
59
  }
57
60
 
58
61
  render() {
59
- if (!this.rootComponent) {
62
+ if (!this.firstChild) {
60
63
  this._guessRootComponent()
61
64
  }
62
- const { rootComponent, db } = this
65
+ const { firstChild, db } = this
63
66
 
64
- if (!rootComponent) throw new Error("Project has no root component")
65
-
66
- rootComponent.setProject(this)
67
-
68
- rootComponent.runRenderCycle()
67
+ if (!firstChild) throw new Error("Project has no root component")
68
+ firstChild.parent = this as any
69
+ firstChild.runRenderCycle()
69
70
  this._hasRenderedAtleastOnce = true
70
71
  }
71
72
 
@@ -103,24 +104,27 @@ export class Circuit {
103
104
  throw new Error("project.preview is not yet implemented")
104
105
  }
105
106
 
106
- computeGlobalSchematicTransform(): Matrix {
107
+ computeSchematicGlobalTransform(): Matrix {
107
108
  return identity()
108
109
  }
109
110
 
110
- computeGlobalPcbTransform(): Matrix {
111
+ _computePcbGlobalTransformBeforeLayout(): Matrix {
111
112
  return identity()
112
113
  }
113
114
 
114
115
  selectAll(selector: string): PrimitiveComponent[] {
115
- return this.rootComponent?.selectAll(selector) ?? []
116
+ return this.firstChild?.selectAll(selector) ?? []
116
117
  }
117
118
 
118
119
  selectOne(
119
120
  selector: string,
120
121
  opts?: { type?: "component" | "port" },
121
122
  ): PrimitiveComponent | null {
122
- return this.rootComponent?.selectOne(selector, opts) ?? null
123
+ return this.firstChild?.selectOne(selector, opts) ?? null
123
124
  }
124
125
  }
125
126
 
127
+ /**
128
+ * @deprecated
129
+ */
126
130
  export const Project = Circuit
@@ -46,6 +46,8 @@ export class NormalComponent<
46
46
  > extends PrimitiveComponent<ZodProps> {
47
47
  reactSubtrees: Array<ReactSubtree> = []
48
48
 
49
+ isPrimitiveContainer = true
50
+
49
51
  constructor(props: z.input<ZodProps>) {
50
52
  super(props)
51
53
  this._addChildrenFromStringFootprint()
@@ -68,6 +70,7 @@ export class NormalComponent<
68
70
  */
69
71
  initPorts() {
70
72
  const { config } = this
73
+ const portsToCreate: Port[] = []
71
74
  if (config.schematicSymbolName) {
72
75
  const sym = symbols[
73
76
  `${config.schematicSymbolName}_horz` as keyof typeof symbols
@@ -79,32 +82,36 @@ export class NormalComponent<
79
82
 
80
83
  if (port) {
81
84
  port.schematicSymbolPortDef = symPort
82
- this.add(port)
85
+ portsToCreate.push(port)
83
86
  }
84
87
  }
85
88
 
89
+ this.addAll(portsToCreate)
86
90
  return
87
91
  }
88
92
 
89
93
  const portsFromFootprint = this.getPortsFromFootprint()
90
-
91
- this.addAll(portsFromFootprint)
94
+ portsToCreate.push(...portsFromFootprint)
92
95
 
93
96
  const pinLabels: Record<string, string> | undefined =
94
97
  this._parsedProps.pinLabels
95
98
  if (pinLabels) {
96
99
  for (let [pinNumber, label] of Object.entries(pinLabels)) {
97
100
  pinNumber = pinNumber.replace("pin", "")
98
- const port = this.selectOne(`port[pinNumber='${pinNumber}']`)
99
- if (!port) {
101
+ const existingPort = portsToCreate.find(
102
+ (p) => p._parsedProps.pinNumber === Number(pinNumber),
103
+ )
104
+ if (!existingPort) {
100
105
  throw new Error(
101
106
  `Could not find port for pin number ${pinNumber} in chip ${this.getString()}`,
102
107
  )
103
108
  }
104
- port.externallyAddedAliases.push(label)
105
- port.props.name = label
109
+ existingPort.externallyAddedAliases.push(label)
110
+ existingPort.props.name = label
106
111
  }
107
112
  }
113
+
114
+ this.addAll(portsToCreate)
108
115
  }
109
116
 
110
117
  _addChildrenFromStringFootprint() {
@@ -148,7 +155,7 @@ export class NormalComponent<
148
155
  doInitialSourceRender() {
149
156
  const ftype = this.config.sourceFtype
150
157
  if (!ftype) return
151
- const { db } = this.project!
158
+ const { db } = this.root!
152
159
  const { _parsedProps: props } = this
153
160
  const source_component = db.source_component.insert({
154
161
  ftype,
@@ -166,7 +173,7 @@ export class NormalComponent<
166
173
  * You can override this method to do more complicated things.
167
174
  */
168
175
  doInitialSchematicComponentRender() {
169
- const { db } = this.project!
176
+ const { db } = this.root!
170
177
  const { schematicSymbolName } = this.config
171
178
  if (!schematicSymbolName) return
172
179
  // TODO switch between horizontal and vertical based on schRotation
@@ -191,10 +198,10 @@ export class NormalComponent<
191
198
  }
192
199
 
193
200
  doInitialPcbComponentRender() {
194
- const { db } = this.project!
201
+ const { db } = this.root!
195
202
  const { _parsedProps: props } = this
196
203
  const pcb_component = db.pcb_component.insert({
197
- center: this.getGlobalPcbPosition(),
204
+ center: this._getGlobalPcbPositionBeforeLayout(),
198
205
  // width/height are computed in the PcbComponentSizeCalculation phase
199
206
  width: 0,
200
207
  height: 0,
@@ -211,7 +218,7 @@ export class NormalComponent<
211
218
  */
212
219
  doInitialPcbComponentSizeCalculation(): void {
213
220
  if (!this.pcb_component_id) return
214
- const { db } = this.project!
221
+ const { db } = this.root!
215
222
  const { _parsedProps: props } = this
216
223
 
217
224
  let minX = Infinity
@@ -221,7 +228,7 @@ export class NormalComponent<
221
228
 
222
229
  for (const child of this.children) {
223
230
  if (child.isPcbPrimitive) {
224
- const { x, y } = child.getGlobalPcbPosition()
231
+ const { x, y } = child._getGlobalPcbPositionBeforeLayout()
225
232
  const { width, height } = child.getPcbSize()
226
233
  minX = Math.min(minX, x - width / 2)
227
234
  minY = Math.min(minY, y - height / 2)
@@ -246,6 +253,10 @@ export class NormalComponent<
246
253
  }
247
254
  }
248
255
 
256
+ doInitialInitializePortsFromChildren(): void {
257
+ this.initPorts()
258
+ }
259
+
249
260
  doInitialReactSubtreesRender(): void {
250
261
  if (isReactElement(this.props.footprint)) {
251
262
  if (this.reactSubtrees.some((rs) => rs.element === this.props.footprint))
@@ -256,6 +267,20 @@ export class NormalComponent<
256
267
  }
257
268
  }
258
269
 
270
+ _hasExistingPortExactly(port1: Port): boolean {
271
+ const existingPorts = this.children.filter(
272
+ (c) => c.componentName === "Port",
273
+ ) as Port[]
274
+ return existingPorts.some((port2) => {
275
+ const aliases1 = port1.getNameAndAliases()
276
+ const aliases2 = port2.getNameAndAliases()
277
+ return (
278
+ aliases1.length === aliases2.length &&
279
+ aliases1.every((alias) => aliases2.includes(alias))
280
+ )
281
+ })
282
+ }
283
+
259
284
  add(componentOrElm: PrimitiveComponent | ReactElement) {
260
285
  let component: PrimitiveComponent
261
286
  if (isReactElement(componentOrElm)) {
@@ -266,6 +291,23 @@ export class NormalComponent<
266
291
  component = componentOrElm as PrimitiveComponent
267
292
  }
268
293
 
294
+ if (component.componentName === "Port") {
295
+ if (this._hasExistingPortExactly(component as Port)) return
296
+ // Check if this port is already contained in the children, skip if it's
297
+ // already defined
298
+ const existingPorts = this.children.filter(
299
+ (c) => c.componentName === "Port",
300
+ ) as Port[]
301
+ const conflictingPort = existingPorts.find((p) =>
302
+ p.isMatchingAnyOf(component.getNameAndAliases()),
303
+ )
304
+ if (conflictingPort) {
305
+ throw new Error(
306
+ `Conflicting ports added. Port 1: ${conflictingPort}, Port 2: ${component}`,
307
+ )
308
+ }
309
+ }
310
+
269
311
  super.add(component)
270
312
  }
271
313
 
@@ -304,6 +346,12 @@ export class NormalComponent<
304
346
  newPorts.push(newPort)
305
347
  }
306
348
 
349
+ if (newPorts.length === 0) {
350
+ throw new Error(
351
+ `No ports found in chip ${this} (define a schPortArrangement, a footprint or add portHints to elements of the footprint)`,
352
+ )
353
+ }
354
+
307
355
  return newPorts
308
356
  }
309
357
 
@@ -41,7 +41,6 @@ export abstract class PrimitiveComponent<
41
41
  }
42
42
  }
43
43
 
44
- project: Circuit | null = null
45
44
  props: z.input<ZodProps>
46
45
  _parsedProps: z.infer<ZodProps>
47
46
 
@@ -60,11 +59,22 @@ export abstract class PrimitiveComponent<
60
59
  return (
61
60
  Boolean(this.props.subcircuit) ||
62
61
  // Implied opaque group for top-level group
63
- (this.lowercaseComponentName === "group" &&
64
- this?.parent?.props?.name === "$root")
62
+ (this.lowercaseComponentName === "group" && (this?.parent as any)?.isRoot)
65
63
  )
66
64
  }
67
65
 
66
+ /**
67
+ * A primitive container is a component that contains one or more ports and
68
+ * primitive components that are designed to interact.
69
+ *
70
+ * For example a resistor contains ports and smtpads that interact, so the
71
+ * resistor is a primitive container. Inside a primitive container, the ports
72
+ * and pads are likely to reference each other and look for eachother during
73
+ * the port matching phase.
74
+ *
75
+ */
76
+ isPrimitiveContainer = false
77
+
68
78
  source_group_id: string | null = null
69
79
  source_component_id: string | null = null
70
80
  schematic_component_id: string | null = null
@@ -86,13 +96,6 @@ export abstract class PrimitiveComponent<
86
96
  }
87
97
  }
88
98
 
89
- setProject(project: Circuit) {
90
- this.project = project
91
- for (const c of this.children) {
92
- c.setProject(project)
93
- }
94
- }
95
-
96
99
  setProps(props: Partial<z.input<ZodProps>>) {
97
100
  const newProps = this.config.zodProps.parse({
98
101
  ...this.props,
@@ -129,9 +132,9 @@ export abstract class PrimitiveComponent<
129
132
  * components, including this component's translation and rotation.
130
133
  *
131
134
  * This is used to compute this component's position as well as all children
132
- * components positions
135
+ * components positions before layout is applied
133
136
  */
134
- computePcbGlobalTransform(): Matrix {
137
+ _computePcbGlobalTransformBeforeLayout(): Matrix {
135
138
  const { _parsedProps: props } = this
136
139
  const manualPlacement =
137
140
  this.getSubcircuit()._getManualPlacementForComponent(this)
@@ -143,7 +146,7 @@ export abstract class PrimitiveComponent<
143
146
  this.props.pcbY === undefined
144
147
  ) {
145
148
  return compose(
146
- this.parent?.computePcbGlobalTransform() ?? identity(),
149
+ this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
147
150
  compose(
148
151
  translate(manualPlacement.x, manualPlacement.y),
149
152
  rotate(((props.pcbRotation ?? 0) * Math.PI) / 180),
@@ -152,11 +155,46 @@ export abstract class PrimitiveComponent<
152
155
  }
153
156
 
154
157
  return compose(
155
- this.parent?.computePcbGlobalTransform() ?? identity(),
158
+ this.parent?._computePcbGlobalTransformBeforeLayout() ?? identity(),
156
159
  this.computePcbPropsTransform(),
157
160
  )
158
161
  }
159
162
 
163
+ getPrimitiveContainer(): PrimitiveComponent | null {
164
+ if (this.isPrimitiveContainer) return this
165
+ return this.parent?.getPrimitiveContainer?.() ?? null
166
+ }
167
+
168
+ /**
169
+ * Compute the bounds of this component the circuit json elements associated
170
+ * with it.
171
+ */
172
+ _getCircuitJsonBounds(): {
173
+ center: { x: number; y: number }
174
+ bounds: { left: number; top: number; right: number; bottom: number }
175
+ width: number
176
+ height: number
177
+ } {
178
+ return {
179
+ center: { x: 0, y: 0 },
180
+ bounds: { left: 0, top: 0, right: 0, bottom: 0 },
181
+ width: 0,
182
+ height: 0,
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Set the position of this component from the layout solver. This method
188
+ * should operate using CircuitJson associated with this component, like
189
+ * _getCircuitJsonBounds it can be called multiple times as different
190
+ * parents apply layout to their children.
191
+ */
192
+ _setPositionFromLayout(newCenter: { x: number; y: number }) {
193
+ throw new Error(
194
+ `_setPositionFromLayout not implemented for ${this.componentName}`,
195
+ )
196
+ }
197
+
160
198
  /**
161
199
  * Computes a transformation matrix from the props of this component for
162
200
  * schematic components
@@ -209,7 +247,7 @@ export abstract class PrimitiveComponent<
209
247
  for (const position of placementConfig.positions) {
210
248
  if (isMatchingSelector(component, position.selector)) {
211
249
  const center = applyToPoint(
212
- this.computePcbGlobalTransform(),
250
+ this._computePcbGlobalTransformBeforeLayout(),
213
251
  position.center,
214
252
  )
215
253
  return center
@@ -219,17 +257,23 @@ export abstract class PrimitiveComponent<
219
257
  return null
220
258
  }
221
259
 
222
- getGlobalPcbPosition(): { x: number; y: number } {
223
- return applyToPoint(this.computePcbGlobalTransform(), { x: 0, y: 0 })
260
+ _getGlobalPcbPositionBeforeLayout(): { x: number; y: number } {
261
+ return applyToPoint(this._computePcbGlobalTransformBeforeLayout(), {
262
+ x: 0,
263
+ y: 0,
264
+ })
224
265
  }
225
266
 
226
- getGlobalSchematicPosition(): { x: number; y: number } {
267
+ _getGlobalSchematicPositionBeforeLayout(): { x: number; y: number } {
227
268
  return applyToPoint(this.computeSchematicGlobalTransform(), { x: 0, y: 0 })
228
269
  }
229
270
 
271
+ get root(): Circuit | null {
272
+ return this.parent?.root ?? null
273
+ }
274
+
230
275
  onAddToParent(parent: PrimitiveComponent) {
231
276
  this.parent = parent
232
- this.project = parent.project
233
277
  }
234
278
 
235
279
  /**
@@ -2,12 +2,13 @@ import type { PCBPlacementError, PCBTraceError } from "@tscircuit/soup"
2
2
  import { Component, createElement, type ReactElement } from "react"
3
3
 
4
4
  export const orderedRenderPhases = [
5
- "ReactSubtreesRender", // probably going to be removed b/c subtrees should render instantly
5
+ "ReactSubtreesRender",
6
+ "InitializePortsFromChildren",
6
7
  "CreateNetsFromProps",
7
8
  "CreateTracesFromProps",
8
9
  "SourceRender",
9
10
  "SourceParentAttachment",
10
- "PortDiscovery", // probably going to be removed b/c port discovery can always be done on prop change
11
+ "PortDiscovery",
11
12
  "PortMatching",
12
13
  "SourceTraceRender",
13
14
  "SchematicComponentRender",
@@ -15,8 +16,9 @@ export const orderedRenderPhases = [
15
16
  "SchematicPortRender",
16
17
  "SchematicTraceRender",
17
18
  "PcbComponentRender",
18
- "PcbPortRender",
19
19
  "PcbPrimitiveRender",
20
+ "PcbFootprintLayout",
21
+ "PcbPortRender",
20
22
  "PcbParentAttachment",
21
23
  "PcbLayout",
22
24
  "PcbTraceRender",
@@ -16,3 +16,4 @@ export { Chip } from "./normal-components/Chip"
16
16
  export { Jumper } from "./normal-components/Jumper"
17
17
  export { SilkscreenPath } from "./primitive-components/SilkscreenPath"
18
18
  export { PlatedHole } from "./primitive-components/PlatedHole"
19
+ export { Constraint } from "./primitive-components/Constraint"
@@ -18,7 +18,7 @@ export class Board extends Group<typeof boardProps> {
18
18
  }
19
19
 
20
20
  doInitialPcbComponentRender(): void {
21
- const { db } = this.project!
21
+ const { db } = this.root!
22
22
  const { _parsedProps: props } = this
23
23
 
24
24
  if (!props.width || !props.height) {
@@ -36,13 +36,13 @@ export class Board extends Group<typeof boardProps> {
36
36
  }
37
37
 
38
38
  removePcbComponentRender(): void {
39
- const { db } = this.project!
39
+ const { db } = this.root!
40
40
  if (!this.pcb_board_id) return
41
41
  db.pcb_board.delete(this.pcb_board_id!)
42
42
  this.pcb_board_id = null
43
43
  }
44
44
 
45
- computePcbGlobalTransform(): Matrix {
45
+ _computePcbGlobalTransformBeforeLayout(): Matrix {
46
46
  return identity()
47
47
  }
48
48
  }
@@ -54,7 +54,7 @@ export class Capacitor extends NormalComponent<
54
54
  }
55
55
 
56
56
  doInitialSourceRender() {
57
- const { db } = this.project!
57
+ const { db } = this.root!
58
58
  const { _parsedProps: props } = this
59
59
  const source_component = db.source_component.insert({
60
60
  ftype: "simple_capacitor",
@@ -22,7 +22,7 @@ export class Chip<PinLabels extends string = never> extends NormalComponent<
22
22
  }
23
23
 
24
24
  doInitialSourceRender(): void {
25
- const { db } = this.project!
25
+ const { db } = this.root!
26
26
  const { _parsedProps: props } = this
27
27
 
28
28
  const source_component = db.source_component.insert({
@@ -36,7 +36,7 @@ export class Chip<PinLabels extends string = never> extends NormalComponent<
36
36
  }
37
37
 
38
38
  doInitialSchematicComponentRender() {
39
- const { db } = this.project!
39
+ const { db } = this.root!
40
40
  const { _parsedProps: props } = this
41
41
 
42
42
  const ports = this.children.filter((child) => child instanceof Port)
@@ -81,7 +81,7 @@ export class Chip<PinLabels extends string = never> extends NormalComponent<
81
81
  }
82
82
 
83
83
  doInitialPcbComponentRender() {
84
- const { db } = this.project!
84
+ const { db } = this.root!
85
85
  const { _parsedProps: props } = this
86
86
 
87
87
  const pcb_component = db.pcb_component.insert({
@@ -22,7 +22,7 @@ export class Jumper<PinLabels extends string = never> extends NormalComponent<
22
22
  }
23
23
 
24
24
  doInitialSourceRender(): void {
25
- const { db } = this.project!
25
+ const { db } = this.root!
26
26
  const { _parsedProps: props } = this
27
27
 
28
28
  const source_component = db.source_component.insert({
@@ -36,7 +36,7 @@ export class Jumper<PinLabels extends string = never> extends NormalComponent<
36
36
  }
37
37
 
38
38
  doInitialSchematicComponentRender() {
39
- const { db } = this.project!
39
+ const { db } = this.root!
40
40
  const { _parsedProps: props } = this
41
41
 
42
42
  const ports = this.children.filter((child) => child instanceof Port)
@@ -84,7 +84,7 @@ export class Jumper<PinLabels extends string = never> extends NormalComponent<
84
84
  }
85
85
 
86
86
  doInitialPcbComponentRender() {
87
- const { db } = this.project!
87
+ const { db } = this.root!
88
88
  const { _parsedProps: props } = this
89
89
 
90
90
  const pcb_component = db.pcb_component.insert({
@@ -43,7 +43,7 @@ export class Resistor extends NormalComponent<
43
43
  }
44
44
 
45
45
  doInitialSourceRender() {
46
- const { db } = this.project!
46
+ const { db } = this.root!
47
47
  const { _parsedProps: props } = this
48
48
  const source_component = db.source_component.insert({
49
49
  ftype: "simple_resistor",
@@ -0,0 +1,49 @@
1
+ import { constraintProps } from "@tscircuit/props"
2
+ import { z } from "zod"
3
+ import { PrimitiveComponent } from "../base-components/PrimitiveComponent"
4
+
5
+ export class Constraint extends PrimitiveComponent<typeof constraintProps> {
6
+ get config() {
7
+ return {
8
+ zodProps: constraintProps,
9
+ }
10
+ }
11
+
12
+ constructor(props: z.input<typeof constraintProps>) {
13
+ super(props)
14
+ if ("xdist" in props || "ydist" in props) {
15
+ if (!("edgeToEdge" in props) && !("centerToCenter" in props)) {
16
+ throw new Error(
17
+ "edgeToEdge, centerToCenter (or from*/to* props) must be set for xdist or ydist for <constraint />",
18
+ )
19
+ }
20
+ }
21
+ }
22
+
23
+ _getAllReferencedComponents(): {
24
+ componentsWithSelectors: Array<{
25
+ component: PrimitiveComponent<any>
26
+ selector: string
27
+ }>
28
+ } {
29
+ const componentsWithSelectors: Array<{
30
+ component: PrimitiveComponent<any>
31
+ selector: string
32
+ }> = []
33
+
34
+ for (const key of ["left", "right", "top", "bottom"]) {
35
+ if (key in this._parsedProps) {
36
+ const selector = (this._parsedProps as any)[key] as string
37
+ const component = this.getSubcircuit().selectOne(selector)
38
+ if (component) {
39
+ componentsWithSelectors.push({
40
+ selector,
41
+ component,
42
+ })
43
+ }
44
+ }
45
+ }
46
+
47
+ return { componentsWithSelectors }
48
+ }
49
+ }