imrishabh18/corne-keyboard

The code defines and renders two surface-mount chip components with SMT pads, silkscreen outlines, and 3D CAD models (OBJ and STEP) for PCB assembly.

Version
2.0.21
License
unset
Stars
1

lib/CorneHalf.tsx

import { CPG135001S30 } from "../imports/CPG135001S30/CPG135001S30"
import { B_2100S04P_A110 } from "../imports/B_2100S04P_A110/B_2100S04P_A110"
import { A_2_54_1_3P_ } from "../imports/A_2_54_1_3P_/A_2_54_1_3P_"
import { reviewedRightAutorouter } from "./routing/reviewed-right"
import { reviewedLeftAutorouter } from "./routing/reviewed-left"
import { Fragment } from "react"
import { A_0402WGF1001TCE } from "../imports/A_0402WGF1001TCE/A_0402WGF1001TCE"
import { A_0402WGF1002TCE } from "../imports/A_0402WGF1002TCE/A_0402WGF1002TCE"
import { A_0402WGF270JTCE } from "../imports/A_0402WGF270JTCE/A_0402WGF270JTCE"
import { A_0402WGF5101TCE } from "../imports/A_0402WGF5101TCE/A_0402WGF5101TCE"
import { A_0402WGF5601TCE } from "../imports/A_0402WGF5601TCE/A_0402WGF5601TCE"
import { A_1N4148WT } from "../imports/A_1N4148WT/A_1N4148WT"
import { CC0402JRNPO9BN270 } from "../imports/CC0402JRNPO9BN270/CC0402JRNPO9BN270"
import { CL05A105KA5NQNC } from "../imports/CL05A105KA5NQNC/CL05A105KA5NQNC"
import { CL05A106MQ5NUNC } from "../imports/CL05A106MQ5NUNC/CL05A106MQ5NUNC"
import { CL05B104KO5NNNC } from "../imports/CL05B104KO5NNNC/CL05B104KO5NNNC"
import { NCP1117LPST33T3G } from "../imports/NCP1117LPST33T3G/NCP1117LPST33T3G"
import { PJ_399B_6A } from "../imports/PJ_399B_6A/PJ_399B_6A"
import { Q22FA23V00418 } from "../imports/Q22FA23V00418/Q22FA23V00418"
import { RB521S30T1G } from "../imports/RB521S30T1G/RB521S30T1G"
import { RP2040 } from "../imports/RP2040/RP2040"
import { SK6812MINI_E } from "../imports/SK6812MINI_E/SK6812MINI_E"
import { TPD4E05U06DQAR } from "../imports/TPD4E05U06DQAR/TPD4E05U06DQAR"
import { TS_1088R_02026 } from "../imports/TS_1088R_02026/TS_1088R_02026"
import { TYPE_C_31_M_13C } from "../imports/TYPE_C_31_M_13C/TYPE_C_31_M_13C"
import { W25Q128JVSIQ } from "../imports/W25Q128JVSIQ/W25Q128JVSIQ"
import { ChocKey } from "./ChocKey"
import corneChocolateLeft from "./generated/corne-chocolate-v4.1-left.layout.json"
import corneChocolateRight from "./generated/corne-chocolate-v4.1-right.layout.json"

export type CorneSide = "left" | "right"

type LayoutElement = Record<string, any> & { type: string }
type ComponentSpec = Record<string, any> & {
  kind: "chip" | "switch" | "resistor" | "capacitor"
  name: string
  layer: "top" | "bottom"
  footprint: LayoutElement[]
}
type CorneLayout = {
  outline: Array<{ x: number; y: number }>
  components: ComponentSpec[]
  connections: Array<{
    name: string
    selectors: string[]
    thickness?: number
  }>
  boardHoles: Array<Record<string, any>>
}

const oscillatorNetNames = new Set([
  "Net_U1_XIN",
  "Net_C2_Pad1",
  "Net_U1_XOUT",
])

const getCanonicalNetName = (name: string) => {
  if (name === "rGND" || name === "GNDR") return "GND"
  if (name.startsWith("rNet_")) return name.slice(1)
  if (name.startsWith("Net_r")) return `Net_${name.slice(5)}`
  return name
}

const getUnprefixedReference = (name: string) =>
  name.startsWith("r") && /^[A-Z]/.test(name.slice(1)) ? name.slice(1) : name

const normalizeRotation = (rotation: number) => {
  const normalized = rotation % 360
  return normalized < 0 ? normalized + 360 : normalized
}

