@tscircuit/core 0.0.1
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/README.md +35 -0
- package/dist/index.cjs +5353 -0
- package/index.ts +1 -0
- package/lib/Project.ts +84 -0
- package/lib/components/base-components/NormalComponent.ts +330 -0
- package/lib/components/base-components/PrimitiveComponent.ts +299 -0
- package/lib/components/base-components/Renderable.ts +113 -0
- package/lib/components/index.ts +14 -0
- package/lib/components/normal-components/Board.ts +38 -0
- package/lib/components/normal-components/Capacitor.ts +23 -0
- package/lib/components/normal-components/Diode.ts +27 -0
- package/lib/components/normal-components/Led.ts +27 -0
- package/lib/components/normal-components/Resistor.ts +19 -0
- package/lib/components/primitive-components/Footprint.ts +4 -0
- package/lib/components/primitive-components/Group.ts +10 -0
- package/lib/components/primitive-components/Net.ts +12 -0
- package/lib/components/primitive-components/Port.ts +187 -0
- package/lib/components/primitive-components/SmtPad.ts +80 -0
- package/lib/components/primitive-components/Trace.ts +219 -0
- package/lib/components/primitive-components/TraceHint.ts +4 -0
- package/lib/fiber/catalogue.ts +22 -0
- package/lib/fiber/create-instance-from-react-element.ts +168 -0
- package/lib/fiber/intrinsic-jsx.ts +43 -0
- package/lib/index.ts +5 -0
- package/lib/register-catalogue.ts +3 -0
- package/lib/utils/autorouting/SimpleRouteJson.ts +37 -0
- package/lib/utils/autorouting/computeObstacleBounds.ts +10 -0
- package/lib/utils/constants.ts +35 -0
- package/lib/utils/createComponentsFromSoup.ts +86 -0
- package/lib/utils/get-relative-direction.ts +14 -0
- package/lib/utils/getPortFromHints.ts +10 -0
- package/lib/utils/projectPointInDirection.ts +18 -0
- package/lib/utils/selector-matching/index.ts +62 -0
- package/package.json +40 -0
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lib/index"
|
package/lib/Project.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AnySoupElement } from "@tscircuit/soup"
|
|
2
|
+
import type { PrimitiveComponent } from "./components/base-components/PrimitiveComponent"
|
|
3
|
+
import type { SoupUtilObjects } from "@tscircuit/soup-util"
|
|
4
|
+
import { su } from "@tscircuit/soup-util"
|
|
5
|
+
import { isValidElement, type ReactElement } from "react"
|
|
6
|
+
import { createInstanceFromReactElement } from "./fiber/create-instance-from-react-element"
|
|
7
|
+
import { identity, type Matrix } from "transformation-matrix"
|
|
8
|
+
|
|
9
|
+
export class Project {
|
|
10
|
+
rootComponent: PrimitiveComponent | null = null
|
|
11
|
+
children: PrimitiveComponent[]
|
|
12
|
+
db: SoupUtilObjects
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.children = []
|
|
16
|
+
this.db = su([])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
add(componentOrElm: PrimitiveComponent | ReactElement) {
|
|
20
|
+
let component: PrimitiveComponent
|
|
21
|
+
if (isValidElement(componentOrElm)) {
|
|
22
|
+
// TODO store subtree
|
|
23
|
+
component = createInstanceFromReactElement(componentOrElm)
|
|
24
|
+
} else {
|
|
25
|
+
component = componentOrElm as PrimitiveComponent
|
|
26
|
+
}
|
|
27
|
+
this.children.push(component)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_guessRootComponent() {
|
|
31
|
+
if (this.rootComponent) return
|
|
32
|
+
if (this.children.length === 1) {
|
|
33
|
+
this.rootComponent = this.children[0]
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (this.children.length === 0) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Not able to guess root component: Project has no children (use project.add(...))",
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this.children.length > 0) {
|
|
43
|
+
const board =
|
|
44
|
+
this.children.find((c) => c.componentName === "Board") ?? null
|
|
45
|
+
|
|
46
|
+
if (board) {
|
|
47
|
+
this.rootComponent = board
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Not able to guess root component: Project has multiple children and no board",
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render() {
|
|
57
|
+
if (!this.rootComponent) {
|
|
58
|
+
this._guessRootComponent()
|
|
59
|
+
}
|
|
60
|
+
const { rootComponent, db } = this
|
|
61
|
+
|
|
62
|
+
if (!rootComponent) throw new Error("Project has no root component")
|
|
63
|
+
|
|
64
|
+
rootComponent.setProject(this)
|
|
65
|
+
|
|
66
|
+
rootComponent.runRenderCycle()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getSoup(): AnySoupElement[] {
|
|
70
|
+
return this.db.toArray()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getCircuitJson(): AnySoupElement[] {
|
|
74
|
+
return this.getSoup()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
computeGlobalSchematicTransform(): Matrix {
|
|
78
|
+
return identity()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
computeGlobalPcbTransform(): Matrix {
|
|
82
|
+
return identity()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { Footprint } from "../primitive-components/Footprint"
|
|
2
|
+
import { ZodType, z } from "zod"
|
|
3
|
+
import { PrimitiveComponent } from "./PrimitiveComponent"
|
|
4
|
+
import { Port } from "../primitive-components/Port"
|
|
5
|
+
import { symbols, type BaseSymbolName, type SchSymbol } from "schematic-symbols"
|
|
6
|
+
import { fp } from "footprinter"
|
|
7
|
+
import {
|
|
8
|
+
isValidElement as isReactElement,
|
|
9
|
+
isValidElement,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react"
|
|
13
|
+
import {
|
|
14
|
+
createInstanceFromReactElement,
|
|
15
|
+
type ReactSubtree,
|
|
16
|
+
} from "lib/fiber/create-instance-from-react-element"
|
|
17
|
+
import { getPortFromHints } from "lib/utils/getPortFromHints"
|
|
18
|
+
import { createComponentsFromSoup } from "lib/utils/createComponentsFromSoup"
|
|
19
|
+
|
|
20
|
+
export type PortMap<T extends string> = {
|
|
21
|
+
[K in T]: Port
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A NormalComponent is the base class for most components that a user will
|
|
26
|
+
* interact with. It has the ability to set a footprint and discover ports.
|
|
27
|
+
*
|
|
28
|
+
* When you're extending a NormalComponent, you almost always want to override
|
|
29
|
+
* initPorts() to create ports for the component.
|
|
30
|
+
*
|
|
31
|
+
* class Led extends NormalComponent<typeof resistorProps> {
|
|
32
|
+
* pin1: Port = this.portMap.pin1
|
|
33
|
+
* pin2: Port = this.portMap.pin2
|
|
34
|
+
*
|
|
35
|
+
* initPorts() {
|
|
36
|
+
* this.add(new Port({ pinNumber: 1, aliases: ["anode", "pos"] }))
|
|
37
|
+
* this.add(new Port({ pinNumber: 2, aliases: ["cathode", "neg"] }))
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*/
|
|
41
|
+
export class NormalComponent<
|
|
42
|
+
ZodProps extends ZodType = any,
|
|
43
|
+
PortNames extends string = never,
|
|
44
|
+
> extends PrimitiveComponent<ZodProps> {
|
|
45
|
+
reactSubtrees: Array<ReactSubtree> = []
|
|
46
|
+
|
|
47
|
+
constructor(props: z.input<ZodProps>) {
|
|
48
|
+
super(props)
|
|
49
|
+
this._addChildrenFromStringFootprint()
|
|
50
|
+
this.initPorts()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Override this method for better control over the auto-discovery of ports.
|
|
55
|
+
*
|
|
56
|
+
* If you override this method just do something like:
|
|
57
|
+
* initPorts() {
|
|
58
|
+
* this.add(new Port({ pinNumber: 1, aliases: ["anode", "pos"] }))
|
|
59
|
+
* this.add(new Port({ pinNumber: 2, aliases: ["cathode", "neg"] }))
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* By default, we'll pull the ports from the first place we find them:
|
|
63
|
+
* 1. `config.schematicSymbolName`
|
|
64
|
+
* 2. `props.footprint`
|
|
65
|
+
*
|
|
66
|
+
*/
|
|
67
|
+
initPorts() {
|
|
68
|
+
const { config } = this
|
|
69
|
+
if (config.schematicSymbolName) {
|
|
70
|
+
const sym = symbols[
|
|
71
|
+
`${config.schematicSymbolName}_horz` as keyof typeof symbols
|
|
72
|
+
] as SchSymbol | undefined
|
|
73
|
+
if (!sym) return
|
|
74
|
+
|
|
75
|
+
for (const symPort of sym.ports) {
|
|
76
|
+
const port = getPortFromHints(symPort.labels)
|
|
77
|
+
|
|
78
|
+
if (port) {
|
|
79
|
+
port.schematicSymbolPortDef = symPort
|
|
80
|
+
this.add(port)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const portsFromFootprint = this.getPortsFromFootprint()
|
|
88
|
+
|
|
89
|
+
this.addAll(portsFromFootprint)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_addChildrenFromStringFootprint() {
|
|
93
|
+
const { footprint } = this.props
|
|
94
|
+
if (!footprint) return
|
|
95
|
+
if (typeof footprint === "string") {
|
|
96
|
+
const fpSoup = fp.string(footprint).soup()
|
|
97
|
+
const fpComponents = createComponentsFromSoup(fpSoup)
|
|
98
|
+
this.addAll(fpComponents)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get portMap(): PortMap<PortNames> {
|
|
103
|
+
return new Proxy(
|
|
104
|
+
{},
|
|
105
|
+
{
|
|
106
|
+
get: (target, prop): Port => {
|
|
107
|
+
const port = this.children.find(
|
|
108
|
+
(c) =>
|
|
109
|
+
c.componentName === "Port" &&
|
|
110
|
+
(c as Port).isMatchingNameOrAlias(prop as string),
|
|
111
|
+
)
|
|
112
|
+
if (!port) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`There was an issue finding the port "${prop.toString()}" inside of a ${this.componentName} component with name: "${this.props.name}". This is a bug in @tscircuit/core`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
return port as Port
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
) as any
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getInstanceForReactElement(element: ReactElement): NormalComponent | null {
|
|
124
|
+
for (const subtree of this.reactSubtrees) {
|
|
125
|
+
if (subtree.element === element) return subtree.component
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
doInitialSourceRender() {
|
|
131
|
+
const ftype = this.config.sourceFtype
|
|
132
|
+
if (!ftype) return
|
|
133
|
+
const { db } = this.project!
|
|
134
|
+
const { _parsedProps: props } = this
|
|
135
|
+
const source_component = db.source_component.insert({
|
|
136
|
+
ftype,
|
|
137
|
+
name: props.name,
|
|
138
|
+
manufacturer_part_number: props.manufacturerPartNumber ?? props.mfn,
|
|
139
|
+
supplier_part_numbers: props.supplierPartNumbers,
|
|
140
|
+
})
|
|
141
|
+
this.source_component_id = source_component.source_component_id
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Render the schematic component for this NormalComponent using the
|
|
146
|
+
* config.schematicSymbolName if it exists.
|
|
147
|
+
*
|
|
148
|
+
* You can override this method to do more complicated things.
|
|
149
|
+
*/
|
|
150
|
+
doInitialSchematicComponentRender() {
|
|
151
|
+
const { db } = this.project!
|
|
152
|
+
const { schematicSymbolName } = this.config
|
|
153
|
+
if (!schematicSymbolName) return
|
|
154
|
+
// TODO switch between horizontal and vertical based on schRotation
|
|
155
|
+
const symbol_name = `${this.config.schematicSymbolName}_horz`
|
|
156
|
+
|
|
157
|
+
const symbol = (symbols as any)[symbol_name] as SchSymbol | undefined
|
|
158
|
+
|
|
159
|
+
if (!symbol) {
|
|
160
|
+
throw new Error(`Could not find schematic-symbol "${symbol_name}"`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const schematic_component = db.schematic_component.insert({
|
|
164
|
+
center: { x: this.props.schX ?? 0, y: this.props.schY ?? 0 },
|
|
165
|
+
rotation: this.props.schRotation ?? 0,
|
|
166
|
+
size: symbol.size,
|
|
167
|
+
source_component_id: this.source_component_id!,
|
|
168
|
+
|
|
169
|
+
// @ts-ignore
|
|
170
|
+
symbol_name,
|
|
171
|
+
})
|
|
172
|
+
this.schematic_component_id = schematic_component.schematic_component_id
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
doInitialPcbComponentRender() {
|
|
176
|
+
const { db } = this.project!
|
|
177
|
+
const { _parsedProps: props } = this
|
|
178
|
+
const pcb_component = db.pcb_component.insert({
|
|
179
|
+
center: { x: this.props.pcbX ?? 0, y: this.props.pcbY ?? 0 },
|
|
180
|
+
// width/height are computed in the PcbAnalysis phase
|
|
181
|
+
width: 0,
|
|
182
|
+
height: 0,
|
|
183
|
+
layer: props.layer ?? "top",
|
|
184
|
+
rotation: props.rotation ?? 0,
|
|
185
|
+
source_component_id: this.source_component_id!,
|
|
186
|
+
})
|
|
187
|
+
this.pcb_component_id = pcb_component.pcb_component_id
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
doInitialPcbAnalysis(): void {
|
|
191
|
+
const { db } = this.project!
|
|
192
|
+
const { _parsedProps: props } = this
|
|
193
|
+
|
|
194
|
+
// TODO Examine children to compute width/height and pcbX/pcbY
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_renderReactSubtree(element: ReactElement): ReactSubtree {
|
|
198
|
+
return {
|
|
199
|
+
element,
|
|
200
|
+
component: createInstanceFromReactElement(element),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
doInitialReactSubtreesRender(): void {
|
|
205
|
+
if (isReactElement(this.props.footprint)) {
|
|
206
|
+
if (this.reactSubtrees.some((rs) => rs.element === this.props.footprint))
|
|
207
|
+
return
|
|
208
|
+
const subtree = this._renderReactSubtree(this.props.footprint)
|
|
209
|
+
this.reactSubtrees.push(subtree)
|
|
210
|
+
this.add(subtree.component)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
add(componentOrElm: PrimitiveComponent | ReactElement) {
|
|
215
|
+
let component: PrimitiveComponent
|
|
216
|
+
if (isReactElement(componentOrElm)) {
|
|
217
|
+
const subtree = this._renderReactSubtree(componentOrElm)
|
|
218
|
+
this.reactSubtrees.push(subtree)
|
|
219
|
+
component = subtree.component
|
|
220
|
+
} else {
|
|
221
|
+
component = componentOrElm as PrimitiveComponent
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
super.add(component)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getPortsFromFootprint(): Port[] {
|
|
228
|
+
let { footprint } = this.props
|
|
229
|
+
|
|
230
|
+
if (!footprint || isValidElement(footprint)) {
|
|
231
|
+
footprint = this.children.find((c) => c.componentName === "Footprint")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeof footprint === "string") {
|
|
235
|
+
const fpSoup = fp.string(footprint).soup()
|
|
236
|
+
|
|
237
|
+
const newPorts: Port[] = []
|
|
238
|
+
for (const elm of fpSoup) {
|
|
239
|
+
if ("port_hints" in elm && elm.port_hints) {
|
|
240
|
+
const newPort = getPortFromHints(elm.port_hints)
|
|
241
|
+
if (!newPort) continue
|
|
242
|
+
newPorts.push(newPort)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return newPorts
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
!isValidElement(footprint) &&
|
|
250
|
+
footprint &&
|
|
251
|
+
footprint.componentName === "Footprint"
|
|
252
|
+
) {
|
|
253
|
+
const fp = footprint as Footprint
|
|
254
|
+
|
|
255
|
+
const newPorts: Port[] = []
|
|
256
|
+
for (const fpChild of fp.children) {
|
|
257
|
+
const newPort = getPortFromHints(fpChild.props.portHints ?? [])
|
|
258
|
+
if (!newPort) continue
|
|
259
|
+
newPorts.push(newPort)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return newPorts
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Explore children for possible smtpads etc.
|
|
266
|
+
const newPorts: Port[] = []
|
|
267
|
+
if (!footprint) {
|
|
268
|
+
for (const child of this.children) {
|
|
269
|
+
if (child.props.portHints && child.isPcbPrimitive) {
|
|
270
|
+
const port = getPortFromHints(child.props.portHints)
|
|
271
|
+
if (port) newPorts.push(port)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return newPorts
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getPortsFromSchematicSymbol(): Port[] {
|
|
279
|
+
const { config } = this
|
|
280
|
+
if (!config.schematicSymbolName) return []
|
|
281
|
+
const symbol: SchSymbol = (symbols as any)[config.schematicSymbolName]
|
|
282
|
+
if (!symbol) return []
|
|
283
|
+
const newPorts: Port[] = []
|
|
284
|
+
for (const symbolPort of symbol.ports) {
|
|
285
|
+
const port = getPortFromHints(symbolPort.labels)
|
|
286
|
+
if (port) {
|
|
287
|
+
port.schematicSymbolPortDef = symbolPort
|
|
288
|
+
newPorts.push(port)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return newPorts
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Use data from our props to create ports for this component.
|
|
296
|
+
*
|
|
297
|
+
* Generally, this is done by looking at the schematic and the footprint,
|
|
298
|
+
* reading the pins, making sure there aren't duplicates.
|
|
299
|
+
*
|
|
300
|
+
* Can probably be removed in favor of initPorts()
|
|
301
|
+
*
|
|
302
|
+
*/
|
|
303
|
+
doInitialPortDiscovery(): void {
|
|
304
|
+
const newPorts = [
|
|
305
|
+
...this.getPortsFromFootprint(),
|
|
306
|
+
...this.getPortsFromSchematicSymbol(),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
const existingPorts = this.children.filter(
|
|
310
|
+
(c) => c.componentName === "Port",
|
|
311
|
+
) as Port[]
|
|
312
|
+
|
|
313
|
+
for (const newPort of newPorts) {
|
|
314
|
+
const existingPort = existingPorts.find((p) =>
|
|
315
|
+
p.isMatchingAnyOf(newPort.getNameAndAliases()),
|
|
316
|
+
)
|
|
317
|
+
if (existingPort) {
|
|
318
|
+
if (
|
|
319
|
+
!existingPort.schematicSymbolPortDef &&
|
|
320
|
+
newPort.schematicSymbolPortDef
|
|
321
|
+
) {
|
|
322
|
+
existingPort.schematicSymbolPortDef = newPort.schematicSymbolPortDef
|
|
323
|
+
}
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
existingPorts.push(newPort)
|
|
327
|
+
this.add(newPort)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { AnySoupElement, AnySourceComponent } from "@tscircuit/soup"
|
|
2
|
+
import type { Project } from "../../Project"
|
|
3
|
+
import type { ZodType } from "zod"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import { symbols, type SchSymbol, type BaseSymbolName } from "schematic-symbols"
|
|
6
|
+
import { isValidElement as isReactElement, type ReactElement } from "react"
|
|
7
|
+
import type { Port } from "../primitive-components/Port"
|
|
8
|
+
import { Renderable, type RenderPhase } from "./Renderable"
|
|
9
|
+
import {
|
|
10
|
+
applyToPoint,
|
|
11
|
+
compose,
|
|
12
|
+
identity,
|
|
13
|
+
translate,
|
|
14
|
+
type Matrix,
|
|
15
|
+
} from "transformation-matrix"
|
|
16
|
+
import { isMatchingSelector } from "lib/utils/selector-matching"
|
|
17
|
+
|
|
18
|
+
export interface BaseComponentConfig {
|
|
19
|
+
schematicSymbolName?: BaseSymbolName | null
|
|
20
|
+
zodProps: ZodType
|
|
21
|
+
sourceFtype?: AnySourceComponent["ftype"] | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A PrimitiveComponent (SmtPad, Port etc.) doesn't have the ability to contain
|
|
26
|
+
* React subtrees or explicit handling of the "footprint" prop. But otherwise
|
|
27
|
+
* has most of the features of a NormalComponent.
|
|
28
|
+
*/
|
|
29
|
+
export abstract class PrimitiveComponent<
|
|
30
|
+
ZodProps extends ZodType = any,
|
|
31
|
+
> extends Renderable {
|
|
32
|
+
parent: PrimitiveComponent | null = null
|
|
33
|
+
children: PrimitiveComponent[]
|
|
34
|
+
childrenPendingRemoval: PrimitiveComponent[]
|
|
35
|
+
|
|
36
|
+
get config(): BaseComponentConfig {
|
|
37
|
+
return {
|
|
38
|
+
zodProps: z.object({}).passthrough(),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
project: Project | null = null
|
|
43
|
+
props: z.input<ZodProps>
|
|
44
|
+
_parsedProps: z.infer<ZodProps>
|
|
45
|
+
|
|
46
|
+
componentName = ""
|
|
47
|
+
lowercaseComponentName = ""
|
|
48
|
+
|
|
49
|
+
source_group_id: string | null = null
|
|
50
|
+
source_component_id: string | null = null
|
|
51
|
+
schematic_component_id: string | null = null
|
|
52
|
+
pcb_component_id: string | null = null
|
|
53
|
+
cad_component_id: string | null = null
|
|
54
|
+
|
|
55
|
+
constructor(props: z.input<ZodProps>) {
|
|
56
|
+
super(props)
|
|
57
|
+
this.children = []
|
|
58
|
+
this.childrenPendingRemoval = []
|
|
59
|
+
this.props = props ?? {}
|
|
60
|
+
this._parsedProps = this.config.zodProps.parse(
|
|
61
|
+
props ?? {},
|
|
62
|
+
) as z.infer<ZodProps>
|
|
63
|
+
if (!this.componentName) {
|
|
64
|
+
this.componentName = this.constructor.name
|
|
65
|
+
this.lowercaseComponentName = this.componentName.toLowerCase()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setProject(project: Project) {
|
|
70
|
+
this.project = project
|
|
71
|
+
for (const c of this.children) {
|
|
72
|
+
c.setProject(project)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setProps(props: Partial<z.input<ZodProps>>) {
|
|
77
|
+
const newProps = this.config.zodProps.parse({
|
|
78
|
+
...this.props,
|
|
79
|
+
...props,
|
|
80
|
+
}) as z.infer<ZodProps>
|
|
81
|
+
const oldProps = this.props
|
|
82
|
+
this.props = newProps
|
|
83
|
+
this._parsedProps = this.config.zodProps.parse(props) as z.infer<ZodProps>
|
|
84
|
+
this.onPropsChange({
|
|
85
|
+
oldProps,
|
|
86
|
+
newProps,
|
|
87
|
+
changedProps: Object.keys(props),
|
|
88
|
+
})
|
|
89
|
+
this.parent?.onChildChanged(this)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Computes a transformation matrix from the props of this component for PCB
|
|
94
|
+
* components
|
|
95
|
+
*/
|
|
96
|
+
computePcbPropsTransform(): Matrix {
|
|
97
|
+
// TODO rotations
|
|
98
|
+
return compose(translate(this.props.pcbX, this.props.pcbY))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Compute a transformation matrix combining all parent transforms for PCB
|
|
103
|
+
* components
|
|
104
|
+
*/
|
|
105
|
+
computePcbGlobalTransform(): Matrix {
|
|
106
|
+
return compose(
|
|
107
|
+
this.parent?.computePcbGlobalTransform() ?? identity(),
|
|
108
|
+
this.computePcbPropsTransform(),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Computes a transformation matrix from the props of this component for
|
|
114
|
+
* schematic components
|
|
115
|
+
*/
|
|
116
|
+
computeSchematicPropsTransform(): Matrix {
|
|
117
|
+
return compose(translate(this.props.schX ?? 0, this.props.schY ?? 0))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compute a transformation matrix combining all parent transforms for this
|
|
122
|
+
* component
|
|
123
|
+
*/
|
|
124
|
+
computeSchematicGlobalTransform(): Matrix {
|
|
125
|
+
return compose(
|
|
126
|
+
this.parent?.computeSchematicGlobalTransform?.() ?? identity(),
|
|
127
|
+
this.computeSchematicPropsTransform(),
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getSchematicSymbol(variant: "horz" | "vert" | null = null): SchSymbol | null {
|
|
132
|
+
if (variant === null) {
|
|
133
|
+
return this.getSchematicSymbol(
|
|
134
|
+
this.props.schRotation % 90 === 0 ? "vert" : "horz",
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
const { config } = this
|
|
138
|
+
if (!config.schematicSymbolName) return null
|
|
139
|
+
return symbols[
|
|
140
|
+
`${config.schematicSymbolName}_${variant}` as keyof typeof symbols
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getGlobalPcbPosition(): { x: number; y: number } {
|
|
145
|
+
return applyToPoint(this.computePcbGlobalTransform(), { x: 0, y: 0 })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getGlobalSchematicPosition(): { x: number; y: number } {
|
|
149
|
+
return applyToPoint(this.computeSchematicGlobalTransform(), { x: 0, y: 0 })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onAddToParent(parent: PrimitiveComponent) {
|
|
153
|
+
this.parent = parent
|
|
154
|
+
this.project = parent.project
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Called whenever the props change
|
|
159
|
+
*/
|
|
160
|
+
onPropsChange(params: {
|
|
161
|
+
oldProps: z.infer<ZodProps>
|
|
162
|
+
newProps: z.infer<ZodProps>
|
|
163
|
+
changedProps: string[]
|
|
164
|
+
}) {}
|
|
165
|
+
|
|
166
|
+
onChildChanged(child: PrimitiveComponent) {
|
|
167
|
+
this.parent?.onChildChanged(child)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
add(component: PrimitiveComponent) {
|
|
171
|
+
component.onAddToParent(this)
|
|
172
|
+
component.parent = this
|
|
173
|
+
this.children.push(component)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
addAll(components: PrimitiveComponent[]) {
|
|
177
|
+
for (const component of components) {
|
|
178
|
+
this.add(component)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
remove(component: PrimitiveComponent) {
|
|
183
|
+
this.children = this.children.filter((c) => c !== component)
|
|
184
|
+
this.childrenPendingRemoval.push(component)
|
|
185
|
+
component.shouldBeRemoved = true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getNameAndAliases(): string[] {
|
|
189
|
+
return [
|
|
190
|
+
this._parsedProps.name,
|
|
191
|
+
...(this._parsedProps.portHints ?? []),
|
|
192
|
+
].filter(Boolean)
|
|
193
|
+
}
|
|
194
|
+
isMatchingNameOrAlias(name: string) {
|
|
195
|
+
return this.getNameAndAliases().includes(name)
|
|
196
|
+
}
|
|
197
|
+
isMatchingAnyOf(aliases: Array<string | number>) {
|
|
198
|
+
return this.getNameAndAliases().some((a) =>
|
|
199
|
+
aliases.map((a) => a.toString()).includes(a),
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
doesSelectorMatch(selector: string): boolean {
|
|
204
|
+
const myTypeNames = [this.componentName, this.lowercaseComponentName]
|
|
205
|
+
const myClassNames = [this._parsedProps.name].filter(Boolean)
|
|
206
|
+
|
|
207
|
+
const parts = selector.trim().split(/\> /)[0]
|
|
208
|
+
const firstPart = parts[0]
|
|
209
|
+
|
|
210
|
+
if (parts.length > 1) return false
|
|
211
|
+
if (selector === "*") return true
|
|
212
|
+
if (selector[0] === "#" && selector.slice(1) === this.props.id) return true
|
|
213
|
+
if (selector[0] === "." && myClassNames.includes(selector.slice(1)))
|
|
214
|
+
return true
|
|
215
|
+
if (/^[a-zA-Z0-9_]/.test(firstPart) && myTypeNames.includes(firstPart))
|
|
216
|
+
return true
|
|
217
|
+
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
selectAll(selector: string): PrimitiveComponent[] {
|
|
222
|
+
const parts = selector.trim().split(/\s+/)
|
|
223
|
+
let results: PrimitiveComponent[] = [this]
|
|
224
|
+
|
|
225
|
+
let onlyDirectChildren = false
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (part === ">") {
|
|
228
|
+
onlyDirectChildren = true
|
|
229
|
+
} else {
|
|
230
|
+
results = results.flatMap((component) => {
|
|
231
|
+
return (
|
|
232
|
+
onlyDirectChildren ? component.children : component.getDescendants()
|
|
233
|
+
).filter((descendant) => isMatchingSelector(descendant, part))
|
|
234
|
+
})
|
|
235
|
+
onlyDirectChildren = false
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return results.filter((component) => component !== this)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
selectOne(
|
|
243
|
+
selector: string,
|
|
244
|
+
options?: {
|
|
245
|
+
type?: string
|
|
246
|
+
port?: boolean
|
|
247
|
+
pcbPrimitive?: boolean
|
|
248
|
+
schematicPrimitive?: boolean
|
|
249
|
+
},
|
|
250
|
+
): PrimitiveComponent | null {
|
|
251
|
+
let type = options?.type?.toLowerCase()
|
|
252
|
+
if (options?.port) type = "port"
|
|
253
|
+
if (type) {
|
|
254
|
+
return (
|
|
255
|
+
this.selectAll(selector).find(
|
|
256
|
+
(c) => c.lowercaseComponentName === type,
|
|
257
|
+
) ?? null
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
if (options?.pcbPrimitive) {
|
|
261
|
+
return this.selectAll(selector).find((c) => c.isPcbPrimitive) ?? null
|
|
262
|
+
}
|
|
263
|
+
if (options?.schematicPrimitive) {
|
|
264
|
+
return (
|
|
265
|
+
this.selectAll(selector).find((c) => c.isSchematicPrimitive) ?? null
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
return this.selectAll(selector)[0] ?? null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getDescendants(): PrimitiveComponent[] {
|
|
272
|
+
const descendants: PrimitiveComponent[] = []
|
|
273
|
+
for (const child of this.children) {
|
|
274
|
+
descendants.push(child)
|
|
275
|
+
descendants.push(...child.getDescendants())
|
|
276
|
+
}
|
|
277
|
+
return descendants
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getString(): string {
|
|
281
|
+
const { lowercaseComponentName: cname, _parsedProps: props, parent } = this
|
|
282
|
+
if (parent?.props?.name && props?.name) {
|
|
283
|
+
return `<${cname}#${this._renderId}(.${parent?.props.name}>.${props?.name}) />`
|
|
284
|
+
}
|
|
285
|
+
if (props?.name) {
|
|
286
|
+
return `<${cname}#${this._renderId} name=".${props?.name}" />`
|
|
287
|
+
}
|
|
288
|
+
if (props?.portHints) {
|
|
289
|
+
return `<${cname}#${this._renderId}(${props.portHints.map((ph: string) => `.${ph}`).join(", ")}) />`
|
|
290
|
+
}
|
|
291
|
+
return `<${cname}#${this._renderId} />`
|
|
292
|
+
}
|
|
293
|
+
get [Symbol.toStringTag](): string {
|
|
294
|
+
return this.getString()
|
|
295
|
+
}
|
|
296
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
297
|
+
return this.getString()
|
|
298
|
+
}
|
|
299
|
+
}
|