Skip to content

Commit 7fd526c

Browse files
authored
Fix rotated snap points (#480)
1 parent fa6bf66 commit 7fd526c

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

src/components/DimensionOverlay.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "lib/util/get-primitive-bounding-box"
1111
import type { BoundingBox } from "lib/util/get-primitive-bounding-box"
1212
import { useDiagonalLabel } from "hooks/useDiagonalLabel"
13+
import { getPrimitiveSnapPoints } from "lib/util/get-primitive-snap-points"
1314

1415
interface Props {
1516
transform?: Matrix
@@ -83,6 +84,13 @@ export const DimensionOverlay = ({
8384
for (const primitive of primitives) {
8485
if (!primitive._element) continue
8586
if (shouldExcludePrimitiveFromSnapping(primitive)) continue
87+
if (primitive.pcb_drawing_type === "pill") continue
88+
if (
89+
primitive.pcb_drawing_type === "rect" &&
90+
primitive.ccw_rotation &&
91+
primitive.ccw_rotation !== 0
92+
)
93+
continue
8694
const bbox = getPrimitiveBoundingBox(primitive)
8795
if (!bbox) continue
8896

@@ -96,9 +104,35 @@ export const DimensionOverlay = ({
96104
return boundingBoxes
97105
}, [primitives])
98106

107+
const primitiveSnappingPoints = useMemo(() => {
108+
const snapPoints: {
109+
anchor: NinePointAnchor | string
110+
point: { x: number; y: number }
111+
element: object
112+
}[] = []
113+
114+
for (const primitive of primitives) {
115+
if (!primitive._element) continue
116+
if (shouldExcludePrimitiveFromSnapping(primitive)) continue
117+
118+
const primitivePoints = getPrimitiveSnapPoints(primitive)
119+
if (primitivePoints.length === 0) continue
120+
121+
for (const snap of primitivePoints) {
122+
snapPoints.push({
123+
anchor: snap.anchor,
124+
point: snap.point,
125+
element: primitive._element as object,
126+
})
127+
}
128+
}
129+
130+
return snapPoints
131+
}, [primitives])
132+
99133
const snappingPoints = useMemo(() => {
100134
const points: {
101-
anchor: NinePointAnchor | "origin"
135+
anchor: NinePointAnchor | "origin" | string
102136
point: { x: number; y: number }
103137
element: object | null
104138
}[] = []
@@ -133,14 +167,18 @@ export const DimensionOverlay = ({
133167
}
134168
})
135169

170+
for (const snap of primitiveSnappingPoints) {
171+
points.push(snap)
172+
}
173+
136174
points.push({
137175
anchor: "origin",
138176
point: { x: 0, y: 0 },
139177
element: null,
140178
})
141179

142180
return points
143-
}, [elementBoundingBoxes])
181+
}, [elementBoundingBoxes, primitiveSnappingPoints])
144182

145183
const snappingPointsWithScreen = useMemo(() => {
146184
return snappingPoints.map((snap, index) => ({
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { NinePointAnchor } from "circuit-json"
2+
import type { Primitive } from "../types"
3+
4+
export interface PrimitiveSnapPoint {
5+
anchor: NinePointAnchor | string
6+
point: { x: number; y: number }
7+
}
8+
9+
const rotatePoint = (
10+
point: { x: number; y: number },
11+
center: { x: number; y: number },
12+
rotationDeg: number,
13+
) => {
14+
const radians = (rotationDeg * Math.PI) / 180
15+
const cos = Math.cos(radians)
16+
const sin = Math.sin(radians)
17+
18+
const translatedX = point.x - center.x
19+
const translatedY = point.y - center.y
20+
21+
const rotatedX = translatedX * cos - translatedY * sin
22+
const rotatedY = translatedX * sin + translatedY * cos
23+
24+
return {
25+
x: rotatedX + center.x,
26+
y: rotatedY + center.y,
27+
}
28+
}
29+
30+
const getNinePointAnchors = (
31+
center: { x: number; y: number },
32+
halfWidth: number,
33+
halfHeight: number,
34+
rotationDeg: number,
35+
): PrimitiveSnapPoint[] => {
36+
const basePoints: Record<NinePointAnchor, { x: number; y: number }> = {
37+
top_left: { x: center.x - halfWidth, y: center.y - halfHeight },
38+
top_center: { x: center.x, y: center.y - halfHeight },
39+
top_right: { x: center.x + halfWidth, y: center.y - halfHeight },
40+
center_left: { x: center.x - halfWidth, y: center.y },
41+
center: { x: center.x, y: center.y },
42+
center_right: { x: center.x + halfWidth, y: center.y },
43+
bottom_left: { x: center.x - halfWidth, y: center.y + halfHeight },
44+
bottom_center: { x: center.x, y: center.y + halfHeight },
45+
bottom_right: { x: center.x + halfWidth, y: center.y + halfHeight },
46+
}
47+
48+
if (rotationDeg === 0) {
49+
return Object.entries(basePoints).map(([anchor, point]) => ({
50+
anchor: anchor as NinePointAnchor,
51+
point,
52+
}))
53+
}
54+
55+
return Object.entries(basePoints).map(([anchor, point]) => ({
56+
anchor: anchor as NinePointAnchor,
57+
point: rotatePoint(point, center, rotationDeg),
58+
}))
59+
}
60+
61+
export const getPrimitiveSnapPoints = (
62+
primitive: Primitive,
63+
): PrimitiveSnapPoint[] => {
64+
switch (primitive.pcb_drawing_type) {
65+
case "rect": {
66+
const rotation = primitive.ccw_rotation ?? 0
67+
return getNinePointAnchors(
68+
{ x: primitive.x, y: primitive.y },
69+
primitive.w / 2,
70+
primitive.h / 2,
71+
rotation,
72+
)
73+
}
74+
case "pill": {
75+
const rotation = primitive.ccw_rotation ?? 0
76+
return getNinePointAnchors(
77+
{ x: primitive.x, y: primitive.y },
78+
primitive.w / 2,
79+
primitive.h / 2,
80+
rotation,
81+
)
82+
}
83+
case "circle": {
84+
return [
85+
{ anchor: "circle_center", point: { x: primitive.x, y: primitive.y } },
86+
{
87+
anchor: "circle_right",
88+
point: { x: primitive.x + primitive.r, y: primitive.y },
89+
},
90+
{
91+
anchor: "circle_left",
92+
point: { x: primitive.x - primitive.r, y: primitive.y },
93+
},
94+
{
95+
anchor: "circle_top",
96+
point: { x: primitive.x, y: primitive.y - primitive.r },
97+
},
98+
{
99+
anchor: "circle_bottom",
100+
point: { x: primitive.x, y: primitive.y + primitive.r },
101+
},
102+
]
103+
}
104+
case "oval": {
105+
return [
106+
{ anchor: "oval_center", point: { x: primitive.x, y: primitive.y } },
107+
{
108+
anchor: "oval_right",
109+
point: { x: primitive.x + primitive.rX, y: primitive.y },
110+
},
111+
{
112+
anchor: "oval_left",
113+
point: { x: primitive.x - primitive.rX, y: primitive.y },
114+
},
115+
{
116+
anchor: "oval_top",
117+
point: { x: primitive.x, y: primitive.y - primitive.rY },
118+
},
119+
{
120+
anchor: "oval_bottom",
121+
point: { x: primitive.x, y: primitive.y + primitive.rY },
122+
},
123+
]
124+
}
125+
case "line": {
126+
const midPoint = {
127+
x: (primitive.x1 + primitive.x2) / 2,
128+
y: (primitive.y1 + primitive.y2) / 2,
129+
}
130+
return [
131+
{ anchor: "line_start", point: { x: primitive.x1, y: primitive.y1 } },
132+
{ anchor: "line_mid", point: midPoint },
133+
{ anchor: "line_end", point: { x: primitive.x2, y: primitive.y2 } },
134+
]
135+
}
136+
case "polygon": {
137+
return primitive.points.map((point, index) => ({
138+
anchor: `polygon_vertex_${index}`,
139+
point,
140+
}))
141+
}
142+
case "polygon_with_arcs": {
143+
return primitive.brep_shape.outer_ring.vertices.map((vertex, index) => ({
144+
anchor: `polygon_with_arcs_vertex_${index}`,
145+
point: { x: vertex.x, y: vertex.y },
146+
}))
147+
}
148+
default:
149+
return []
150+
}
151+
}

0 commit comments

Comments
 (0)