const getJlcComponent = (component: ComponentSpec) => {
  const reference = getUnprefixedReference(component.name)

  if (reference === "J3") return B_2100S04P_A110
  if (reference === "J4") return A_2_54_1_3P_
  if (reference === "J1") return TYPE_C_31_M_13C
  if (reference === "J2") return PJ_399B_6A
  if (reference === "U1") return RP2040
  if (reference === "U2") return NCP1117LPST33T3G
  if (reference === "U3") return TPD4E05U06DQAR
  if (reference === "U4") return W25Q128JVSIQ
  if (reference === "Y1") return Q22FA23V00418
  if (reference === "D1") return A_1N4148WT
  if (reference === "ZD1") return RB521S30T1G
  if (/^SMDSW\d+$/.test(reference)) return TS_1088R_02026
  if (/^(?:EX)?LED\d+$/.test(reference)) return SK6812MINI_E

  if (component.kind === "resistor") {
    const resistance = String(component.resistance).toLowerCase()
    if (resistance === "5.1k" || ["R1", "R2"].includes(reference)) {
      return A_0402WGF5101TCE
    }
    if (resistance === "1k" || ["R3", "R6"].includes(reference)) {
      return A_0402WGF1001TCE
    }
    if (resistance === "27" || ["R4", "R5"].includes(reference)) {
      return A_0402WGF270JTCE
    }
    if (resistance === "10k" || ["R7", "R9"].includes(reference)) {
      return A_0402WGF1002TCE
    }
    if (resistance === "5.6k" || reference === "R8") {
      return A_0402WGF5601TCE
    }
  }

  if (component.kind === "capacitor") {
    const capacitance = String(component.capacitance).toLowerCase()
    if (capacitance === "27p" || ["C1", "C2"].includes(reference)) {
      return CC0402JRNPO9BN270
    }
    if (
      capacitance === "100n" ||
      ["C3", "C4", "C7", "C9", "C10", "C12", "C13", "C14", "C15"].includes(
        reference,
      )
    ) {
      return CL05B104KO5NNNC
    }
    if (capacitance === "1u" || ["C5", "C8", "C16"].includes(reference)) {
      return CL05A105KA5NQNC
    }
    if (capacitance === "10u" || ["C6", "C11"].includes(reference)) {
      return CL05A106MQ5NUNC
    }
  }

  return null
}

const getJlcPlacement = (component: ComponentSpec) => {
  const reference = getUnprefixedReference(component.name)
  let pcbX = component.pcbX
  let pcbY = component.pcbY
  let pcbRotation = component.pcbRotation ?? 0

  // Align vendor footprint datums to the mechanical locations in the KiCad
  // board. These offsets were measured using matching pins, not courtyard
  // centers, so edge-mounted connectors stay flush with the board outline.
  if (reference === "J4") {
    pcbX -= 2.54
    pcbRotation = 0
  } else if (reference === "J1") {
    pcbY -= 3.025
    pcbRotation += 180
  } else if (reference === "J2") {
    pcbX += component.name.startsWith("r") ? 7.7 : -7.7
    pcbRotation -= 90
  } else if (/^(?:EX)?LED\d+$/.test(reference)) {
    // KiCad's bottom-side component angle is mirrored relative to tscircuit's
    // imported component transform. Axis-aligned LEDs look unchanged, but the
    // angled thumb LEDs otherwise lean in the opposite direction.
    pcbRotation = -pcbRotation
  } else if (["U1", "Y1", "D1"].includes(reference)) {
    pcbRotation += 180
  } else if (["U3", "U4"].includes(reference)) {
    pcbRotation -= 90
  }

  // A 0.25 mm shift opens the bottom GND connection beside C5.
  if (component.name === "rC5") pcbX -= 0.25

  return { pcbX, pcbY, pcbRotation: normalizeRotation(pcbRotation) }
}

const expandJlcSelector = (selector: string) => {
  const match = selector.match(/^\.([A-Za-z0-9_]+) > \.([A-Za-z0-9_]+)$/)
  if (!match) return [selector]

  const [, componentName, portName] = match
  const reference = getUnprefixedReference(componentName)
  const select = (port: string) => `.${componentName} > .${port}`

  if (/^EXSW\d+$/.test(reference)) {
    const encoder = `.${componentName}_ENCODER_PADS > .${portName}`
    if (portName === "S1") return [encoder, select("pin2")]
    if (portName === "S2") return [encoder, select("pin1")]
    return [encoder]
  }

  if (reference === "J1" && portName === "S1") {
    return ["EH1", "EH2", "EH3", "EH4"].map(select)
  }

  if (reference === "J2") {
    if (portName === "R") return ["pin2", "pin6"].map(select)
    if (portName === "S") return ["pin1", "pin4"].map(select)
    if (portName === "T") return ["pin3", "pin5"].map(select)
  }

  if (reference === "U2" && portName === "pin2") {
    return ["pin2", "pin4"].map(select)
  }

  return [selector]
}

