krishnax12/tensa-zangetsu-keychain-nfc
This code defines a PCB footprint and 3D model for an NFC-enabled electronic component featuring an NXP NT2H1311F0DTLH chip, a PCB copper coil antenna, and a custom-shaped, silkscreen-illustrated metal blade housing.
- Version
- 0.0.3
- License
- unset
- Stars
- 1
dist/index.cjs
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var objPath = require('././assets/NT2H1311F0DTLH-1e41f0db.obj');
var stepPath = require('././assets/NT2H1311F0DTLH-ce4c1152.step');
const pinLabels = {
pin1: ["GND"],
pin2: ["LB"],
pin3: ["FD"],
pin4: ["LA"],
pin5: ["EP"]
};
const pinAttributes = {
pin1: { requiresGround: true }
};
const NT2H1311F0DTLH = (props) => {
return (jsxRuntime.jsx("chip", { pinLabels: pinLabels, pinAttributes: pinAttributes, supplierPartNumbers: {
"jlcpcb": [
"C2654853"
]
}, manufacturerPartNumber: "NT2H1311F0DTLH", footprint: jsxRuntime.jsxs("footprint", { children: [jsxRuntime.jsx("smtpad", { portHints: ["pin1"], pcbX: "-0.249936mm", pcbY: "-0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsxRuntime.jsx("smtpad", { portHints: ["pin2"], pcbX: "0.249936mm", pcbY: "-0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsxRuntime.jsx("smtpad", { portHints: ["pin3"], pcbX: "0.249936mm", pcbY: "0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsxRuntime.jsx("smtpad", { portHints: ["pin4"], pcbX: "-0.249936mm", pcbY: "0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsxRuntime.jsx("smtpad", { portHints: ["pin5"], pcbX: "0mm", pcbY: "0mm", width: "0.999998mm", height: "0.7999984mm", shape: "rect" }), jsxRuntime.jsx("silkscreenpath", { route: [{ "x": -0.7500112000000172, "y": 0.999998000000005 }, { "x": -0.7500112000000172, "y": -0.999998000000005 }] }), jsxRuntime.jsx("silkscreenpath", { route: [{ "x": -0.6061455999999907, "y": -0.999998000000005 }, { "x": -0.7500112000000172, "y": -0.999998000000005 }] }), jsxRuntime.jsx("silkscreenpath", { route: [{ "x": 0.7500111999999035, "y": 0.999998000000005 }, { "x": 0.7500111999999035, "y": -0.999998000000005 }, { "x": 0.6061455999999907, "y": -0.999998000000005 }] }), jsxRuntime.jsx("silkscreenpath", { route: [{ "x": 0.6061455999999907, "y": 0.999998000000005 }, { "x": 0.7500111999999035, "y": 0.999998000000005 }] }), jsxRuntime.jsx("silkscreenpath", { route: [{ "x": -0.7500112000000172, "y": 0.999998000000005 }, { "x": -0.6061455999999907, "y": 0.999998000000005 }] }), jsxRuntime.jsx("silkscreencircle", { pcbX: "-0.700024mm", pcbY: "-1.400048mm", radius: "0.100076mm" }), jsxRuntime.jsx("silkscreentext", { text: "{NAME}", pcbX: "-0.0381mm", pcbY: "2.3462mm", anchorAlignment: "center", fontSize: "1mm" }), jsxRuntime.jsx("courtyardoutline", { outline: [{ "x": -1.0627999999999247, "y": 1.5962000000000671 }, { "x": 0.9865999999999531, "y": 1.5962000000000671 }, { "x": 0.9865999999999531, "y": -1.7485999999998967 }, { "x": -1.0627999999999247, "y": -1.7485999999998967 }, { "x": -1.0627999999999247, "y": 1.5962000000000671 }] })] }), cadModel: {
objUrl: objPath,
stepUrl: stepPath,
pcbRotationOffset: 0,
modelOriginPosition: { x: 0, y: 0.000012700000070253736, z: -0.02 },
}, ...props }));
};
const points = (values) => values.map(([x, y]) => ({ x, y }));
// Existing board outline — unchanged.
const outline = points([
[18.58, 113.94],
[7.5, 103.67],
[1.79, 96.82],
[0.49, 94.87],
[0.49, 93.4],
[-0.65, 92.91],
[-3.1, 88.51],
[-5.71, 81.66],
[-8.8, 63.08],
[-9.45, 54.77],
[-9.45, 52.81],
[-8.8, 51.83],
[-9.62, 50.86],
[-9.94, 45.97],
[-8.8, 44.01],
[-9.94, 43.52],
[-10.11, 42.54],
[-10.6, 32.76],
[-10.6, 31.79],
[-8.8, 30.8],
[-10.76, 29.34],
[-11.08, 12.71],
[-10.27, 11.25],
[-11.57, 10.76],
[-14.51, 5.38],
[-16.14, 3.91],
[-16.3, 3.42],
[-16.14, -4.89],
[-15.16, -7.33],
[-15.49, -9.78],
[-14.51, -15.65],
[-15, -16.95],
[-12.06, -18.91],
[-7.66, -20.86],
[-8.48, -13.69],
[-9.62, -11.74],
[-8.8, -10.43],
[-8.96, -3.91],
[-3.91, -0.82],
[-2.28, -1.3],
[-1.47, -40.1],
[0.82, -40.75],
[3.42, -39.77],
[1.79, -13.37],
[1.79, -1.63],
[1.96, -0.65],
[8.31, 2.93],
[10.6, 6.19],
[9.45, 6.52],
[10.92, 7.5],
[10.6, 11.9],
[8.31, 12.39],
[8.31, 13.37],
[10.43, 13.86],
[10.27, 16.79],
[7.01, 17.28],
[9.29, 17.77],
[9.29, 20.21],
[7.33, 20.7],
[7.33, 21.19],
[9.94, 21.68],
[9.94, 24.61],
[9.29, 25.59],
[9.94, 27.06],
[9.94, 34.88],
[9.29, 35.37],
[10.11, 38.3],
[7.66, 38.79],
[7.66, 39.28],
[8.64, 40.75],
[10.27, 41.24],
[10.43, 47.11],
[9.62, 47.6],
[10.6, 49.55],
[10.92, 54.44],
[8.8, 54.93],
[8.96, 57.87],
[11.41, 60.8],
[12.88, 76.94],
[12.22, 81.83],
[12.55, 96.01],
[13.2, 96.5],
[15, 103.83],
[18.58, 113.61],
]);
// Existing shaped guard opening — unchanged, unplated.
const guardOpening = points([
[-13.6, -10.4],
[-12.9, -9.8],
[-11.3, -11],
[-10.2, -14.2],
[-9.9, -17.6],
[-12.1, -16.4],
[-13.1, -15.4],
[-13.7, -11.1],
]);
function Silk({ route, width = 0.25 }) {
return (jsxRuntime.jsx("silkscreenpath", { layer: "top", strokeWidth: width, route: points(route) }));
}
// Long white core with a pointed shoulder.
const core = [
[-2.9, 1.8],
[-2.7, 28],
[-2.1, 51],
[-1.2, 66],
[1.2, 74],
[3.3, 66],
[2.6, 51],
[2.3, 28],
[2.4, 4.7],
[-2.9, 1.8],
];
// Overlapping silkscreen strokes create a solid fill.
function FilledCore() {
const rows = [];
for (let y = 2; y < 73.8; y += 0.16) {
const xs = [];
for (let i = 0; i < core.length - 1; i++) {
const [ax, ay] = core[i];
const [bx, by] = core[i + 1];
if ((ay <= y && by > y) || (by <= y && ay > y)) {
xs.push(ax + ((y - ay) * (bx - ax)) / (by - ay));
}
}
xs.sort((a, b) => a - b);
if (xs.length === 2 && xs[1] - xs[0] > 0.2) {
rows.push(jsxRuntime.jsx(Silk, { route: [
[xs[0] + 0.09, y],
[xs[1] - 0.09, y],
], width: 0.32 }, y.toFixed(2)));
}
}
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: rows });
}
// Left guard solid silkscreen fill.
function FilledLeftGuard() {
const inside = (x, y, polygon) => {
let result = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const a = polygon[i];
const b = polygon[j];
if (a.y > y !== b.y > y &&
x < a.x + ((b.x - a.x) * (y - a.y)) / (b.y - a.y)) {
result = !result;
}
}
return result;
};
const edgeDistance = (x, y, polygon) => {
let nearest = Infinity;
for (let i = 0; i < polygon.length; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % polygon.length];
const dx = b.x - a.x;
const dy = b.y - a.y;
const t = Math.max(0, Math.min(1, ((x - a.x) * dx + (y - a.y) * dy) / (dx * dx + dy * dy || 1)));
nearest = Math.min(nearest, Math.hypot(x - a.x - t * dx, y - a.y - t * dy));
}
return nearest;
};
const rows = [];
for (let y = -20.5; y <= -3.2; y += 0.12) {
let start = null;
for (let x = -16.5; x <= -7.2; x += 0.08) {
const valid = inside(x, y, outline) &&
!inside(x, y, guardOpening) &&
edgeDistance(x, y, outline) >= 0.55 &&
edgeDistance(x, y, guardOpening) >= 0.55;
if (valid && start === null)
start = x;
if (!valid && start !== null) {
if (x - 0.08 > start) {
rows.push(jsxRuntime.jsx(Silk, { route: [
[start, y],
[x - 0.08, y],
], width: 0.3 }, `${y}-${start}`));
}
start = null;
}
}
}
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: rows });
}
// Angular white panel borders adapted from the reference.
const panels = [
// Tip facets.
[
[17.7, 112.2],
[3.1, 96.2],
[7.5, 93.1],
[17.7, 112.2],
],
[
[2.5, 95.4],
[-4.4, 81.7],
[-7.1, 68.1],
[-1.9, 63.6],
[1.2, 73],
[2.2, 83.3],
[7.5, 92.3],
[2.5, 95.4],
],
[
[2.2, 83.3],
[5.7, 79.2],
[11, 83.4],
[11.7, 95.3],
[17.7, 112.2],
],
[
[1.9, 81.8],
[1.5, 74.8],
[4.8, 68.2],
[8.9, 73.2],
[10.7, 81.7],
[5.7, 77.6],
[1.9, 81.8],
],
// Left blade panels.
[
[-7.5, 66.3],
[-8.2, 55],
[-8.6, 40.2],
[-3.5, 39.8],
[-2.8, 61.5],
[-7.5, 66.3],
],
[
[-8.6, 39],
[-9.1, 31.8],
[-9.5, 13],
[-10.5, 10.3],
[-5, 6.5],
[-3.7, 14],
[-3.5, 39],
[-8.6, 39],
],
// Right blade panels.
[
[4.4, 65.4],
[3.5, 40.2],
[7.2, 40.2],
[7.2, 54],
[9.4, 60.6],
[8.9, 71],
[4.4, 65.4],
],
[
[3.5, 39],
[3.3, 15],
[7.1, 10.1],
[6.2, 17.5],
[6.5, 29],
[7, 39],
[3.5, 39],
],
// Guard facets.
[
[-10.8, 9.4],
[-14.7, 3.4],
[-10.5, -0.2],
[-4.1, 4.5],
[-10.8, 9.4],
],
[
[-9.8, -1],
[-3.8, 3.4],
[7.7, 8.8],
[3.3, 13.5],
[3.3, 6],
[-3.3, 1.2],
[-9.8, -2.7],
[-9.8, -8.5],
],
[
[-15, 2],
[-15, -4.6],
[-14, -7.5],
],
[
[-14.2, 1.3],
[-14.2, -4.4],
[-13.5, -6.9],
],
// Border around the shaped guard opening.
[
[-14.3, -9.1],
[-13, -8.3],
[-10.4, -10.5],
[-9.2, -14],
[-8.8, -18.9],
[-12.7, -17.3],
[-14, -15.8],
[-14.3, -9.1],
],
];
// Follow the existing handle taper with inset silkscreen.
function gripPoint(t, side) {
const y = -2.2 - 36.4 * t;
const left = -2.28 + 0.81 * ((-y - 1.3) / 38.8) + 0.45;
const right = (y >= -13.37 ? 1.79 : 1.79 + 1.63 * ((-y - 13.37) / 26.4)) - 0.45;
return [left + (right - left) * side, y];
}
// Passive NFC prototype. Tune the fabricated coil before production.
// U1: NXP NT2H1311F0DTL, NTAG213F, HXSON4.
// NXP datasheet: https://www.nxp.com/docs/en/data-sheet/NTAG213F_216F.pdf
// L1 is PCB copper, NOT an additional component to purchase.
// Its 2.6 uH schematic value is a TARGET, not a measured inductance.
const coilWidth = 0.25;
const coilPitch = 0.65;
const coilTurns = 3;
const coilOrigin = [1.65, 56.5];
const coilBoundary = [
[-6.7, 14],
[-6, 40],
[-4.6, 66],
[-1, 82],
[10, 99],
[8.4, 78],
[7.6, 66],
[5.1, 44],
[5.6, 14],
];
// Offset each polygon edge inward, then intersect adjacent offset lines.
function insetLoop(poly, distance) {
const area = poly.reduce((sum, a, i) => {
const b = poly[(i + 1) % poly.length];
return sum + a[0] * b[1] - b[0] * a[1];
}, 0);
const sign = area > 0 ? 1 : -1;
const edges = poly.map((a, i) => {
const b = poly[(i + 1) % poly.length];
const dx = b[0] - a[0], dy = b[1] - a[1], length = Math.hypot(dx, dy);
return {
p: [
a[0] - ((sign * dy) / length) * distance,
a[1] + ((sign * dx) / length) * distance,
],
d: [dx, dy],
};
});
return edges.map((edge, i) => {
const previous = edges[(i + edges.length - 1) % edges.length];
const cross = (a, b) => a[0] * b[1] - a[1] * b[0];
const delta = [edge.p[0] - previous.p[0], edge.p[1] - previous.p[1]];
const t = cross(delta, edge.d) / cross(previous.d, edge.d);
return [
previous.p[0] + t * previous.d[0],
previous.p[1] + t * previous.d[1],
];
});
}
const antennaRoute = Array.from({ length: coilTurns }, (_, turn) => {
const loop = insetLoop(coilBoundary, turn * coilPitch);
const baseY = 14 + turn * coilPitch;
return [[-1.5, baseY], ...loop, [1.5, baseY]];
}).flat();
function NfcCircuit() {
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(NT2H1311F0DTLH, { name: "U1", pcbX: 0, pcbY: 10, layer: "bottom", schX: 0, schY: 0, pinLabels: { pin1: "GND", pin2: "LB", pin3: "FD", pin4: "LA" }, pinAttributes: {
pin1: { providesGround: true, mustBeConnected: true },
pin2: { mustBeConnected: true },
pin3: { isUsingOpenDrain: true, mustBeConnected: true },
pin4: { mustBeConnected: true },
} }), jsxRuntime.jsx("inductor", { name: "L1", inductance: "2.6uH", pcbX: coilOrigin[0], pcbY: coilOrigin[1], allowOffBoard: true, schX: -4, schY: 0, footprint: jsxRuntime.jsxs("footprint", { children: [jsxRuntime.jsx("smtpad", { shape: "circle", radius: 0.275, pcbX: -1.5 - coilOrigin[0], pcbY: 14 - coilOrigin[1], layer: "bottom", portHints: ["pin1"] }), jsxRuntime.jsx("platedhole", { shape: "circle", holeDiameter: 0.3, outerDiameter: 0.6, pcbX: 1.5 - coilOrigin[0], pcbY: 15.3 - coilOrigin[1], portHints: ["pin2"] }), jsxRuntime.jsx("pcbtrace", { route: antennaRoute.map(([x, y]) => ({
route_type: "wire",
x: x - coilOrigin[0],
y: y - coilOrigin[1],
width: coilWidth,
layer: "bottom",
})) })] }) }), jsxRuntime.jsx("trace", { name: "RF_A", from: ".U1 > .LA", to: ".L1 > .pin1", thickness: 0.2 }), jsxRuntime.jsx("trace", { name: "RF_B", from: ".U1 > .LB", to: ".L1 > .pin2", thickness: 0.2 }), jsxRuntime.jsx("trace", { name: "FD_GND", from: ".U1 > .GND", to: ".U1 > .FD", thickness: 0.15 })] }));
}
/** Passive NFC blade prototype: no battery, no copper planes. RF tuning required. */
const ZangetsuNfcBlade = () => (jsxRuntime.jsxs("board", { width: "38mm", pcbStyle: { viaHoleDiameter: 0.3, viaPadDiameter: 0.6 }, height: "160mm", solderMaskColor: "black", silkscreenColor: "white", outline: outline, children: [jsxRuntime.jsx("cutout", { shape: "polygon", points: guardOpening }), jsxRuntime.jsx(NfcCircuit, {}), jsxRuntime.jsx(FilledCore, {}), jsxRuntime.jsx(FilledLeftGuard, {}), jsxRuntime.jsx(Silk, { route: core, width: 0.2 }), panels.map((route, i) => (jsxRuntime.jsx(Silk, { route: route }, `panel-${i}`))), jsxRuntime.jsx(Silk, { route: [
gripPoint(0, 0),
gripPoint(1, 0),
gripPoint(1, 1),
gripPoint((13.37 - 2.2) / 36.4, 1),
gripPoint(0, 1),
gripPoint(0, 0),
] }), Array.from({ length: 14 }, (_, i) => (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(Silk, { route: [gripPoint(i / 14, 0), gripPoint((i + 1) / 14, 1)], width: 0.2 }), jsxRuntime.jsx(Silk, { route: [gripPoint(i / 14, 1), gripPoint((i + 1) / 14, 0)], width: 0.2 })] }, `wrap-${i}`))), jsxRuntime.jsx(Silk, { route: [gripPoint(0.02, 0), gripPoint(0.02, 1)] }), jsxRuntime.jsx(Silk, { route: [gripPoint(0.98, 0), gripPoint(0.98, 1)] })] }));
const ZangetsuRefOutlinePCB = ZangetsuNfcBlade;
const TensaZangetsu = ZangetsuNfcBlade;
exports.TensaZangetsu = TensaZangetsu;
exports.ZangetsuNfcBlade = ZangetsuNfcBlade;
exports.ZangetsuRefOutlinePCB = ZangetsuRefOutlinePCB;
exports.default = ZangetsuNfcBlade;