ShiboSoftwareDev/linux-gameboy-advance

Restores PCB assembly metadata by assigning part numbers and 3D CAD models, removing test-point bodies, and validating top-side SMT solder-paste features without altering copper or pads.

Version
1.0.17
License
unset
Stars
0

scripts/generate-screen-model.mjs

import { writeFile } from "node:fs/promises"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import jscad from "@jscad/modeling"
import { convertJscadModelToGltf } from "jscad-to-gltf"

const { primitives, transforms } = jscad
const { cuboid, roundedCuboid } = primitives
const { rotateX, translate } = transforms

// ER-TFT035IPS-6 capacitive-touch configuration, modeled from the
// manufacturer's outline drawing. The origin is the center of the mating edge
// of the 50-pin FPC so tscircuit can anchor the assembly directly to J_LCD.
const PANEL = {
  width: 55.5,
  height: 84.96,
  thickness: 2.5,
  activeWidth: 48.96,
  activeHeight: 73.44,
  flexWidth: 25.5,
  flexThickness: 0.18,
  contactPitch: 0.5,
  contactWidth: 0.32,
  contactCount: 50,
  stiffenerLength: 5,
  stiffenerThickness: 0.3,
}

const COLORS = {
  active: [0.018, 0.12, 0.17],
  bezel: [0.035, 0.04, 0.05],
  backlightFrame: [0.63, 0.66, 0.68],
  reflector: [0.82, 0.84, 0.85],
  flex: [0.86, 0.47, 0.075],
  flexTrace: [0.48, 0.19, 0.035],
  copper: [0.83, 0.49, 0.12],
  stiffener: [0.16, 0.32, 0.65],
  driver: [0.035, 0.035, 0.04],
}

const geometries = []

const add = (geom, color) => geometries.push({ geom, color })

const box = ({ size, center, color, radius = 0 }) => {
  const shape = radius > 0
    ? roundedCuboid({ size, center, roundRadius: radius, segments: 16 })
    : cuboid({ size, center })
  add(shape, color)
}

const stripBetween = ({ start, end, width, thickness, color }) => {
  const dy = end[1] - start[1]
  const dz = end[2] - start[2]
  const length = Math.hypot(dy, dz)
  const center = [
    (start[0] + end[0]) / 2,
    (start[1] + end[1]) / 2,
    (start[2] + end[2]) / 2,
  ]
  const angle = Math.atan2(dz, dy)
  const strip = cuboid({ size: [width, length, thickness] })
  add(translate(center, rotateX(angle, strip)), color)
}

// The panel is mounted in front of the component side, leaving 7 mm from the
// PCB surface to the metal backlight frame. Its portrait geometry is rotated
// into landscape by the real connector insertion direction in index.circuit.
const panelNearEdgeY = 1
const panelCenterY = panelNearEdgeY + PANEL.height / 2
const panelBackZ = 7

box({
  size: [54.5, 83, 1.55],
  center: [0, panelCenterY, panelBackZ + 0.775],
  color: COLORS.backlightFrame,
  radius: 0.18,
})
box({
  size: [53.7, 82.2, 0.08],
  center: [0, panelCenterY, panelBackZ - 0.05],
  color: COLORS.reflector,
})
box({
  size: [PANEL.width, PANEL.height, 0.78],
  center: [0, panelCenterY, panelBackZ + 1.94],
  color: COLORS.bezel,
  radius: 0.14,
})
box({
  size: [PANEL.activeWidth, PANEL.activeHeight, 0.07],
  center: [0, panelCenterY, panelBackZ + PANEL.thickness + 0.025],
  color: COLORS.active,
  radius: 0.02,
})

// FPC: a low section enters the bottom-contact ZIF, then rises under the
// display and lies against the display back. This represents the real folded
// installation instead of shortening the cable to an artificial 1 mm tab.
const flexPath = [
  [0, 0, 0.32],
  [0, 8, 0.32],
  [0, 10, 1.25],
  [0, 12, 3.65],
  [0, 14, panelBackZ - 0.28],
  [0, 30, panelBackZ - 0.28],
]

for (let i = 0; i < flexPath.length - 1; i += 1) {
  stripBetween({
    start: flexPath[i],
    end: flexPath[i + 1],
    width: PANEL.flexWidth,
    thickness: PANEL.flexThickness,
    color: COLORS.flex,
  })
}

// Copper fingers are on the lower face for the bottom-contact J_LCD socket.
for (let i = 0; i < PANEL.contactCount; i += 1) {
  const x = (i - (PANEL.contactCount - 1) / 2) * PANEL.contactPitch
  box({
    size: [PANEL.contactWidth, PANEL.stiffenerLength, 0.035],
    center: [x, PANEL.stiffenerLength / 2, 0.2125],
    color: COLORS.copper,
  })
  box({
    size: [0.18, 17.5, 0.025],
    center: [x, 21.25, panelBackZ - 0.16],
    color: COLORS.flexTrace,
  })
}

box({
  size: [PANEL.flexWidth, PANEL.stiffenerLength, PANEL.stiffenerThickness],
  center: [0, PANEL.stiffenerLength / 2, 0.56],
  color: COLORS.stiffener,
})

// The bonded driver area and strain-relief strip remain visible from the rear.
box({
  size: [20.5, 2.2, 0.42],
  center: [0, 28.2, panelBackZ - 0.52],
  color: COLORS.driver,
  radius: 0.08,
})
box({
  size: [PANEL.flexWidth, 1.1, 0.32],
  center: [0, 30, panelBackZ - 0.44],
  color: COLORS.flex,
})

const result = await convertJscadModelToGltf(
  { geometries },
  {
    format: "glb",
    meshName: "ER-TFT035IPS-6-realistic-assembly",
    axisTransform: "none",
  },
)

const scriptDir = dirname(fileURLToPath(import.meta.url))
const outputPath = resolve(scriptDir, "../assets/er-tft035ips-6-realistic.glb")
await writeFile(outputPath, new Uint8Array(result.data))
console.log(`wrote ${outputPath} (${result.byteLength} bytes)`)