const renderFootprintElement = (element: LayoutElement, index: number) => {
  const key = `${element.type}-${index}`

  switch (element.type) {
    case "pcb_smtpad": {
      const commonProps = {
        shape: element.shape,
        layer: element.layer,
        portHints: element.port_hints,
        width: element.width,
        height: element.height,
        radius: element.radius,
        ccwRotation: element.ccw_rotation,
        cornerRadius: element.corner_radius,
      } as any

      if (element.shape === "polygon") {
        return (
          <Fragment key={key}>
            <smtpad {...commonProps} points={element.points} />
          </Fragment>
        )
      }
      return (
        <Fragment key={key}>
          <smtpad {...commonProps} pcbX={element.x} pcbY={element.y} />
        </Fragment>
      )
    }
    case "pcb_plated_hole": {
      const shape =
        element.shape === "rotated_pill_hole_with_rect_pad"
          ? "pill_hole_with_rect_pad"
          : element.shape
      return (
        <Fragment key={key}>
          <platedhole
            shape={shape as any}
            pcbX={element.x}
            pcbY={element.y}
            pcbRotation={
              element.hole_ccw_rotation ??
              element.rect_ccw_rotation ??
              element.ccw_rotation
            }
            holeDiameter={element.hole_diameter}
            outerDiameter={element.outer_diameter}
            holeWidth={element.hole_width}
            holeHeight={element.hole_height}
            outerWidth={element.outer_width}
            outerHeight={element.outer_height}
            rectPadWidth={element.rect_pad_width}
            rectPadHeight={element.rect_pad_height}
            holeOffsetX={element.hole_offset_x}
            holeOffsetY={element.hole_offset_y}
            portHints={element.port_hints}
          />
        </Fragment>
      )
    }
    case "pcb_hole":
      return (
        <Fragment key={key}>
          <hole
            shape={element.hole_shape as any}
            diameter={element.hole_diameter}
            width={element.hole_width}
            height={element.hole_height}
            pcbRotation={element.ccw_rotation}
            pcbX={element.x}
            pcbY={element.y}
          />
        </Fragment>
      )
    case "pcb_silkscreen_path":
      return (
        <Fragment key={key}>
          <silkscreenpath
            route={element.route}
            layer={element.layer}
            strokeWidth={element.stroke_width}
          />
        </Fragment>
      )
    case "pcb_silkscreen_text":
      return (
        <Fragment key={key}>
          <silkscreentext
            text={element.text}
            layer={element.layer}
            font={element.font}
            fontSize={element.font_size}
            anchorAlignment={element.anchor_alignment}
            pcbX={element.anchor_position?.x}
            pcbY={element.anchor_position?.y}
            pcbRotation={element.ccw_rotation}
          />
        </Fragment>
      )
    case "pcb_fabrication_note_path":
      return (
        <Fragment key={key}>
          <fabricationnotepath
            route={element.route}
            layer={element.layer}
            strokeWidth={element.stroke_width}
          />
        </Fragment>
      )
    case "pcb_fabrication_note_text":
      return (
        <Fragment key={key}>
          <fabricationnotetext
            text={element.text}
            layer={element.layer}
            font={element.font}
            fontSize={element.font_size}
            anchorAlignment={element.anchor_alignment}
            pcbX={element.anchor_position?.x}
            pcbY={element.anchor_position?.y}
            pcbRotation={element.ccw_rotation}
          />
        </Fragment>
      )
    case "pcb_courtyard_outline":
      return (
        <Fragment key={key}>
          <courtyardoutline
            outline={element.outline}
            layer={element.layer}
          />
        </Fragment>
      )
    default:
      return null
  }
}

