muse/esp32-eink-display
A 55×46 mm two-layer ESP32-S3 e-paper controller board with USB‑C power/programming, buck regulation, FPC display connector, UC8179-style boost/charge-pump circuitry, ESD protection, controls, and routed passive components.
- Version
- 1.3.4
- License
- unset
- Stars
- 0
renderer-fix/upstream.patch
diff --git a/lib/compile-circuit.ts b/lib/compile-circuit.ts
index 7f16e1a..e85b657 100644
--- a/lib/compile-circuit.ts
+++ b/lib/compile-circuit.ts
@@ -28,6 +28,28 @@ function shape(e: Element, hole = false): Point[][] {
(r) => expandBrepRing(r.vertices),
)
if (e.outline?.length) return [e.outline]
+ if (e.shape === "rotated_pill_hole_with_rect_pad") {
+ // Pad and slot use independent rotations. Hole offsets are board-space
+ // coordinates, as in the reference canvas renderer, not pad-local offsets.
+ const base = center(e)
+ const c = hole
+ ? {
+ x: base.x + (e.hole_offset_x ?? 0),
+ y: base.y + (e.hole_offset_y ?? 0),
+ }
+ : base
+ const width = hole ? e.hole_width : e.rect_pad_width
+ const height = hole ? e.hole_height : e.rect_pad_height
+ return [
+ rectangle(
+ c,
+ width,
+ height,
+ hole ? Math.min(width, height) / 2 : (e.rect_border_radius ?? 0),
+ (hole ? e.hole_ccw_rotation : e.rect_ccw_rotation) ?? 0,
+ ),
+ ]
+ }
const kind = hole ? (e.hole_shape ?? e.shape) : e.shape
if (kind === "polygon") return [e.points ?? e.vertices ?? []]
const base = center(e),
diff --git a/tests/fixtures/muse-socket-plated-holes.circuit.json b/tests/fixtures/muse-socket-plated-holes.circuit.json
new file mode 100644
index 0000000..eddab70
--- /dev/null
+++ b/tests/fixtures/muse-socket-plated-holes.circuit.json
@@ -0,0 +1,60 @@
+[
+ {
+ "type": "pcb_plated_hole",
+ "pcb_plated_hole_id": "pcb_plated_hole_0",
+ "pcb_component_id": "pcb_component_1",
+ "pcb_port_id": "pcb_port_3",
+ "outer_width": 1.5999968,
+ "outer_height": 1.5999968,
+ "hole_width": 1.0499852,
+ "hole_height": 1.0499852,
+ "shape": "rotated_pill_hole_with_rect_pad",
+ "port_hints": [
+ "unnamed_platedhole1",
+ "pin1"
+ ],
+ "x": -33.43,
+ "y": 26.67,
+ "layers": [
+ "top",
+ "bottom"
+ ],
+ "is_covered_with_solder_mask": false,
+ "subcircuit_id": "subcircuit_source_group_30",
+ "hole_shape": "rotated_pill",
+ "pad_shape": "rect",
+ "hole_ccw_rotation": 270,
+ "rect_ccw_rotation": 270,
+ "rect_pad_width": 1.5999968,
+ "rect_pad_height": 1.5999968
+ },
+ {
+ "type": "pcb_plated_hole",
+ "pcb_plated_hole_id": "pcb_plated_hole_22",
+ "pcb_component_id": "pcb_component_2",
+ "pcb_port_id": "pcb_port_25",
+ "outer_width": 1.5999968,
+ "outer_height": 1.5999968,
+ "hole_width": 1.0499852,
+ "hole_height": 1.0499852,
+ "shape": "rotated_pill_hole_with_rect_pad",
+ "port_hints": [
+ "unnamed_platedhole23",
+ "pin1"
+ ],
+ "x": -10.570000000000002,
+ "y": 26.67,
+ "layers": [
+ "top",
+ "bottom"
+ ],
+ "is_covered_with_solder_mask": false,
+ "subcircuit_id": "subcircuit_source_group_30",
+ "hole_shape": "rotated_pill",
+ "pad_shape": "rect",
+ "hole_ccw_rotation": 270,
+ "rect_ccw_rotation": 270,
+ "rect_pad_width": 1.5999968,
+ "rect_pad_height": 1.5999968
+ }
+]
diff --git a/tests/geometry.test.ts b/tests/geometry.test.ts
index 4c413e7..172c5bb 100644
--- a/tests/geometry.test.ts
+++ b/tests/geometry.test.ts
@@ -10,6 +10,7 @@ import {
import { fixtures, silkscreenGraphics } from "../site/fixtures"
import large from "./fixtures/am3352-dev-board.circuit.json"
import breakout from "./fixtures/f1c100s-breakout.circuit.json"
+import museSockets from "./fixtures/muse-socket-plated-holes.circuit.json"
function area(mesh: ReturnType<MeshBuilder["build"]>) {
let area = 0
@@ -277,3 +278,113 @@ test("keepout fill and stripes preserve holes in concave polygons", () => {
expect(stripeArea).toBeGreaterThan(0)
expect(stripeArea).toBeLessThan(fillArea)
})
+
+function meshBounds(mesh: ReturnType<MeshBuilder["build"]>) {
+ const xs: number[] = [],
+ ys: number[] = []
+ for (let i = 0; i < mesh.vertices.length; i += 8) {
+ xs.push(mesh.vertices[i])
+ ys.push(mesh.vertices[i + 1])
+ }
+ return [Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys)]
+}
+
+test("Muse imported socket pads compile on both copper layers and through the board", () => {
+ const scene = compileCircuitJson(museSockets as any)
+ expect(scene.diagnostics).toEqual([])
+ for (const name of [
+ "top",
+ "bottom",
+ "soldermask_top",
+ "soldermask_bottom",
+ "board",
+ "drill",
+ ]) {
+ const layer = scene.layers.find((l) => l.name === name)!
+ const mesh =
+ name === "board" || name.startsWith("soldermask")
+ ? layer.erase
+ : layer.paint
+ expect(mesh.indices.length).toBeGreaterThan(0)
+ expect([...mesh.vertices].every(Number.isFinite)).toBe(true)
+ }
+ for (const name of ["top", "bottom"]) {
+ const layer = scene.layers.find((l) => l.name === name)!
+ expect(area(layer.paint)).toBeCloseTo(2 * 1.5999968 ** 2)
+ expect(area(layer.erase)).toBeCloseTo(2 * Math.PI * (1.0499852 / 2) ** 2, 2)
+ }
+})
+
+test("rotated pill holes have independent pad and drill rotations and world-space offsets", () => {
+ for (const explicitHoleShape of [false, true]) {
+ const scene = compileCircuitJson([
+ {
+ type: "pcb_plated_hole",
+ pcb_plated_hole_id: "rotated-slot",
+ shape: "rotated_pill_hole_with_rect_pad",
+ ...(explicitHoleShape
+ ? { hole_shape: "rotated_pill", pad_shape: "rect" }
+ : {}),
+ x: 10,
+ y: 20,
+ rect_pad_width: 8,
+ rect_pad_height: 6,
+ rect_ccw_rotation: 90,
+ hole_width: 4,
+ hole_height: 2,
+ hole_ccw_rotation: 0,
+ hole_offset_x: 0.5,
+ hole_offset_y: -0.25,
+ layers: ["top", "inner1", "bottom"],
+ },
+ ] as any)
+ expect(scene.diagnostics).toEqual([])
+ for (const name of ["top", "inner1", "bottom"]) {
+ const layer = scene.layers.find((l) => l.name === name)!
+ expect(meshBounds(layer.paint)).toEqual([7, 13, 16, 24])
+ const drillBounds = meshBounds(layer.erase)
+ // The slot stays horizontal when the rectangular pad rotates vertically.
+ for (const [i, expected] of [8.5, 12.5, 18.75, 20.75].entries())
+ expect(drillBounds[i]).toBeCloseTo(expected, 2)
+ expect(area(layer.paint)).toBeCloseTo(48)
+ expect(area(layer.erase)).toBeCloseTo(4 + Math.PI, 2)
+ }
+ const drill = scene.layers.find((l) => l.name === "drill")!.paint
+ for (const name of ["board", "soldermask_top", "soldermask_bottom"])
+ expect(
+ scene.layers.find((l) => l.name === name)!.erase.vertices.length,
+ ).toBeGreaterThan(0)
+ const erase = scene.layers.find((l) => l.name === "top")!.erase
+ expect(drill.vertices.length).toBe(erase.vertices.length)
+ for (let i = 0; i < drill.vertices.length; i += 8) {
+ expect(drill.vertices[i]).toBe(erase.vertices[i])
+ expect(drill.vertices[i + 1]).toBe(erase.vertices[i + 1])
+ }
+ }
+})
+
+test("rounded rectangular pads stay rectangular while their slots rotate independently", () => {
+ const scene = compileCircuitJson([
+ {
+ type: "pcb_plated_hole",
+ pcb_plated_hole_id: "rounded-slot",
+ shape: "rotated_pill_hole_with_rect_pad",
+ hole_shape: "rotated_pill",
+ x: 0,
+ y: 0,
+ rect_pad_width: 8,
+ rect_pad_height: 6,
+ rect_border_radius: 0.5,
+ rect_ccw_rotation: 0,
+ hole_width: 4,
+ hole_height: 2,
+ hole_ccw_rotation: 90,
+ layers: ["top", "bottom"],
+ },
+ ] as any)
+ expect(scene.diagnostics).toEqual([])
+ const layer = scene.layers.find((l) => l.name === "top")!
+ expect(area(layer.paint)).toBeCloseTo(48 - (4 - Math.PI) * 0.5 ** 2, 2)
+ for (const [i, expected] of [-1, 1, -2, 2].entries())
+ expect(meshBounds(layer.erase)[i]).toBeCloseTo(expected, 2)
+})
