@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.
@@ -1,4 +1,161 @@
1
1
  import type { footprintProps } from "@tscircuit/props"
2
2
  import { PrimitiveComponent } from "../base-components/PrimitiveComponent"
3
+ import type { Constraint } from "./Constraint"
4
+ import * as kiwi from "@lume/kiwi"
5
+ import Debug from "debug"
3
6
 
4
- export class Footprint extends PrimitiveComponent<typeof footprintProps> {}
7
+ const debug = Debug("tscircuit:core:footprint")
8
+
9
+ export class Footprint extends PrimitiveComponent<typeof footprintProps> {
10
+ /**
11
+ * A footprint is a constrainedlayout, the db elements are adjusted according
12
+ * to any constraints that are defined.
13
+ */
14
+ doInitialPcbFootprintLayout() {
15
+ const constraints = this.children.filter(
16
+ (child) => child.componentName === "Constraint",
17
+ ) as Constraint[]
18
+
19
+ if (constraints.length === 0) return
20
+
21
+ const involvedComponents = constraints
22
+ .flatMap(
23
+ (constraint) =>
24
+ constraint._getAllReferencedComponents().componentsWithSelectors,
25
+ )
26
+ .map(({ component, selector }) => ({
27
+ component,
28
+ selector,
29
+ bounds: component._getCircuitJsonBounds(),
30
+ }))
31
+
32
+ function getComponentDetails(selector: string) {
33
+ return involvedComponents.find(({ selector: s }) => s === selector)
34
+ }
35
+
36
+ const solver = new kiwi.Solver()
37
+
38
+ const kVars: { [varName: string]: kiwi.Variable } = {}
39
+ function getKVar(name: string) {
40
+ if (!(name in kVars)) {
41
+ kVars[name] = new kiwi.Variable(name)
42
+ }
43
+ return kVars[name]
44
+ }
45
+
46
+ // 1. Create kiwi variables to represent each component's center
47
+ for (const { selector, bounds } of involvedComponents) {
48
+ const kvx = getKVar(`${selector}_x`)
49
+ const kvy = getKVar(`${selector}_y`)
50
+ solver.addEditVariable(kvx, kiwi.Strength.weak)
51
+ solver.addEditVariable(kvy, kiwi.Strength.weak)
52
+ solver.suggestValue(kvx, bounds.center.x)
53
+ solver.suggestValue(kvy, bounds.center.y)
54
+ }
55
+
56
+ // 2. Add kiwi constraints using the parsed constraint properties
57
+ for (const constraint of constraints) {
58
+ const props = constraint._parsedProps
59
+
60
+ if ("xdist" in props) {
61
+ const { xdist, left, right, edgeToEdge, centerToCenter } = props
62
+ const leftVar = getKVar(`${left}_x`)
63
+ const rightVar = getKVar(`${right}_x`)
64
+ const leftBounds = getComponentDetails(left)?.bounds!
65
+ const rightBounds = getComponentDetails(right)?.bounds!
66
+
67
+ if (centerToCenter) {
68
+ // right - left = xdist
69
+ const expr = new kiwi.Expression(rightVar, [-1, leftVar])
70
+ solver.addConstraint(
71
+ new kiwi.Constraint(
72
+ expr,
73
+ kiwi.Operator.Eq,
74
+ props.xdist,
75
+ kiwi.Strength.required,
76
+ ),
77
+ )
78
+ } else if (edgeToEdge) {
79
+ // rightEdge - leftEdge = xdist
80
+ // right + rightBounds.width/2 - left - leftBounds.width/2 = xdist
81
+ const expr = new kiwi.Expression(
82
+ rightVar,
83
+ rightBounds.width / 2,
84
+ [-1, leftVar],
85
+ -leftBounds.width / 2,
86
+ )
87
+ solver.addConstraint(
88
+ new kiwi.Constraint(
89
+ expr,
90
+ kiwi.Operator.Eq,
91
+ props.xdist,
92
+ kiwi.Strength.required,
93
+ ),
94
+ )
95
+ }
96
+ }
97
+
98
+ // 3. Solve the system of equations
99
+ solver.updateVariables()
100
+ if (debug.enabled) {
101
+ console.log("Solution to layout constraints:")
102
+ console.table(
103
+ Object.entries(kVars).map(([key, kvar]) => ({
104
+ var: key,
105
+ val: kvar.value(),
106
+ })),
107
+ )
108
+ }
109
+
110
+ // 3.1 Compute the global offset. There are different ways to do this:
111
+ // - If any component has a fixed position, then that can be used as the
112
+ // origin to determine the offset of all other components
113
+ // - If no component has a fixed position, then we recenter everything
114
+ // using the new bounds of all the involved components
115
+
116
+ // TODO determine if there's a fixed component
117
+
118
+ // Determine the new bounds all the involved components and compute the
119
+ // bounds of this footprint
120
+ const bounds = {
121
+ left: Infinity,
122
+ right: -Infinity,
123
+ top: -Infinity,
124
+ bottom: Infinity,
125
+ }
126
+ for (const {
127
+ selector,
128
+ bounds: { width, height },
129
+ } of involvedComponents) {
130
+ const kvx = getKVar(`${selector}_x`)
131
+ const kvy = getKVar(`${selector}_y`)
132
+
133
+ const newLeft = kvx.value() - width / 2
134
+ const newRight = kvx.value() + width / 2
135
+ const newTop = kvy.value() + height / 2
136
+ const newBottom = kvy.value() - height / 2
137
+
138
+ bounds.left = Math.min(bounds.left, newLeft)
139
+ bounds.right = Math.max(bounds.right, newRight)
140
+ bounds.top = Math.max(bounds.top, newTop)
141
+ bounds.bottom = Math.min(bounds.bottom, newBottom)
142
+ }
143
+
144
+ // Compute the global offset, we can use this to recenter each component
145
+ const globalOffset = {
146
+ x: -(bounds.right + bounds.left) / 2,
147
+ y: -(bounds.top + bounds.bottom) / 2,
148
+ }
149
+
150
+ // 4. Update the component positions
151
+ for (const { component, selector } of involvedComponents) {
152
+ const kvx = getKVar(`${selector}_x`)
153
+ const kvy = getKVar(`${selector}_y`)
154
+ component._setPositionFromLayout({
155
+ x: kvx.value() + globalOffset.x,
156
+ y: kvy.value() + globalOffset.y,
157
+ })
158
+ }
159
+ }
160
+ }
161
+ }
@@ -18,7 +18,7 @@ export class Net extends PrimitiveComponent<typeof netProps> {
18
18
  }
