gokul/usb3-sd-hub
Engineering draft: four-layer, computer-powered USB 3 hub with one USB-C host socket, two USB-C and two USB-A accessory sockets, and a full-size SD slot. Board and three schematic previews are available. Routing and production checks are incomplete; not fabrication ready.
- Version
- 0.1.5
- License
- MIT
- Stars
- 0
design/paired-paths.ts
import type { FanoutTracePath } from "@tscircuit/props";
export type Point = { x: number; y: number };
export type NumericPath = Omit<FanoutTracePath, "route"> & {
route: Array<FanoutTracePath["route"][number] & Point>;
};
export const traceWidthMm = 0.136;
export const halfCenterGapMm = (traceWidthMm + 0.15) / 2;
export function wire(
point: Point,
layer: "top" | "bottom" = "bottom",
): NumericPath["route"][number] {
return { ...point, route_type: "wire", width: traceWidthMm, layer };
}
export function via(
point: Point,
fromLayer: "top" | "bottom",
): NumericPath["route"][number] {
return {
...point,
route_type: "via",
from_layer: fromLayer,
to_layer: fromLayer === "top" ? "bottom" : "top",
via_diameter: 0.45,
via_hole_diameter: 0.2,
};
}
// Intersect the adjacent offset lines. The centre path contains straight/45-degree
// segments; miter offsets retain the same 0.286 mm perpendicular centre spacing.
export function offsetPath({
points,
offsetMm,
}: { points: Point[]; offsetMm: number }): Point[] {
const segments = points.slice(1).map((end, index) => {
const start = points[index];
const dx = end.x - start.x;
const dy = end.y - start.y;
const length = Math.hypot(dx, dy);
if (length === 0) throw new Error("Duplicate centre-path point");
return {
start: {
x: start.x + (dy / length) * offsetMm,
y: start.y - (dx / length) * offsetMm,
},
end: {
x: end.x + (dy / length) * offsetMm,
y: end.y - (dx / length) * offsetMm,
},
dx,
dy,
};
});
return [
segments[0].start,
...segments.slice(1).map((next, index) => {
const previous = segments[index];
const determinant = previous.dx * next.dy - previous.dy * next.dx;
if (Math.abs(determinant) < 1e-9) return previous.end;
const fraction =
((next.start.x - previous.start.x) * next.dy -
(next.start.y - previous.start.y) * next.dx) /
determinant;
return {
x: previous.start.x + fraction * previous.dx,
y: previous.start.y + fraction * previous.dy,
};
}),
segments.at(-1)!.end,
];
}
export function copperLengthMm(path: NumericPath): number {
return path.route.slice(1).reduce((sum, end, index) => {
const start = path.route[index];
return (
sum +
(start.route_type === "wire" &&
end.route_type === "wire" &&
start.layer === end.layer
? Math.hypot(end.x - start.x, end.y - start.y)
: 0)
);
}, 0);
}
// Spread compensation over separated, short excursions. Every excursion must be
// checked for coupling and clearance on the complete board after it is inserted.
export function matchOnHorizontal({
paths,
startXsMm,
awayDirection,
}: {
paths: NumericPath[];
startXsMm: number[];
awayDirection: [number, number];
}): NumericPath[] {
const lengths = paths.map(copperLengthMm);
const shorterIndex = lengths[0] < lengths[1] ? 0 : 1;
const differenceMm = Math.abs(lengths[0] - lengths[1]);
if (differenceMm <= 0.1) return paths;
const count = Math.max(1, Math.ceil(differenceMm / 0.8));
if (count > startXsMm.length)
throw new Error("Not enough clear compensation regions");
const cornerMm = 0.1;
const depthMm = (differenceMm / count + 4 * cornerMm * (2 - Math.SQRT2)) / 2;
return paths.map((path, index) => {
if (index !== shorterIndex) return path;
const starts = startXsMm.slice(0, count);
const endIndex = path.route.findIndex((point, routeIndex) => {
const previous = path.route[routeIndex - 1];
return (
previous?.route_type === "wire" &&
point.route_type === "wire" &&
point.layer === "bottom" &&
previous.layer === "bottom" &&
Math.abs(point.y - previous.y) < 1e-8 &&
previous.x < starts[0] &&
point.x > starts.at(-1)! + 0.7
);
});
if (endIndex < 0) throw new Error("Horizontal compensation region missing");
const y = path.route[endIndex].y;
const direction = awayDirection[index];
const compensation = starts.flatMap((x) => [
wire({ x, y }),
wire({ x: x + 0.1, y: y + direction * cornerMm }),
wire({ x: x + 0.1, y: y + direction * (depthMm - cornerMm) }),
wire({ x: x + 0.2, y: y + direction * depthMm }),
wire({ x: x + 0.5, y: y + direction * depthMm }),
wire({ x: x + 0.6, y: y + direction * (depthMm - cornerMm) }),
wire({ x: x + 0.6, y: y + direction * cornerMm }),
wire({ x: x + 0.7, y }),
]);
return {
...path,
route: [
...path.route.slice(0, endIndex),
...compensation,
...path.route.slice(endIndex),
],
};
});
}