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)
+})