const renderComponent = (component: ComponentSpec): React.ReactNode => {
  const reference = getUnprefixedReference(component.name)
  if (/^SW\d+$/.test(reference)) {
    return (
      <ChocKey
        key={component.name}
        name={component.name}
        pcbX={component.pcbX}
        pcbY={component.pcbY}
        pcbRotation={normalizeRotation(-(component.pcbRotation ?? 0))}
      />
    )
  }

  if (/^EXSW\d+$/.test(reference)) {
    // The original footprint supports a Choc socket OR an EC12 encoder.
    // Assemble the exact C5333465 socket. Keep the alternative encoder pads
    // as unpopulated PCB features, with no misleading socket part identity.
    const encoderPads = {
      ...component,
      name: `${component.name}_ENCODER_PADS`,
      doNotPlace: true,
      footprint: component.footprint.filter((e) =>
        e.type !== "pcb_smtpad" &&
        !(e.type === "pcb_hole" && Math.abs(e.hole_diameter - 3) < 0.001) &&
        e.type !== "pcb_courtyard_outline"
      ),
    }
    return (
      <Fragment key={component.name}>
        {renderComponent(encoderPads)}
        <CPG135001S30
          name={component.name}
          pcbX={component.pcbX - 2.5}
          pcbY={component.pcbY - 4.8}
          pcbRotation={0}
          layer="bottom"
          allowOffBoard
          obstructsWithinBounds={false}
        />
      </Fragment>
    )
  }

  const JlcComponent = getJlcComponent(component)
  if (JlcComponent) {
    const placement = getJlcPlacement(component)
    const ImportedComponent = JlcComponent as any
    return (
      <ImportedComponent
        key={component.name}
        name={component.name}
        {...placement}
        layer={component.layer}
        doNotPlace={["J3", "J4"].includes(reference)}
        allowOffBoard
        obstructsWithinBounds={false}
      >
        {reference === "J2" && (
          <>
            <tracehint
              for="port.pin2"
              offsets={[
                { x: 4.57, y: 3.85 },
                { x: 4.57, y: -3.85 },
              ]}
            />
            <tracehint
              for="port.pin3"
              offsets={[
                { x: 4.57, y: 3.8 },
                { x: 4.57, y: -3.8 },
              ]}
            />
          </>
        )}
      </ImportedComponent>
    )
  }

  const footprint = (
    <footprint originalLayer={component.layer}>
      {component.footprint.map(renderFootprintElement)}
    </footprint>
  )
  const commonProps = {
    name: component.name,
    pcbX: component.pcbX,
    pcbY: component.pcbY,
    pcbRotation: component.pcbRotation,
    layer: component.layer,
    footprint,
    doNotPlace: component.doNotPlace || reference === "JP1" || reference.startsWith("LOGO_"),
    supplierPartNumbers: component.supplierPartNumbers,
    manufacturerPartNumber: component.manufacturerPartNumber,
    allowOffBoard: true,
    obstructsWithinBounds: false,
  } as any

  switch (component.kind) {
    case "switch":
      return (
        <switch
          key={component.name}
          {...commonProps}
          pinLabels={component.pinLabels}
        />
      )
    case "resistor":
      return (
        <resistor
          key={component.name}
          {...commonProps}
          resistance={component.resistance ?? "0ohm"}
        />
      )
    case "capacitor":
      return (
        <capacitor
          key={component.name}
          {...commonProps}
          capacitance={component.capacitance ?? "0pF"}
        />
      )
    default:
      return (
        <chip
          key={component.name}
          {...commonProps}
          pinLabels={component.pinLabels}
          noSchematicRepresentation={
            Object.keys(component.pinLabels).length === 0
          }
        />
      )
  }
}

/**
 * Native, autoroutable Corne Chocolate v4.1 half.
 *
 * KiCad contributes component footprints, placements, net connectivity, and
 * the finished Edge.Cuts outline. No KiCad copper trace, via, or zone is used.
 */
const MAIN_MATRIX_BOTTOM_EDGE_Y = -18.608518
const MAIN_MATRIX_SIDE_CORNER_Y = -17.608518
const EDGE_CLEARANCE_EXTENSION_MM = 0.4
const CONTROLLER_NOTCH_INNER_EDGE_X = 54.170004
const CONTROLLER_NOTCH_CLEARANCE_EXTENSION_MM = 1.5

