mohan-bee/tscircuit-robot-vacuum

A tscircuit package created by mohan-bee.

Version
1.0.0
License
unset
Stars
0

robot-mechanical.tsx

PCBPCB preview for robot-mechanical.tsx
SchematicSchematic preview for robot-mechanical.tsx
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>
    </>}
  </>
}