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.js

import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import React from 'react';
import objPath from '././assets/NT2H1311F0DTLH-1e41f0db.obj';
import stepPath from '././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 (jsx("chip", { pinLabels: pinLabels, pinAttributes: pinAttributes, supplierPartNumbers: {
            "jlcpcb": [
                "C2654853"
            ]
        }, manufacturerPartNumber: "NT2H1311F0DTLH", footprint: jsxs("footprint", { children: [jsx("smtpad", { portHints: ["pin1"], pcbX: "-0.249936mm", pcbY: "-0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsx("smtpad", { portHints: ["pin2"], pcbX: "0.249936mm", pcbY: "-0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsx("smtpad", { portHints: ["pin3"], pcbX: "0.249936mm", pcbY: "0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsx("smtpad", { portHints: ["pin4"], pcbX: "-0.249936mm", pcbY: "0.999998mm", width: "0.2500122mm", height: "0.6999986mm", shape: "rect" }), jsx("smtpad", { portHints: ["pin5"], pcbX: "0mm", pcbY: "0mm", width: "0.999998mm", height: "0.7999984mm", shape: "rect" }), jsx("silkscreenpath", { route: [{ "x": -0.7500112000000172, "y": 0.999998000000005 }, { "x": -0.7500112000000172, "y": -0.999998000000005 }] }), jsx("silkscreenpath", { route: [{ "x": -0.6061455999999907, "y": -0.999998000000005 }, { "x": -0.7500112000000172, "y": -0.999998000000005 }] }), jsx("silkscreenpath", { route: [{ "x": 0.7500111999999035, "y": 0.999998000000005 }, { "x": 0.7500111999999035, "y": -0.999998000000005 }, { "x": 0.6061455999999907, "y": -0.999998000000005 }] }), jsx("silkscreenpath", { route: [{ "x": 0.6061455999999907, "y": 0.999998000000005 }, { "x": 0.7500111999999035, "y": 0.999998000000005 }] }), jsx("silkscreenpath", { route: [{ "x": -0.7500112000000172, "y": 0.999998000000005 }, { "x": -0.6061455999999907, "y": 0.999998000000005 }] }), jsx("silkscreencircle", { pcbX: "-0.700024mm", pcbY: "-1.400048mm", radius: "0.100076mm" }), jsx("silkscreentext", { text: "{NAME}", pcbX: "-0.0381mm", pcbY: "2.3462mm", anchorAlignment: "center", fontSize: "1mm" }), 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 (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(jsx(Silk, { route: [
                    [xs[0] + 0.09, y],
                    [xs[1] - 0.09, y],
                ], width: 0.32 }, y.toFixed(2)));
        }
    }
    return jsx(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(jsx(Silk, { route: [
                            [start, y],
                            [x - 0.08, y],
                        ], width: 0.3 }, `${y}-${start}`));
                }
                start = null;
            }
        }
    }
    return jsx(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 (jsxs(Fragment, { children: [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 },
                } }), jsx("inductor", { name: "L1", inductance: "2.6uH", pcbX: coilOrigin[0], pcbY: coilOrigin[1], allowOffBoard: true, schX: -4, schY: 0, footprint: jsxs("footprint", { children: [jsx("smtpad", { shape: "circle", radius: 0.275, pcbX: -1.5 - coilOrigin[0], pcbY: 14 - coilOrigin[1], layer: "bottom", portHints: ["pin1"] }), jsx("platedhole", { shape: "circle", holeDiameter: 0.3, outerDiameter: 0.6, pcbX: 1.5 - coilOrigin[0], pcbY: 15.3 - coilOrigin[1], portHints: ["pin2"] }), jsx("pcbtrace", { route: antennaRoute.map(([x, y]) => ({
                                route_type: "wire",
                                x: x - coilOrigin[0],
                                y: y - coilOrigin[1],
                                width: coilWidth,
                                layer: "bottom",
                            })) })] }) }), jsx("trace", { name: "RF_A", from: ".U1 > .LA", to: ".L1 > .pin1", thickness: 0.2 }), jsx("trace", { name: "RF_B", from: ".U1 > .LB", to: ".L1 > .pin2", thickness: 0.2 }), 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 = () => (jsxs("board", { width: "38mm", pcbStyle: { viaHoleDiameter: 0.3, viaPadDiameter: 0.6 }, height: "160mm", solderMaskColor: "black", silkscreenColor: "white", outline: outline, children: [jsx("cutout", { shape: "polygon", points: guardOpening }), jsx(NfcCircuit, {}), jsx(FilledCore, {}), jsx(FilledLeftGuard, {}), jsx(Silk, { route: core, width: 0.2 }), panels.map((route, i) => (jsx(Silk, { route: route }, `panel-${i}`))), 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) => (jsxs(React.Fragment, { children: [jsx(Silk, { route: [gripPoint(i / 14, 0), gripPoint((i + 1) / 14, 1)], width: 0.2 }), jsx(Silk, { route: [gripPoint(i / 14, 1), gripPoint((i + 1) / 14, 0)], width: 0.2 })] }, `wrap-${i}`))), jsx(Silk, { route: [gripPoint(0.02, 0), gripPoint(0.02, 1)] }), jsx(Silk, { route: [gripPoint(0.98, 0), gripPoint(0.98, 1)] })] }));
const ZangetsuRefOutlinePCB = ZangetsuNfcBlade;
const TensaZangetsu = ZangetsuNfcBlade;

export { TensaZangetsu, ZangetsuNfcBlade, ZangetsuRefOutlinePCB, ZangetsuNfcBlade as default };