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),
			],
		};
	});
}