export const CorneHalf = ({ side }: { side: CorneSide }) => {
  const layout = (side === "left"
    ? corneChocolateLeft
    : corneChocolateRight) as CorneLayout
  const outline = layout.outline.map((point) => {
    const adjustedPoint =
      Math.abs(point.y - MAIN_MATRIX_BOTTOM_EDGE_Y) < 0.000001 ||
      Math.abs(point.y - MAIN_MATRIX_SIDE_CORNER_Y) < 0.000001
        ? { ...point, y: point.y - EDGE_CLEARANCE_EXTENSION_MM }
        : point

    return Math.abs(Math.abs(point.x) - CONTROLLER_NOTCH_INNER_EDGE_X) <
      0.000001
      ? {
          ...adjustedPoint,
          x:
            point.x +
            Math.sign(point.x) * CONTROLLER_NOTCH_CLEARANCE_EXTENSION_MM,
        }
      : adjustedPoint
  })

  return (
    <board
      name={`CORNE_CHOCOLATE_V4_1_${side.toUpperCase()}`}
      outline={outline}
      minViaHoleDiameter={0.2}
      minViaPadDiameter={0.45}
      autorouter={{
        algorithmFn: side === "left" ? reviewedLeftAutorouter : reviewedRightAutorouter,
        allowViaInPad: true,
      }}
      autorouterEffortLevel="1x"
      isViaInPadAllowed
      placementDrcChecksDisabled
    >
      <net name="GND" routingPhaseIndex={0} isGroundNet />
      <keepout
        shape="rect"
        pcbX={side === "right" ? 16.25 : -16.25}
        pcbY={-18.758518}
        width={14.5}
        height={0.5}
        layers={["top", "bottom"]}
      />
      {layout.components.map(renderComponent)}
      {layout.boardHoles.map((holeSpec, index) => (
        <Fragment key={`board-hole-${holeSpec.name ?? index}`}>
          <hole
            name={holeSpec.name}
            shape={holeSpec.shape}
            diameter={holeSpec.diameter}
            width={holeSpec.width}
            height={holeSpec.height}
            pcbRotation={holeSpec.pcbRotation}
            pcbX={holeSpec.pcbX}
            pcbY={holeSpec.pcbY}
          />
        </Fragment>
      ))}
      {layout.connections.flatMap((connection, connectionIndex) => {
        const selectors = connection.selectors.flatMap(expandJlcSelector)
        const j2Prefix = side === "right" ? ".rJ2" : ".J2"
        const j2BridgePair = [
          ["pin2", "pin6"],
          ["pin3", "pin5"],
        ]
          .map(([startPin, endPin]) => [
            `${j2Prefix} > .${startPin}`,
            `${j2Prefix} > .${endPin}`,
          ])
          .find(([startSelector, endSelector]) =>
            selectors.includes(startSelector) && selectors.includes(endSelector),
          )

        if (j2BridgePair) {
          const [startSelector, endSelector] = j2BridgePair
          const remainingSelectors = selectors.filter(
            (selector) => !j2BridgePair.includes(selector),
          )

          return [
            <Fragment key={`j2-bridge-${connectionIndex}`}>
              <trace
                routingPhaseIndex={0}
                from={startSelector}
                to={endSelector}
                thickness={connection.thickness ?? 0.15}
              />
            </Fragment>,
            <Fragment key={`j2-signal-${connectionIndex}`}>
              <trace
                routingPhaseIndex={0}
                path={[...remainingSelectors, endSelector]}
                thickness={connection.thickness ?? 0.15}
              />
            </Fragment>,
          ]
        }

        return getCanonicalNetName(connection.name) === "GND"
          ? selectors.map((selector, selectorIndex) => (
              <Fragment key={`ground-${connectionIndex}-${selectorIndex}`}>
                <trace
                  routingPhaseIndex={0}
                  from={selector}
                  to="net.GND"
                  thickness={connection.thickness ?? 0.15}
                />
              </Fragment>
            ))
          : oscillatorNetNames.has(getCanonicalNetName(connection.name))
            ? selectors.slice(1).map((selector, selectorIndex) => (
                <Fragment
                  key={`oscillator-${connectionIndex}-${selectorIndex}`}
                >
                  <trace
                    routingPhaseIndex={0}
                    from={selectors[0]}
                    to={selector}
                    thickness={connection.thickness ?? 0.15}
                  />
                </Fragment>
              ))
            : [
                <Fragment key={`signal-${connectionIndex}`}>
                  <trace
                    routingPhaseIndex={0}
                    path={selectors}
                    thickness={connection.thickness ?? 0.15}
                  />
                </Fragment>,
              ]
      })}
      <copperpour
        name="GND_PLANE"
        layer="bottom"
        connectsTo="net.GND"
        boardEdgeMargin={0.3}
        // Reserve 0.01 mm for the solver's polygon approximation of circles.
        clearance={0.21}
      />
    </board>
  )
}