Skip to content

Commit

Permalink
Merge pull request #104 from tscircuit/bigrefac
Browse files Browse the repository at this point in the history
Major Refactor to All Schematic Objects - Use Absolute Coordinates, Breakup Functions, Fix Offsets
  • Loading branch information
seveibar authored Nov 5, 2024
2 parents 5d8dd57 + 88cc8ab commit 7e08ae8
Show file tree
Hide file tree
Showing 23 changed files with 939 additions and 498 deletions.
Binary file modified bun.lockb
Binary file not shown.
85 changes: 53 additions & 32 deletions lib/sch/convert-circuit-json-to-schematic-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
compose,
scale,
translate,
fromTriangles,
type Matrix,
fromTwoMovingPoints,
} from "transformation-matrix"
import { drawSchematicGrid } from "./draw-schematic-grid"
import { drawSchematicLabeledPoints } from "./draw-schematic-labeled-points"
import { getSchematicBoundsFromCircuitJson } from "./get-schematic-bounds-from-circuit-json"
import { createSchematicComponent } from "./svg-object-fns/create-svg-objects-from-sch-component"
import { createSvgObjectsFromSchematicComponent } from "./svg-object-fns/create-svg-objects-from-sch-component"
import { createSvgObjectsFromSchDebugObject } from "./svg-object-fns/create-svg-objects-from-sch-debug-object"
import { createSchematicTrace } from "./svg-object-fns/create-svg-objects-from-sch-trace"