19
19
 
20
20
  doInitialSourceComponentRender(): void {
21
- const { db } = this.project!
21
+ const { db } = this.root!
22
22
  const { _parsedProps: props } = this
23
23
 
24
24
  const net = db.source_net.insert({
@@ -77,7 +77,7 @@ export class Net extends PrimitiveComponent<typeof netProps> {
77
77
  * such that the nets are fully connected
78
78
  */
79
79
  doInitialPcbRouteNetIslands(): void {
80
- const { db } = this.project!
80
+ const { db } = this.root!
81
81
  const { _parsedProps: props } = this
82
82
 
83
83
  const traces = this._getAllDirectlyConnectedTraces().filter(
@@ -109,10 +109,10 @@ export class Net extends PrimitiveComponent<typeof netProps> {
109
109
  for (const [A, B] of islandPairs) {
110
110
  // Find two closest ports on the island
111
111
  const Apositions: Array<{ x: number; y: number }> = A.ports.map((port) =>
112
- port.getGlobalPcbPosition(),
112
+ port._getGlobalPcbPositionBeforeLayout(),
113
113
  )
114
114
  const Bpositions: Array<{ x: number; y: number }> = B.ports.map((port) =>
115
- port.getGlobalPcbPosition(),
115
+ port._getGlobalPcbPositionBeforeLayout(),
116
116
  )
117
117
 
118
118
  let closestDist = Infinity
@@ -46,10 +46,10 @@ export class PlatedHole extends PrimitiveComponent<typeof platedHoleProps> {
46
46
  }
47
47
 
48
48
  doInitialPcbPrimitiveRender(): void {
49
- const { db } = this.project!
49
+ const { db } = this.root!
50
50
  const { _parsedProps: props } = this
51
51
  if (!props.portHints) return
52
- const position = this.getGlobalPcbPosition()
52
+ const position = this._getGlobalPcbPositionBeforeLayout()
53
53
  if (props.shape === "circle") {
54
54
  const plated_hole_input: PCBPlatedHoleInput = {
55
55
  pcb_component_id: this.parent?.pcb_component_id!,
@@ -32,7 +32,7 @@ export class Port extends PrimitiveComponent<typeof portProps> {
32
32
  this.matchedComponents = []
33
33
  }
34
34
 
35
- getGlobalPcbPosition(): { x: number; y: number } {
35
+ _getGlobalPcbPositionBeforeLayout(): { x: number; y: number } {
36
36
  const matchedPcbElm = this.matchedComponents.find((c) => c.isPcbPrimitive)
37
37
 
38
38
  if (!matchedPcbElm) {
@@ -41,10 +41,10 @@ export class Port extends PrimitiveComponent<typeof portProps> {
41
41
  )
42
42
  }
43
43
 
44
- return matchedPcbElm?.getGlobalPcbPosition() ?? { x: 0, y: 0 }
44
+ return matchedPcbElm?._getGlobalPcbPositionBeforeLayout() ?? { x: 0, y: 0 }
45
45
  }
46
46
 
47
- getGlobalSchematicPosition(): { x: number; y: number } {
47
+ _getGlobalSchematicPositionBeforeLayout(): { x: number; y: number } {
48
48
  if (!this.schematicSymbolPortDef) {
49
49
  return applyToPoint(this.parent!.computeSchematicGlobalTransform(), {
50
50
  x: 0,
@@ -116,7 +116,7 @@ export class Port extends PrimitiveComponent<typeof portProps> {
116
116
  }
117
117
 
118
118
  doInitialSourceRender(): void {
119
- const { db } = this.project!
119
+ const { db } = this.root!
120
120
  const { _parsedProps: props } = this
121
121
 
122
122
  const port_hints = this.getNameAndAliases()
@@ -132,7 +132,7 @@ export class Port extends PrimitiveComponent<typeof portProps> {
132
132
  }
133
133
 
134
134
  doInitialSourceParentAttachment(): void {
135
- const { db } = this.project!
135
+ const { db } = this.root!
136
136
  if (!this.parent?.source_component_id) {
137
137
  throw new Error(
138
138
  `${this.getString()} has no parent source component (parent: ${this.parent?.getString()})`,
@@ -146,13 +146,8 @@ export class Port extends PrimitiveComponent<typeof portProps> {
146
146
  this.source_component_id = this.parent?.source_component_id
147
147
  }
148
148
 
149
- /**
150
- * For PcbPorts, we use the parent attachment phase to determine where to place
151
- * the pcb_port (prior to this phase, the smtpad/platedhole isn't guaranteed
152
- * to exist)
153
- */
154
149
  doInitialPcbPortRender(): void {
155
- const { db } = this.project!
150
+ const { db } = this.root!
156
151
  const { matchedComponents } = this
157
152
 
158
153
  if (!this.parent?.pcb_component_id) {
@@ -173,31 +168,31 @@ export class Port extends PrimitiveComponent<typeof portProps> {
173
168
 
174
169
  const pcbMatch: any = pcbMatches[0]
175
170
 
176
- if ("getGlobalPcbPosition" in pcbMatch) {
171
+ if ("_getCircuitJsonBounds" in pcbMatch) {
177
172
  const pcb_port = db.pcb_port.insert({
178
173
  pcb_component_id: this.parent?.pcb_component_id!,
179
174
  layers: ["top"],
180
175
 
181
- ...pcbMatch.getGlobalPcbPosition(),
176
+ ...pcbMatch._getCircuitJsonBounds().center,
182
177
 
183
178
  source_port_id: this.source_port_id!,
184
179
  })
185
180
  this.pcb_port_id = pcb_port.pcb_port_id
186
181
  } else {
187
182
  throw new Error(
188
- `${pcbMatch.getString()} does not have a getGlobalPcbPosition method (needed for pcb_port placement)`,
183
+ `${pcbMatch.getString()} does not have a _getGlobalPcbPositionBeforeLayout method (needed for pcb_port placement)`,
189
184
  )
190
185
  }
191
186
  }
192
187
 
193
188
  doInitialSchematicPortRender(): void {
194
- const { db } = this.project!
189
+ const { db } = this.root!
195
190
  const { _parsedProps: props } = this
196
191
 
197
192
  if (!this.parent) return
198
193
 
199
- const center = this.getGlobalSchematicPosition()
200
- const parentCenter = this.parent?.getGlobalSchematicPosition()
194
+ const center = this._getGlobalSchematicPositionBeforeLayout()
195
+ const parentCenter = this.parent?._getGlobalSchematicPositionBeforeLayout()
201
196
 
202
197
  this.facingDirection = getRelativeDirection(parentCenter, center)
203
198
 
@@ -14,7 +14,7 @@ export class SilkscreenPath extends PrimitiveComponent<
14
14
  }
15
15
 
16
16
  doInitialPcbPrimitiveRender(): void {
17
- const { db } = this.project!
17
+ const { db } = this.root!
18
18
  const { _parsedProps: props } = this
19
19
 
20
20
  const layer = props.layer ?? "top"
@@ -24,7 +24,7 @@ export class SilkscreenPath extends PrimitiveComponent<
24
24
  )
25
25
  }
26
26
 
27
- const transform = this.computePcbGlobalTransform()
27
+ const transform = this._computePcbGlobalTransformBeforeLayout()
28
28
 
29
29
  const pcb_silkscreen_path = db.pcb_silkscreen_path.insert({
30
30
  pcb_component_id: this.parent?.pcb_component_id!,
@@ -32,8 +32,8 @@ export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
32
32
  }
33
33
 
34
34
  doInitialPortMatching(): void {
35
- const parentPorts = (this.parent?.children ?? []).filter(
36
- (c) => c.componentName === "Port",
35
+ const parentPorts = this.getPrimitiveContainer()?.selectAll(
36
+ "port",
37
37
  ) as Port[]
38
38
 
39
39
  if (!this.props.portHints) {
@@ -50,11 +50,13 @@ export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
50
50
  }
51
51
 
52
52
  doInitialPcbPrimitiveRender(): void {
53
- const { db } = this.project!
53
+ const { db } = this.root!
54
54
  const { _parsedProps: props } = this
55
55
  if (!props.portHints) return
56
- const position = this.getGlobalPcbPosition()
57
- const decomposedMat = decomposeTSR(this.computePcbGlobalTransform())
56
+ const position = this._getGlobalPcbPositionBeforeLayout()
57
+ const decomposedMat = decomposeTSR(
58
+ this._computePcbGlobalTransformBeforeLayout(),
59
+ )
58
60
  const isRotated90 =
59
61
  Math.abs(decomposedMat.rotation.angle * (180 / Math.PI) - 90) < 0.01
60
62
  let pcb_smtpad: PCBSMTPad | null = null
@@ -94,4 +96,52 @@ export class SmtPad extends PrimitiveComponent<typeof smtPadProps> {
94
96
  this.pcb_smtpad_id = pcb_smtpad.pcb_smtpad_id
95
97
  }
96
98
  }
99
+
100
+ _getCircuitJsonBounds(): {
101
+ center: { x: number; y: number }
102
+ bounds: { left: number; top: number; right: number; bottom: number }
103
+ width: number
104
+ height: number
105
+ } {
106
+ const { db } = this.root!
107
+ const smtpad = db.pcb_smtpad.get(this.pcb_smtpad_id!)!
108
+
109
+ if (smtpad.shape === "rect") {
110
+ return {
111
+ center: { x: smtpad.x, y: smtpad.y },
112
+ bounds: {
113
+ left: smtpad.x - smtpad.width / 2,
114
+ top: smtpad.y - smtpad.height / 2,
115
+ right: smtpad.x + smtpad.width / 2,
116
+ bottom: smtpad.y + smtpad.height / 2,
117
+ },
118
+ width: smtpad.width,
119
+ height: smtpad.height,
120
+ }
121
+ }
122
+ if (smtpad.shape === "circle") {
123
+ return {
124
+ center: { x: smtpad.x, y: smtpad.y },
125
+ bounds: {
126
+ left: smtpad.x - smtpad.radius,
127
+ top: smtpad.y - smtpad.radius,
128
+ right: smtpad.x + smtpad.radius,
129
+ bottom: smtpad.y + smtpad.radius,
130
+ },
131
+ width: smtpad.radius * 2,
132
+ height: smtpad.radius * 2,
133
+ }
134
+ }
135
+ throw new Error(
136
+ `circuitJson bounds calculation not implemented for shape "${(smtpad as any).shape}"`,
137
+ )
138
+ }
139
+
140
+ _setPositionFromLayout(newCenter: { x: number; y: number }) {
141
+ const { db } = this.root!
142
+ db.pcb_smtpad.update(this.pcb_smtpad_id!, {
143
+ x: newCenter.x,
144
+ y: newCenter.y,
145
+ })
146
+ }
97
147
  }
@@ -38,7 +38,7 @@ type PcbRouteObjective =
38
38
  | { layers: string[]; x: number; y: number; via?: boolean }
39
39
 
40
40
  const portToObjective = (port: Port): PcbRouteObjective => {
41
- const portPosition = port.getGlobalPcbPosition()
41
+ const portPosition = port._getGlobalPcbPositionBeforeLayout()
42
42
  return {
43
43
  ...portPosition,
44
44
  layers: port.getAvailablePcbLayers(),
@@ -104,7 +104,7 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
104
104
  ports?: undefined
105
105
  portsWithSelectors?: undefined
106
106
  } {
107
- const { db } = this.project!
107
+ const { db } = this.root!
108
108
  const { _parsedProps: props, parent } = this
109
109
 
110
110
  if (!parent) throw new Error("Trace has no parent")
@@ -215,7 +215,7 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
215
215
  }
216
216
 
217
217
  doInitialSourceTraceRender(): void {
218
- const { db } = this.project!
218
+ const { db } = this.root!
219
219
  const { _parsedProps: props, parent } = this
220
220
 
221
221
  if (!parent) {
@@ -238,7 +238,7 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
238
238
  }
239
239
 
240
240
  doInitialPcbTraceRender(): void {
241
- const { db } = this.project!
241
+ const { db } = this.root!
242
242
  const { _parsedProps: props, parent } = this
243
243
 
244
244
  if (!parent) throw new Error("Trace has no parent")
@@ -365,7 +365,7 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
365
365
 
366
366
  // Cache the PCB obstacles, they'll be needed for each segment between
367
367
  // ports/hints
368
- const obstacles = getObstaclesFromSoup(this.project!.db.toArray())
368
+ const obstacles = getObstaclesFromSoup(this.root!.db.toArray())
369
369
  markObstaclesAsConnected(
370
370
  obstacles,
371
371
  orderedRouteObjectives,
@@ -445,7 +445,7 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
445
445
  }
446
446
 
447
447
  doInitialSchematicTraceRender(): void {
448
- const { db } = this.project!
448
+ const { db } = this.root!
449
449
  const { _parsedProps: props, parent } = this
450
450
 
451
451
  if (!parent) throw new Error("Trace has no parent")
@@ -476,7 +476,7 @@ export class Trace extends PrimitiveComponent<typeof traceProps> {
476
476
  for (const { port } of ports) {
477
477
  connection.pointsToConnect.push(
478
478
  projectPointInDirection(
479
- port.getGlobalSchematicPosition(),
479
+ port._getGlobalSchematicPositionBeforeLayout(),
480
480
  port.facingDirection!,
481
481
  0.1501,
482
482
  ),
@@ -8,7 +8,7 @@ export class TraceHint extends PrimitiveComponent<typeof traceHintProps> {
8
8
  matchedPort: Port | null = null
9
9
 
10
10
  doInitialPortMatching(): void {
11
- const { db } = this.project!
11
+ const { db } = this.root!
12
12
  const { _parsedProps: props, parent } = this
13
13
 
14
14
  if (!parent) return
@@ -46,7 +46,7 @@ export class TraceHint extends PrimitiveComponent<typeof traceHintProps> {
46
46
 
47
47
  if (!offsets) return []
48
48
 
49
- const globalTransform = this.computePcbGlobalTransform()
49
+ const globalTransform = this._computePcbGlobalTransformBeforeLayout()
50
50
 
51
51
  return offsets.map(
52
52
  (offset): RouteHintPoint => ({
@@ -39,6 +39,8 @@ declare global {
39
39
  pcbtrace: Props.PcbTraceProps
40
40
  fabricationnotetext: Props.FabricationNoteTextProps
41
41
  fabricationnotepath: Props.FabricationNotePathProps
42
+ constraint: Props.ConstraintProps
43
+ constrainedlayout: Props.ConstrainedLayoutProps
42
44
  jscad: any
43
45
  }
44
46
  }
@@ -1,10 +1,16 @@
1
1
  export type PointLike =
2
- | { getGlobalPcbPosition: () => { x: number; y: number } }
2
+ | { _getGlobalPcbPositionBeforeLayout: () => { x: number; y: number } }
3
3
  | { x: number; y: number }
4
4
 
5
5
  const getDistance = (a: PointLike, b: PointLike) => {
6
- const aPos = "getGlobalPcbPosition" in a ? a.getGlobalPcbPosition() : a
7
- const bPos = "getGlobalPcbPosition" in b ? b.getGlobalPcbPosition() : b
6
+ const aPos =
7
+ "_getGlobalPcbPositionBeforeLayout" in a
8
+ ? a._getGlobalPcbPositionBeforeLayout()
9
+ : a
10
+ const bPos =
11
+ "_getGlobalPcbPositionBeforeLayout" in b
12
+ ? b._getGlobalPcbPositionBeforeLayout()
13
+ : b
8
14
  return Math.sqrt((aPos.x - bPos.x) ** 2 + (aPos.y - bPos.y) ** 2)
9
15
  }
10
16
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@tscircuit/core",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.0.29",
5
+ "version": "0.0.31",
6
6
  "types": "dist/index.d.ts",
7
7
  "main": "dist/index.js",
8
8
  "files": [
@@ -19,10 +19,12 @@
19
19
  "@tscircuit/layout": "^0.0.28",
20
20
  "@tscircuit/log-soup": "^1.0.2",
21
21
  "@types/bun": "latest",
22
+ "@types/debug": "^4.1.12",
22
23
  "@types/react": "^18.3.3",
23
24
  "@types/react-reconciler": "^0.28.8",
24
25
  "bun-match-svg": "0.0.2",
25
26
  "circuit-to-svg": "^0.0.18",
27
+ "debug": "^4.3.6",
26
28
  "howfat": "^0.3.8",
27
29
  "looks-same": "^9.0.1",
28
30
  "tsup": "^8.2.4"
@@ -31,8 +33,9 @@
31
33
  "typescript": "^5.0.0"
32
34
  },
33
35
  "dependencies": {
36
+ "@lume/kiwi": "^0.4.3",
34
37
  "@tscircuit/infgrid-ijump-astar": "^0.0.6",
35
- "@tscircuit/props": "^0.0.51",
38
+ "@tscircuit/props": "^0.0.58",
36
39
  "@tscircuit/soup": "^0.0.58",
37
40
  "@tscircuit/soup-util": "0.0.18",
38
41
  "footprinter": "^0.0.44",