mohan-bee/tscircuit-robot-vacuum
A tscircuit package created by mohan-bee.
- Version
- 1.0.0
- License
- unset
- Stars
- 0
robot-mechanical.tsx
PCB
Schematic
import React from "react"
import { assembly, jscad } from "tscircuit"
/** All dimensions are mm in PCB world: board centre plane z=0; +Y is front.
* Printed parts are authored in tscircuit's native JSCAD JSX. Purchased items
* are deliberately named ENVELOPE: their mounting details are not verified.
*/
export const robotDimensions = {
diameter: 320,
pcbWidth: 160,
pcbHeight: 130,
pcbHoleX: 72,
pcbHoleY: 52,
floorTopZ: -30,
floorBottomZ: -33,
floorContactZ: -52.5,
lidTopZ: 35,
overallTopZ: 58,
overallHeight: 110.5,
wall: 2.4,
} as const
export const pcbMountingHoles = [-72, 72].flatMap((x) =>
[-52, 52].map((y) => ({ x, y })),
)
/** Native CSG tessellation workaround: offset cylinder facet seams from the
* cardinal cut planes. Without this, the native mesh loader produced duplicate
* near-coincident seam vertices on these two compound solids. This changes only
* the polygonal phase of round features; no exported mesh is repaired. */
function StableCylinder({ radius, height, center = [0, 0, 0] }: {
radius: number; height: number; center?: [number, number, number]
}) {
return <jscad.translate offset={center}>
<jscad.rotate angles={[0, 0, 0.017]}>
<jscad.cylinder radius={radius} height={height} />
</jscad.rotate>
</jscad.translate>
}
function Chassis() {
return (
<>
<jscad.subtract>
<jscad.union>
{/* Floor and continuous outer wall; top is open for servicing. */}
<jscad.subtract>
<StableCylinder radius={159} height={60} center={[0, 0, -3]} />
<StableCylinder radius={156.6} height={60} center={[0, 0, 0]} />
</jscad.subtract>
{pcbMountingHoles.map(({ x, y }) => (
<StableCylinder key={`${x},${y}`} radius={4.5} height={29.2}
center={[x, y, -15.4]} />
))}
{/* Four lid screws: bosses are independent of PCB standoffs. */}
{[-1, 1].flatMap((sx) => [-1, 1].map((sy) => (
<StableCylinder key={`${sx},${sy}`} radius={5.5} height={60}
center={[sx * 101, sy * 101, -3]} />
)))}
</jscad.union>
{/* Wheel pockets extend through the floor and outer wall. */}
{[-130, 130].map((x) => (
<jscad.cuboid key={x} size={[64, 80, 72]} center={[x + Math.sign(x) * 11, 0, -20]} />
))}
{/* Roller access: allows removal from the underside. */}
<jscad.cuboid size={[144, 46, 44]} center={[0, 90, -36]} />
{/* Clearance apertures for the separately printed underside air raceway. */}
<jscad.cuboid size={[25.8, 182.8, 8]} center={[-95, 12, -31]} />
<jscad.cuboid size={[55.8, 14.8, 8]} center={[-80, -69.6, -31]} />
{/* Side brush spindle and front caster mounting opening. */}
<StableCylinder radius={4} height={12} center={[-120, 80, -30]} />
<StableCylinder radius={6} height={12} center={[0, 135, -30]} />
{/* Bare downward-facing cliff sensor apertures. */}
{[[-112, 105], [112, 105], [-100, -115], [100, -115]].map(([x, y]) => (
<jscad.cuboid key={`${x},${y}`} size={[13, 9, 12]} center={[x, y, -30]} />
))}
{pcbMountingHoles.map(({ x, y }) => (
<StableCylinder key={`b${x},${y}`} radius={1.25} height={34}
center={[x, y, -14]} />
))}
{[-1, 1].flatMap((sx) => [-1, 1].map((sy) => (
<StableCylinder key={`l${sx},${sy}`} radius={1.5} height={66}
center={[sx * 101, sy * 101, -2]} />
)))}
{/* Rear exhaust slots. No CFD or finger-protection claim. */}
{[-36, -24, -12, 0, 12, 24, 36].map((x) => (
<jscad.cuboid key={x} size={[7, 20, 18]} center={[x, -153, 12]} />
))}
</jscad.subtract>
<jscad.translate offset={[0, 0, -0.8]}>
<jscad.rectangle name="pcb" size={[160, 130]} reference />
</jscad.translate>
</>
)
}
function Lid() {
return (
<jscad.subtract>
<jscad.union>
<jscad.cylinder radius={160} height={3} center={[0, 0, 33.5]} />
<jscad.subtract>
<jscad.cylinder radius={156.1} height={5} center={[0, 0, 29.5]} />
<jscad.cylinder radius={153.7} height={7} center={[0, 0, 29.5]} />
</jscad.subtract>
</jscad.union>
<jscad.cylinder radius={35} height={14} center={[0, 15, 32]} />
{[-1, 1].flatMap((sx) => [-1, 1].map((sy) => (
<jscad.cylinder key={`${sx},${sy}`} radius={1.7} height={12}
center={[sx * 101, sy * 101, 32]} />
)))}
{/* Finger opening over bin; bin lid itself remains a separate part. */}
<jscad.cuboid size={[70.8, 54.8, 12]} center={[-43, -106, 32]} />
</jscad.subtract>
)
}
function Bin() {
return (
<jscad.subtract>
<jscad.cuboid size={[70, 54, 29]} center={[-43, -106, 16.5]} />
<jscad.cuboid size={[65.2, 49.2, 31]} center={[-43, -106, 19.4]} />
{/* Dirty-air inlet in forward side, clean-air outlet in right side. */}
<jscad.cuboid size={[25, 8, 13]} center={[-65, -79, 15]} />
<jscad.cuboid size={[8, 30, 17]} center={[-8, -106, 16]} />
</jscad.subtract>
)
}
function BinLid() {
return (
<jscad.union>
<jscad.cuboid size={[70, 54, 3]} center={[-43, -106, 33.5]} />
<jscad.subtract>
<jscad.cuboid size={[64.7, 48.7, 2]} center={[-43, -106, 31]} />
<jscad.cuboid size={[60, 44, 4]} center={[-43, -106, 31]} />
</jscad.subtract>
</jscad.union>
)
}
function DirtyAirDuct() {
return (
<jscad.subtract>
<jscad.union>
{/* Three joined rectangular passages; deliberately inspectable. */}
<jscad.cuboid size={[125, 30, 22]} center={[-45, 90, 1.4]} />
<jscad.cuboid size={[25, 30, 45]} center={[-95, 90, -22.5]} />
<jscad.cuboid size={[25, 182, 15]} center={[-95, 12, -37.5]} />
<jscad.cuboid size={[55, 14, 61]} center={[-80, -69.6, -14.5]} />
</jscad.union>
<jscad.union>
<jscad.cuboid size={[125, 25.2, 17.2]} center={[-42.6, 90, 1.4]} />
<jscad.cuboid size={[20.2, 25.2, 45]} center={[-95, 90, -20.1]} />
<jscad.cuboid size={[20.2, 182, 10.2]} center={[-95, 12, -37.5]} />
<jscad.cuboid size={[50.2, 9.2, 61]} center={[-80, -69.6, -12.1]} />
</jscad.union>
</jscad.subtract>
)
}
function RollerShroud() {
return (
<jscad.subtract>
<jscad.cuboid size={[144, 46, 26]} center={[0, 90, -23]} />
<jscad.cuboid size={[138, 40, 28]} center={[0, 90, -26]} />
<jscad.cuboid size={[120, 25, 10]} center={[0, 90, -10]} />
<jscad.translate offset={[0, 90, -32]}>
<jscad.rotate angles={[0, Math.PI / 2, 0]}>
<jscad.cylinder radius={3.2} height={150} />
</jscad.rotate>
</jscad.translate>
</jscad.subtract>
)
}
function CleanAirAdapter() {
return <jscad.subtract>
<jscad.cuboid size={[56.6, 35, 23]} center={[20.7, -106, 16]} />
<jscad.cuboid size={[60, 30.2, 18.2]} center={[20.7, -106, 16]} />
</jscad.subtract>
}
function CasterCup() {
return <jscad.subtract>
<jscad.union>
<StableCylinder radius={18} height={3} center={[0, 135, -28.5]} />
<StableCylinder radius={14} height={11} center={[0, 135, -34]} />
</jscad.union>
<StableCylinder radius={12} height={12} center={[0, 135, -38]} />
<StableCylinder radius={5.5} height={24} center={[0, 135, -34]} />
{[-1, 1].map((sign) => <StableCylinder key={sign} radius={1.7} height={8}
center={[sign * 15.5, 135, -28.5]} />)}
</jscad.subtract>
}
function WheelHub({ x }: { x: number }) {
return (
<jscad.translate offset={[x, 0, -20]}>
<jscad.rotate angles={[0, Math.PI / 2, 0]}>
<jscad.subtract>
<jscad.cylinder radius={28} height={16} />
<jscad.cylinder radius={3.1} height={20} />
{[0, 1, 2, 3, 4, 5].map((i) => (
<jscad.cylinder key={i} radius={6} height={20}
center={[17 * Math.cos(i * Math.PI / 3), 17 * Math.sin(i * Math.PI / 3), 0]} />
))}
</jscad.subtract>
</jscad.rotate>
</jscad.translate>
)
}
function WheelBracket({ x }: { x: number }) {
const sign = Math.sign(x)
return (
<jscad.subtract>
<jscad.union>
<jscad.cuboid size={[32, 56, 4]} center={[x - sign * 25, 0, -28]} />
<jscad.cuboid size={[4, 48, 27]} center={[x - sign * 16, 0, -16.5]} />
</jscad.union>
<jscad.translate offset={[x - sign * 16, 0, -20]}>
<jscad.rotate angles={[0, Math.PI / 2, 0]}>
<jscad.cylinder radius={6} height={12} />
</jscad.rotate>
</jscad.translate>
{[-18, 18].map((y) => (
<jscad.cylinder key={y} radius={1.7} height={12}
center={[x - sign * 28, y, -28]} />
))}
</jscad.subtract>
)
}
function BatteryTray() {
return (
<jscad.subtract>
<jscad.cuboid size={[110, 65, 23]} center={[0, -80, -19]} />
<jscad.cuboid size={[106, 61, 25]} center={[0, -80, -16]} />
{[-32, 32].map((x) => (
<jscad.cuboid key={x} size={[13, 70, 3.2]} center={[x, -80, -27]} />
))}
</jscad.subtract>
)
}
function Bumper() {
return (
<jscad.subtract>
<jscad.cylinder radius={164} height={18} center={[0, 0, -5]} />
<jscad.cylinder radius={160.8} height={22} center={[0, 0, -5]} />
<jscad.cuboid size={[340, 200, 24]} center={[0, -60, -5]} />
</jscad.subtract>
)
}
/** Geometry-only purchased part envelope. This does not assert a supplier model. */
function Envelope({ name, size, center, shape = "box", rotation }: {
name: string; size: [number, number, number]; center: [number, number, number];
shape?: "box" | "cylinder"; rotation?: [number, number, number]
}) {
const primitive = shape === "cylinder"
? { type: "cylinder", radius: size[0] / 2, height: size[2], segments: 48 }
: { type: "cuboid", size }
return <assembly.part name={name} cadModel={{
jscad: rotation ? { type: "rotate", angles: rotation, shape: primitive } : primitive,
positionOffset: { x: center[0], y: center[1], z: center[2] },
}} />
}
export function RobotMechanicals({ includeExplosion = true }: { includeExplosion?: boolean }) {
return <>
<assembly.printedpart name="CHASSIS" displayName="Open chassis / 3mm floor"
material="petg" color="#344252" jscad={<Chassis />} />
<assembly.printedpart name="TOP_COVER" displayName="Removable top cover"
material="petg" color="#e6e9ed" jscad={<Lid />} />
<assembly.printedpart name="DUSTBIN" displayName="Removable dirty-air bin"
material="petg" color="#a7c6d7" jscad={<Bin />} />
<assembly.printedpart name="BIN_LID" material="petg" color="#b9cfdb" jscad={<BinLid />} />
<assembly.printedpart name="DIRTY_AIR_DUCT" displayName="Dirty air passage; prototype"
material="petg" color="#da9f48" jscad={<DirtyAirDuct />} />
<assembly.printedpart name="CLEAN_AIR_ADAPTER" displayName="Filter-to-blower concept adapter"
material="petg" color="#6ba4ad" jscad={<CleanAirAdapter />} />
<assembly.printedpart name="ROLLER_SHROUD" material="petg" color="#44596b" jscad={<RollerShroud />} />
<assembly.printedpart name="CASTER_CUP" material="petg" color="#708396" jscad={<CasterCup />} />
<assembly.printedpart name="BATTERY_CRADLE" material="petg" color="#5c6b76" jscad={<BatteryTray />} />
<assembly.printedpart name="FRONT_BUMPER" material="petg" color="#efb644" jscad={<Bumper />} />
{[-130, 130].map((x) => <React.Fragment key={x}>
<assembly.printedpart name={x < 0 ? "LEFT_WHEEL_HUB" : "RIGHT_WHEEL_HUB"}
material="petg" color="#586775" jscad={<WheelHub x={x} />} />
<assembly.printedpart name={x < 0 ? "LEFT_MOTOR_BRACKET" : "RIGHT_MOTOR_BRACKET"}
material="petg" color="#768b97" jscad={<WheelBracket x={x} />} />
<Envelope name={x < 0 ? "LEFT_TIRE_ENVELOPE" : "RIGHT_TIRE_ENVELOPE"}
size={[65, 65, 18]} center={[x, 0, -20]} shape="cylinder" rotation={[0, Math.PI / 2, 0]} />
<Envelope name={x < 0 ? "LEFT_GEARMOTOR_ENVELOPE" : "RIGHT_GEARMOTOR_ENVELOPE"}
size={[25, 25, 36]} center={[x - Math.sign(x) * 36, 0, -16]}
shape="cylinder" rotation={[0, Math.PI / 2, 0]} />
</React.Fragment>)}
<Envelope name="PROTECTED_3S_BATTERY_ENVELOPE" size={[105, 60, 20]} center={[0, -80, -18]} />
<Envelope name="BLDC_BLOWER_ENVELOPE" size={[50, 50, 26]} center={[70, -105, 16]}
shape="cylinder" />
<Envelope name="RPLIDAR_C1_APPROX_ENVELOPE" size={[66, 66, 28]} center={[0, 15, 44]}
shape="cylinder" />
<Envelope name="ROLLER_BRISTLE_ENVELOPE" size={[40, 40, 120]} center={[0, 90, -32]}
shape="cylinder" rotation={[0, Math.PI / 2, 0]} />
<Envelope name="ROLLER_MOTOR_ENVELOPE" size={[22, 22, 32]} center={[88, 90, -19]}
shape="cylinder" rotation={[0, Math.PI / 2, 0]} />
<Envelope name="SIDE_BRUSH_MOTOR_ENVELOPE" size={[22, 22, 25]} center={[-120, 80, -18]}
shape="cylinder" />
<Envelope name="CASTER_BALL_ENVELOPE" size={[23, 23, 23]} center={[0, 135, -41]}
shape="cylinder" />
<Envelope name="REPLACEABLE_FILTER_ENVELOPE" size={[3, 30, 19]} center={[-10, -106, 16]} />
<assembly.printedpart name="SIDE_BRUSH_HUB" material="petg" color="#e3aa4b" jscad={
<jscad.subtract>
<jscad.cylinder radius={10} height={5} center={[-120, 80, -45]} />
<jscad.cylinder radius={1.6} height={9} center={[-120, 80, -45]} />
</jscad.subtract>
} />
{/* Purchased bristle bundles; 3 arms shown as thin envelopes. */}
{[0, 1, 2].map((i) => <Envelope key={i} name={`SIDE_BRUSH_BRISTLES_${i}_ENVELOPE`}
size={[42, 5, 3]} center={[-120 + 21 * Math.cos(i * 2 * Math.PI / 3), 80 + 21 * Math.sin(i * 2 * Math.PI / 3), -51]}
rotation={[0, 0, i * 2 * Math.PI / 3]} />)}
{includeExplosion && <>
<assembly.explosion.stage name="open-top" nextStageRef="remove-bin">
<assembly.explosion.move targetRefs={["TOP_COVER", "BIN_LID", "RPLIDAR_C1_APPROX_ENVELOPE"]} offZ={100} />
</assembly.explosion.stage>
<assembly.explosion.stage name="remove-bin" nextStageRef="show-drive">
<assembly.explosion.move targetRefs={["DUSTBIN", "REPLACEABLE_FILTER_ENVELOPE"]} offZ={60} />
</assembly.explosion.stage>
<assembly.explosion.stage name="show-drive">
<assembly.explosion.move targetRefs={["LEFT_WHEEL_HUB", "LEFT_TIRE_ENVELOPE"]} offX={-65} />
<assembly.explosion.move targetRefs={["RIGHT_WHEEL_HUB", "RIGHT_TIRE_ENVELOPE"]} offX={65} />
</assembly.explosion.stage>
</>}
</>
}