Expand All @@ -28,34 +30,49 @@ export function convertCircuitJsonToSchematicSvg(
options?: Options,
): string {
// Get bounds with padding
const bounds = getSchematicBoundsFromCircuitJson(circuitJson)
const { minX, minY, maxX, maxY } = bounds

const padding = 1 // Reduced padding for tighter boundary
const circuitWidth = maxX - minX + 2 * padding
const circuitHeight = maxY - minY + 2 * padding
const realBounds = getSchematicBoundsFromCircuitJson(circuitJson)
const realWidth = realBounds.maxX - realBounds.minX
const realHeight = realBounds.maxY - realBounds.minY

const svgWidth = options?.width ?? 1200
const svgHeight = options?.height ?? 600

// Calculate scale factor to fit circuit within SVG, maintaining aspect ratio
const scaleX = svgWidth / circuitWidth
const scaleY = svgHeight / circuitHeight
const scaleFactor = Math.min(scaleX, scaleY)

// Calculate centering offsets
const offsetX = (svgWidth - circuitWidth * scaleFactor) / 2
const offsetY = (svgHeight - circuitHeight * scaleFactor) / 2

// Create transform matrix
const transform = compose(
translate(
offsetX - minX * scaleFactor + padding * scaleFactor,
svgHeight - offsetY + minY * scaleFactor - padding * scaleFactor,
),
scale(scaleFactor, scaleFactor),
)
// Compute the padding such that we maintain the same aspect ratio
const circuitAspectRatio = realWidth / realHeight
const containerAspectRatio = svgWidth / svgHeight

let screenPaddingPx: { x: number; y: number }
if (circuitAspectRatio > containerAspectRatio) {
// Circuit is wider than container - fit to width
const newHeight = svgWidth / circuitAspectRatio
screenPaddingPx = {
x: 0,
y: (svgHeight - newHeight) / 2,
}
} else {
// Circuit is taller than container - fit to height
const newWidth = svgHeight * circuitAspectRatio
screenPaddingPx = {
x: (svgWidth - newWidth) / 2,
y: 0,
}
}

// Calculate projection using REAL points and SCREEN points
// We're saying to map the real bounds to the screen bounds by giving 3 points
// for each coordinate space
const transform = fromTriangles(
[
{ x: realBounds.minX, y: realBounds.maxY },
{ x: realBounds.maxX, y: realBounds.maxY },
{ x: realBounds.maxX, y: realBounds.minY },
],
[
{ x: screenPaddingPx.x, y: screenPaddingPx.y },
{ x: svgWidth - screenPaddingPx.x, y: screenPaddingPx.y },
{ x: svgWidth - screenPaddingPx.x, y: svgHeight - screenPaddingPx.y },
],
)
const svgChildren: SvgObject[] = []

// Add background rectangle
Expand All @@ -76,7 +93,9 @@ export function convertCircuitJsonToSchematicSvg(
// Add grid if enabled
if (options?.grid) {
const gridConfig = typeof options.grid === "object" ? options.grid : {}
svgChildren.push(drawSchematicGrid({ bounds, transform, ...gridConfig }))
svgChildren.push(
drawSchematicGrid({ bounds: realBounds, transform, ...gridConfig }),
)
}

// Add labeled points if provided
Expand All @@ -93,15 +112,14 @@ export function convertCircuitJsonToSchematicSvg(
const schComponentSvgs: SvgObject[] = []
const schTraceSvgs: SvgObject[] = []

// Process all elements using transform
for (const elm of circuitJson) {
if (elm.type === "schematic_debug_object") {
schDebugObjectSvgs.push(
...createSvgObjectsFromSchDebugObject(elm, transform),
)
} else if (elm.type === "schematic_component") {
schComponentSvgs.push(
...createSchematicComponent({
...createSvgObjectsFromSchematicComponent({
component: elm,
transform,
circuitJson,
Expand Down Expand Up @@ -132,17 +150,20 @@ export function convertCircuitJsonToSchematicSvg(
children: [
{
type: "text",

// DO NOT USE THESE CLASSES!!!!
// PUT STYLES IN THE SVG OBJECTS THEMSELVES
value: `
.boundary { fill: ${colorMap.schematic.background}; }
.schematic-boundary { fill: none; stroke: #fff; stroke-width: 0.3; }
.schematic-boundary { fill: none; stroke: #fff; }
.component { fill: none; stroke: ${colorMap.schematic.component_outline}; }
.chip { fill: ${colorMap.schematic.component_body}; stroke: ${colorMap.schematic.component_outline}; }
.component-pin { fill: none; stroke: ${colorMap.schematic.component_outline}; }
.trace { stroke: ${colorMap.schematic.wire}; stroke-width: ${0.02 * scaleFactor}; fill: none; }
.text { font-family: Arial, sans-serif; font-size: ${0.2 * scaleFactor}px; fill: ${colorMap.schematic.wire}; }
.pin-number { font-size: ${0.15 * scaleFactor}px; fill: ${colorMap.schematic.pin_number}; }
.trace { stroke: ${colorMap.schematic.wire}; }
.text { font-family: sans-serif; fill: ${colorMap.schematic.wire}; }
.pin-number { fill: ${colorMap.schematic.pin_number}; }
.port-label { fill: ${colorMap.schematic.reference}; }
.component-name { font-size: ${0.25 * scaleFactor}px; fill: ${colorMap.schematic.reference}; }
.component-name { fill: ${colorMap.schematic.reference}; }
`,
name: "",
attributes: {},
Expand Down
16 changes: 10 additions & 6 deletions lib/sch/draw-schematic-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function drawSchematicGrid(params: {
for (let x = Math.ceil(minX); x <= Math.floor(maxX); x += cellSize) {
const start = transformPoint(x, minY)
const end = transformPoint(x, maxY)

gridLines.push({
name: "line",
type: "element",
Expand All @@ -48,7 +48,7 @@ export function drawSchematicGrid(params: {
for (let y = Math.ceil(minY); y <= Math.floor(maxY); y += cellSize) {
const start = transformPoint(minX, y)
const end = transformPoint(maxX, y)

gridLines.push({
name: "line",
type: "element",
Expand All @@ -69,18 +69,22 @@ export function drawSchematicGrid(params: {
for (let x = Math.ceil(minX); x <= Math.floor(maxX); x += cellSize) {
for (let y = Math.ceil(minY); y <= Math.floor(maxY); y += cellSize) {
const point = transformPoint(x, y)

gridLines.push({
name: "text",
type: "element",
attributes: {
x: (point.x - 2.5).toString(),
y: (point.y - 2.5).toString(),
y: (point.y - 5).toString(),
fill: colorMap.schematic.grid,
"font-size": ((cellSize / 5) * Math.abs(params.transform.a)).toString(),
"font-size": (
(cellSize / 5) *
Math.abs(params.transform.a)
).toString(),
"fill-opacity": "0.5",
"text-anchor": "middle",
"dominant-baseline": "middle",
"font-family": "sans-serif",
},
children: [
{
Expand All @@ -103,4 +107,4 @@ export function drawSchematicGrid(params: {
attributes: { class: "grid" },
children: gridLines,
}
}
}
110 changes: 110 additions & 0 deletions lib/sch/svg-object-fns/create-svg-objects-for-sch-port-box-line.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type {
AnyCircuitElement,
SchematicComponent,
SchematicPort,
} from "circuit-json"
import type { SvgObject } from "lib/svg-object"
import { applyToPoint, type Matrix } from "transformation-matrix"
import { su } from "@tscircuit/soup-util"
import { getSchStrokeSize } from "lib/utils/get-sch-stroke-size"

const PIN_CIRCLE_RADIUS_MM = 0.02

/**
* The schematic port box line is the line and circle that goes from the edge
* of the component box to the port.
*/
export const createSvgObjectsForSchPortBoxLine = ({
schPort,
schComponent,
transform,
circuitJson,
}: {
schPort: SchematicPort
schComponent: SchematicComponent
transform: Matrix
circuitJson: AnyCircuitElement[]
}): SvgObject[] => {
const svgObjects: SvgObject[] = []

const srcPort = su(circuitJson as any).source_port.get(schPort.source_port_id)

const realEdgePos = {
x: schPort.center.x,
y: schPort.center.y,
}

// schPort.distance_from_component_edge is currently calculated incorrectly
// in core
const realPinLineLength = schPort.distance_from_component_edge ?? 0.4

switch (schPort.side_of_component) {
case "left":
realEdgePos.x += realPinLineLength
break
case "right":
realEdgePos.x -= realPinLineLength
break
case "top":
realEdgePos.y -= realPinLineLength
break
case "bottom":
realEdgePos.y += realPinLineLength
break
}

const screenSchPortPos = applyToPoint(transform, schPort.center)
const screenRealEdgePos = applyToPoint(transform, realEdgePos)

// Subtract the pin circle radius from the pin line length to get the end
const realLineEnd = { ...schPort.center }

switch (schPort.side_of_component) {
case "left":
realLineEnd.x += PIN_CIRCLE_RADIUS_MM
break
case "right":
realLineEnd.x -= PIN_CIRCLE_RADIUS_MM
break
case "top":
realLineEnd.y -= PIN_CIRCLE_RADIUS_MM
break
case "bottom":
realLineEnd.y += PIN_CIRCLE_RADIUS_MM
break
}
const screenLineEnd = applyToPoint(transform, realLineEnd)

// Add port line
svgObjects.push({
name: "line",
type: "element",
attributes: {
class: "component-pin",
x1: screenLineEnd.x.toString(),
y1: screenLineEnd.y.toString(),
x2: screenRealEdgePos.x.toString(),
y2: screenRealEdgePos.y.toString(),
"stroke-width": `${getSchStrokeSize(transform)}px`,
},
value: "",
children: [],
})

// Add port circle
svgObjects.push({
name: "circle",
type: "element",
attributes: {
class: "component-pin",
cx: screenSchPortPos.x.toString(),
cy: screenSchPortPos.y.toString(),
r: (Math.abs(transform.a) * PIN_CIRCLE_RADIUS_MM).toString(),
"stroke-width": `${getSchStrokeSize(transform)}px`,
},
value: "",
children: [],
})

return svgObjects
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type {
AnyCircuitElement,
SchematicComponent,
SchematicPort,
} from "circuit-json"
import type { SvgObject } from "lib/svg-object"
import { colorMap } from "lib/utils/colors"
import { getSchFontSize } from "lib/utils/get-sch-font-size"
import { getUnitVectorFromOutsideToEdge } from "lib/utils/get-unit-vector-from-outside-to-edge"
import { applyToPoint, type Matrix } from "transformation-matrix"

const LABEL_DIST_FROM_EDGE_MM = 0.1

export const createSvgObjectsForSchPortPinLabel = (params: {
schPort: SchematicPort
schComponent: SchematicComponent
transform: Matrix
circuitJson: AnyCircuitElement[]
}): SvgObject[] => {
const svgObjects: SvgObject[] = []
const { schPort, schComponent, transform, circuitJson } = params

const realPinNumberPos = {
x: schPort.center.x,
y: schPort.center.y,
}

if (!schPort.side_of_component) return []
const vecToEdge = getUnitVectorFromOutsideToEdge(schPort.side_of_component)

const realPinEdgeDistance = schPort.distance_from_component_edge ?? 0.4

// Move the pin number halfway to the edge of the box component so it sits
// between the edge and the port, exactly in the middle
realPinNumberPos.x +=
vecToEdge.x * (realPinEdgeDistance + LABEL_DIST_FROM_EDGE_MM)
realPinNumberPos.y +=
vecToEdge.y * (realPinEdgeDistance + LABEL_DIST_FROM_EDGE_MM)

// Transform the pin position from local to global coordinates
const screenPinNumberTextPos = applyToPoint(transform, realPinNumberPos)

const label = schComponent.port_labels?.[`pin${schPort.pin_number}`]

if (!label) return []

svgObjects.push({
name: "text",
type: "element",
attributes: {
class: "pin-number",
x: screenPinNumberTextPos.x.toString(),
y: screenPinNumberTextPos.y.toString(),
style: "font-family: sans-serif;",
fill: colorMap.schematic.pin_number,
"text-anchor": schPort.side_of_component === "left" ? "start" : "end",
"dominant-baseline": "middle",
"font-size": `${getSchFontSize(transform, "pin_number")}px`,
transform:
schPort.side_of_component === "top" ||
schPort.side_of_component === "bottom"
? `rotate(-90 ${screenPinNumberTextPos.x} ${screenPinNumberTextPos.y})`
: "",
},
children: [
{
type: "text",
value: label || "",
name: "",
attributes: {},
children: [],
},
],
value: "",
})

return svgObjects
}
Loading

0 comments on commit 7e08ae8

Please sign in to comment.