Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions lib/components/primitive-components/Constraint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { constraintProps } from "@tscircuit/props"
import { distance } from "circuit-json"
import { z } from "zod"
import { PrimitiveComponent } from "../base-components/PrimitiveComponent"

Expand All @@ -12,15 +13,54 @@ const edgeSpecifiers = [

export type EdgeSpecifier = (typeof edgeSpecifiers)[number]

export class Constraint extends PrimitiveComponent<typeof constraintProps> {
/**
* Extended constraint props that add centerX/centerY for absolute positioning
* of constraint clusters. These will be upstreamed to @tscircuit/props.
*/
export const extendedConstraintProps = z.union([
z.object({
pcb: z.literal(true).optional(),
xDist: distance,
left: z.string(),
right: z.string(),
edgeToEdge: z.literal(true).optional(),
centerToCenter: z.literal(true).optional(),
centerX: distance.optional(),
centerY: distance.optional(),
}),
z.object({
pcb: z.literal(true).optional(),
yDist: distance,
top: z.string(),
bottom: z.string(),
edgeToEdge: z.literal(true).optional(),
centerToCenter: z.literal(true).optional(),
centerX: distance.optional(),
centerY: distance.optional(),
}),
z.object({
pcb: z.literal(true).optional(),
sameY: z.literal(true).optional(),
for: z.array(z.string()),
}),
z.object({
pcb: z.literal(true).optional(),
sameX: z.literal(true).optional(),
for: z.array(z.string()),
}),
])

export class Constraint extends PrimitiveComponent<
typeof extendedConstraintProps
> {
get config() {
return {
componentName: "Constraint",
zodProps: constraintProps,
zodProps: extendedConstraintProps,
}
}

constructor(props: z.input<typeof constraintProps>) {
constructor(props: z.input<typeof extendedConstraintProps>) {
super(props)
if ("xdist" in props || "ydist" in props) {
if (!("edgeToEdge" in props) && !("centerToCenter" in props)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { Group } from "../Group"
import type { Constraint } from "../../Constraint"
import type { PackInput } from "calculate-packing"
import { length } from "circuit-json"
import * as kiwi from "@lume/kiwi"

export type ClusterInfo = {
componentIds: string[]
constraints: Constraint[]
relativeCenters?: Record<string, { x: number; y: number }>
/** Absolute center X position for the cluster (from centerX prop) */
absoluteCenterX?: number
/** Absolute center Y position for the cluster (from centerY prop) */
absoluteCenterY?: number
}

export const applyComponentConstraintClusters = (
Expand Down Expand Up @@ -36,7 +41,13 @@ export const applyComponentConstraintClusters = (
const getIdFromSelector = (sel: string): string | undefined => {
const name = sel.startsWith(".") ? sel.slice(1) : sel
const child = group.children.find((c) => (c as any).name === name)
return child?.pcb_component_id ?? undefined
if (!child) return undefined
// For regular components, use pcb_component_id
if (child.pcb_component_id) return child.pcb_component_id
// For groups, use source_group_id (matches pack input componentId for groups)
if ((child as Group).source_group_id)
return (child as Group).source_group_id!
return undefined
}

for (const constraint of constraints) {
Expand Down Expand Up @@ -245,6 +256,18 @@ export const applyComponentConstraintClusters = (
})

info.relativeCenters = relCenters

// Extract absolute center positioning from constraints (centerX/centerY)
for (const constraint of info.constraints) {
const props = constraint._parsedProps as any
if ("centerX" in props && props.centerX != null) {
info.absoluteCenterX = length.parse(props.centerX)
}
if ("centerY" in props && props.centerY != null) {
info.absoluteCenterY = length.parse(props.centerY)
}
}

clusterMap[info.componentIds[0]] = info
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,40 +66,123 @@ export const applyPackOutput = (

const cluster = clusterMap[componentId]
if (cluster) {
// Use absolute center from centerX/centerY if specified, otherwise
// use the position determined by the pack solver
const clusterCenter = {
x: cluster.absoluteCenterX ?? center.x,
y: cluster.absoluteCenterY ?? center.y,
}
const rotationDegrees = ccwRotationDegrees ?? ccwRotationOffset ?? 0
const angleRad = (rotationDegrees * Math.PI) / 180
for (const memberId of cluster.componentIds) {
const rel = cluster.relativeCenters![memberId]
if (!rel) continue
db.pcb_component.update(memberId, {
position_mode: "packed",
})
const rotatedRel = {
x: rel.x * Math.cos(angleRad) - rel.y * Math.sin(angleRad),
y: rel.x * Math.sin(angleRad) + rel.y * Math.cos(angleRad),
}

// Try as a regular pcb_component first
const member = db.pcb_component.get(memberId)
if (!member) continue
const originalCenter = member.center
const transformMatrix = compose(
group._computePcbGlobalTransformBeforeLayout(),
translate(center.x + rotatedRel.x, center.y + rotatedRel.y),
rotate(angleRad),
translate(-originalCenter.x, -originalCenter.y),
)
const related = db
.toArray()
.filter(
(elm) =>
"pcb_component_id" in elm && elm.pcb_component_id === memberId,
if (member) {
db.pcb_component.update(memberId, {
position_mode: "packed",
})
const originalCenter = member.center
const transformMatrix = compose(
group._computePcbGlobalTransformBeforeLayout(),
translate(
clusterCenter.x + rotatedRel.x,
clusterCenter.y + rotatedRel.y,
),
rotate(angleRad),
translate(-originalCenter.x, -originalCenter.y),
)
transformPCBElements(related as any, transformMatrix)
updateCadRotation({
db,
pcbComponentId: memberId,
rotationDegrees,
layer: member.layer,
})
const related = db
.toArray()
.filter(
(elm) =>
"pcb_component_id" in elm && elm.pcb_component_id === memberId,
)
transformPCBElements(related as any, transformMatrix)
updateCadRotation({
db,
pcbComponentId: memberId,
rotationDegrees,
layer: member.layer,
})
continue
}

// Try as a group (memberId is source_group_id)
const pcbGroup = db.pcb_group
.list()
.find((g) => g.source_group_id === memberId)
if (pcbGroup) {
const originalCenter = pcbGroup.center
const transformMatrix = compose(
group._computePcbGlobalTransformBeforeLayout(),
translate(
clusterCenter.x + rotatedRel.x,
clusterCenter.y + rotatedRel.y,
),
rotate(angleRad),
translate(-originalCenter.x, -originalCenter.y),
)
const relatedElements = db.toArray().filter((elm) => {
if ("source_group_id" in elm && elm.source_group_id) {
if (elm.source_group_id === memberId) return true
if (isDescendantGroup(db, elm.source_group_id, memberId))
return true
}
if ("source_component_id" in elm && elm.source_component_id) {
const sourceComponent = db.source_component.get(
elm.source_component_id,
)
if (sourceComponent?.source_group_id) {
if (sourceComponent.source_group_id === memberId) return true
if (
isDescendantGroup(
db,
sourceComponent.source_group_id,
memberId,
)
)
return true
}
}
if ("pcb_component_id" in elm && elm.pcb_component_id) {
const pcbComp = db.pcb_component.get(elm.pcb_component_id)
if (pcbComp?.source_component_id) {
const sourceComp = db.source_component.get(
pcbComp.source_component_id,
)
if (sourceComp?.source_group_id) {
if (sourceComp.source_group_id === memberId) return true
if (
isDescendantGroup(db, sourceComp.source_group_id, memberId)
)
return true
}
}
}
return false
})
for (const elm of relatedElements) {
if (elm.type === "pcb_component") {
db.pcb_component.update(elm.pcb_component_id, {
position_mode: "packed",
})
}
}
transformPCBElements(relatedElements as any, transformMatrix)
db.pcb_group.update(pcbGroup.pcb_group_id, {
center: {
x: clusterCenter.x + rotatedRel.x,
y: clusterCenter.y + rotatedRel.y,
},
})
}
}
continue
}
Expand Down
5 changes: 4 additions & 1 deletion lib/fiber/intrinsic-jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export interface TscircuitElements {
fabricationnotetext: Props.FabricationNoteTextProps
fabricationnotepath: Props.FabricationNotePathProps
fabricationnotedimension: Props.FabricationNoteDimensionProps
constraint: Props.ConstraintProps
constraint: Props.ConstraintProps & {
centerX?: number | string
centerY?: number | string
}
constrainedlayout: Props.ConstrainedLayoutProps
battery: Props.BatteryProps
pinheader: Props.PinHeaderProps
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading