Files
cadSet/text-to-cad/models/robots/qdd_actuator/.qdd_actuator.step.js
T
2026-07-22 19:38:36 +08:00

346 lines
14 KiB
JavaScript

const Z_AXIS = [0, 0, 1];
const ORIGIN = [0, 0, 0];
// Gear-stage constants mirrored from qdd_actuator.py (module 1.0, fixed ring).
const SUN_TEETH = 24;
const PLANET_TEETH = 30;
const RING_TEETH = 84;
// Fixed-ring planetary reduction: carrier = sun / (1 + Nr/Ns) = sun / 4.5.
const REDUCTION_RATIO = 1 + (RING_TEETH / SUN_TEETH);
const PLANET_CENTER_R = 27;
const PLANET_ANGLES_DEG = [90, 210, 330];
// 4.5 sun revolutions per cycle returns the carrier to exactly 360 deg. The
// rotor/sun end the cycle 180 deg from start, which is invisible: every
// rotating part is 180-deg symmetric (28 alternating magnets = 14 pole pairs,
// 24/30-tooth gears, 6x lightening + 4x coupling holes), so the loop is
// seamless at tooth/pole phase.
const DRIVE_CYCLE_DEG = REDUCTION_RATIO * 360;
// Rotor-bearing ball cage runs near half the inner-race speed; 4/9 (~0.444 vs
// the physical ~0.452 for this geometry) makes the 10-ball ring land exactly
// on its 36-deg symmetry at the end of the drive cycle.
const BALL_CAGE_RATIO = 4 / 9;
// Cross-roller cage: outer race fixed, inner race at carrier speed -> cage at
// half carrier speed. 16 alternating-tilt rollers repeat every 45 deg, and
// (360 / 2) = 180 deg per cycle closes on that symmetry.
const ROLLER_CAGE_RATIO = 0.5;
// Documented static exploded layout from qdd_actuator.py EXPLODE_OFFSETS
// (mm along Z at explode = 1), verified pairwise non-overlapping there.
const EXPLODE_OFFSETS_MM = {
connectorPower: -82,
connectorSignal: -82,
rearCover: -64,
driverPcb: -46,
encoderPcb: -32,
housing: 0,
encoderMagnetRing: 70,
rearBearing: 78,
stator: 94,
rotor: 128,
frontBearing: 154,
ringGear: 168,
sunGear: 196,
planetGear: 222,
carrier: 250,
crossRollerBearing: 280,
frontRetainer: 298,
retainerScrews: 312,
torqueSensor: 330,
outputFlange: 348,
cableTube: 360
};
const PLANET_EXPLODE_RADIAL_MM = 14;
// The viewer floor sits at the assembled model's lowest geometry (the rear
// connectors). Rebase the explosion so the lowest station is 0: every part
// explodes upward or stays put, relative spacing is unchanged, and nothing
// sinks through the floor. The rear connectors keep their assembled height.
const FLOOR_LIFT_MM = -Math.min(...Object.values(EXPLODE_OFFSETS_MM));
// "Keep gear mesh" station: ring, sun, and planets explode together as one
// meshed cluster so the tooth engagement stays watchable; the carrier still
// lifts to its own documented station so its plate does not cover the mesh.
// The cluster spans z 6.8..20.5 mm; 210 keeps >60 mm clearance to the front
// bearing station (154) below and ~28 mm to the carrier station (250) above.
const GEAR_STAGE_CLUSTER_MM = 210;
const PLANETS = [
{ id: "planet1", angleDeg: PLANET_ANGLES_DEG[0] },
{ id: "planet2", angleDeg: PLANET_ANGLES_DEG[1] },
{ id: "planet3", angleDeg: PLANET_ANGLES_DEG[2] }
];
const SHELL_PARTS = [
"housing",
"frontRetainer",
"retainerScrews",
"rearCover",
"connectorPower",
"connectorSignal"
];
const ELECTRONICS_PARTS = ["driverPcb", "encoderPcb"];
const OUTPUT_STACK_PARTS = [
"xrollerInner",
"xrollerOuter",
"xrollerRollers",
"torqueSensor",
"outputFlange"
];
const DRIVE_TRAIN_PARTS = ["sunGear", "planet1", "planet2", "planet3", "carrier"];
function finite(value, fallback = 0) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : fallback;
}
function clamp(value, min, max) {
return Math.min(Math.max(finite(value, min), min), max);
}
function clamp01(value) {
return clamp(value, 0, 1);
}
function wrap01(cycle) {
return ((finite(cycle, 0) % 1) + 1) % 1;
}
function planetCenter(angleDeg) {
const angleRad = angleDeg * Math.PI / 180;
return [
PLANET_CENTER_R * Math.cos(angleRad),
PLANET_CENTER_R * Math.sin(angleRad),
14.5
];
}
function radialUnit(angleDeg) {
const angleRad = angleDeg * Math.PI / 180;
return [Math.cos(angleRad), Math.sin(angleRad), 0];
}
function applyVisibility(effects, params) {
effects.visible("*", true);
effects.visible(SHELL_PARTS, params.showShell !== false);
effects.visible(ELECTRONICS_PARTS, params.showElectronics !== false);
effects.visible(OUTPUT_STACK_PARTS, params.showOutputStack !== false);
}
export default {
manifest: {
schemaVersion: 1,
step: {
path: "models/robots/qdd_actuator/qdd_actuator.step"
},
label: "QDD actuator",
description: "Quasi-direct-drive actuator with viewer-time drivetrain animation: the outrunner rotor spins the sun gear, three planets walk the fixed ring at the derived 4.5:1 reduction, and the carrier drives the torque sensor and output flange.",
units: {
length: "mm",
angle: "deg",
time: "s"
},
features: {
housing: { ref: "#o1.1", label: "Housing", description: "Fixed finned aluminum housing." },
frontRetainer: { ref: "#o1.2", label: "Front retainer ring" },
rearCover: { ref: "#o1.3", label: "Rear cover" },
retainerScrews: { ref: "#o1.4", label: "Retainer screws" },
connectorPower: { ref: "#o1.5", label: "Power connector" },
connectorSignal: { ref: "#o1.6", label: "Signal connector" },
driverPcb: { ref: "#o1.7", label: "Driver PCB" },
encoderPcb: { ref: "#o1.8", label: "Encoder PCB" },
encoderMagnetRing: { ref: "#o1.9", label: "Encoder magnet ring", description: "Spins with the rotor for the rear magnetic encoder." },
stator: { ref: "#o1.10", label: "Stator", description: "Fixed 24-slot stator and windings." },
rotor: { ref: "#o1.11", label: "Rotor", description: "Outrunner rotor bell and 28-magnet ring; the animation input." },
rearBearingInner: { ref: "#o1.12.1", label: "Rear bearing inner race" },
rearBearingOuter: { ref: "#o1.12.2", label: "Rear bearing outer race" },
rearBearingBalls: { ref: "#o1.12.3", label: "Rear bearing balls" },
frontBearingInner: { ref: "#o1.13.1", label: "Front bearing inner race" },
frontBearingOuter: { ref: "#o1.13.2", label: "Front bearing outer race" },
frontBearingBalls: { ref: "#o1.13.3", label: "Front bearing balls" },
sunGear: { ref: "#o1.14", label: "Sun gear (24T)", description: "Bolted to the rotor flange; rotates at input speed.", axis: Z_AXIS, origin: ORIGIN },
ringGear: { ref: "#o1.15", label: "Ring gear (84T)", description: "Fixed internal gear pressed into the housing bore." },
planet1: { ref: "#o1.16", label: "Planet gear 1 (30T)", axis: Z_AXIS, origin: planetCenter(PLANET_ANGLES_DEG[0]) },
planet2: { ref: "#o1.17", label: "Planet gear 2 (30T)", axis: Z_AXIS, origin: planetCenter(PLANET_ANGLES_DEG[1]) },
planet3: { ref: "#o1.18", label: "Planet gear 3 (30T)", axis: Z_AXIS, origin: planetCenter(PLANET_ANGLES_DEG[2]) },
carrier: { ref: "#o1.19", label: "Planet carrier", description: "Output member at 1/4.5 of input speed.", axis: Z_AXIS, origin: ORIGIN },
xrollerInner: { ref: "#o1.20.1", label: "Cross-roller inner ring", description: "Rotates with the output stage." },
xrollerOuter: { ref: "#o1.20.2", label: "Cross-roller outer ring", description: "Fixed in the housing seat." },
xrollerRollers: { ref: "#o1.20.3", label: "Cross-roller rollers" },
torqueSensor: { ref: "#o1.21", label: "Torque sensor flexure", description: "Strain-gauge flexure between carrier and flange." },
outputFlange: { ref: "#o1.22", label: "Output flange" },
cableTube: { ref: "#o1.23", label: "Cable tube", description: "Static hollow-shaft cable conduit." }
},
parameters: {
drive: {
type: "number",
label: "Drive",
description: "Rotor/sun input angle over one closed mesh cycle (4.5 input revolutions = 1 carrier revolution).",
default: 0,
min: 0,
max: DRIVE_CYCLE_DEG,
step: 1,
unit: "deg"
},
explode: {
type: "number",
label: "Explode",
description: "Axial technical explosion from the documented build123d layout, applied after the gear motion.",
default: 0,
min: 0,
max: 1,
step: 0.01
},
keepGearMesh: {
type: "boolean",
label: "Keep gear mesh",
description: "Explode the ring, sun, and planets together as one meshed cluster instead of separate stations; the carrier still lifts clear of the mesh.",
default: true
},
showShell: { type: "boolean", label: "Housing shell", default: true },
showElectronics: { type: "boolean", label: "Electronics", default: true },
showOutputStack: { type: "boolean", label: "Output stack", default: true },
highlightDrive: { type: "boolean", label: "Highlight gear train", default: false }
},
animations: {
driveLoop: {
label: "Drive 4.5:1 reduction",
description: "Continuous rotor input: sun at input speed, planets walking the fixed ring, carrier and output flange at 1/4.5 speed.",
duration: 12,
loop: true,
update({ cycle, set }) {
set("drive", wrap01(cycle) * DRIVE_CYCLE_DEG);
}
},
inspectExplode: {
label: "Exploded drive inspection",
description: "Same gear cycle while the stack separates into exploded stations and returns; the planetary stage stays meshed while 'Keep gear mesh' is on.",
duration: 12,
loop: true,
update({ cycle, set }) {
const phase = wrap01(cycle);
set("drive", phase * DRIVE_CYCLE_DEG);
set("explode", Math.sin(phase * Math.PI));
}
}
}
},
update({ params, effects }) {
const drive = finite(params.drive);
const explode = clamp01(params.explode);
const carrierAngle = drive / REDUCTION_RATIO;
// Mesh-consistent planet spin about its own moving axis, relative to the
// carrier frame: external sun/planet mesh reverses the relative rotation.
const planetSpinRelativeToCarrier = -(SUN_TEETH / PLANET_TEETH) * (drive - carrierAngle);
const ballOrbitAngle = drive * BALL_CAGE_RATIO;
const rollerOrbitAngle = carrierAngle * ROLLER_CAGE_RATIO;
const lift = (key) => (EXPLODE_OFFSETS_MM[key] + FLOOR_LIFT_MM) * explode;
const keepGearMesh = params.keepGearMesh !== false;
const gearLift = (key) => (keepGearMesh ? (GEAR_STAGE_CLUSTER_MM + FLOOR_LIFT_MM) * explode : lift(key));
const planetRadialMm = keepGearMesh ? 0 : PLANET_EXPLODE_RADIAL_MM;
applyVisibility(effects, params);
if (params.highlightDrive === true) {
effects.highlight(DRIVE_TRAIN_PARTS, true);
}
// Input group: rotor bell + magnets, encoder target ring, sun gear.
effects.transform("rotor", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive },
translate: [0, 0, lift("rotor")]
});
effects.transform("encoderMagnetRing", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive },
translate: [0, 0, lift("encoderMagnetRing")]
});
effects.transform("sunGear", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive },
translate: [0, 0, gearLift("sunGear")]
});
// Rotor support bearings: inner races spin with the hub, ball rings orbit
// at cage speed, outer races stay seated in the housing sleeve.
for (const [inner, balls, outer, key] of [
["rearBearingInner", "rearBearingBalls", "rearBearingOuter", "rearBearing"],
["frontBearingInner", "frontBearingBalls", "frontBearingOuter", "frontBearing"]
]) {
effects.transform(inner, {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive },
translate: [0, 0, lift(key)]
});
effects.transform(balls, {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: ballOrbitAngle },
translate: [0, 0, lift(key)]
});
effects.transform(outer, { translate: [0, 0, lift(key)] });
}
// Planets: spin about their own (initial) axis, separate radially in the
// exploded view, then orbit the sun axis with the carrier. Steps compose
// in order in the fixed model frame, so the radial offset and orbit keep
// the planets pointing outward at the current carrier angle.
for (const planet of PLANETS) {
const radial = radialUnit(planet.angleDeg);
effects.transform(planet.id, {
transforms: [
{
rotate: {
axis: Z_AXIS,
origin: planetCenter(planet.angleDeg),
angleDeg: planetSpinRelativeToCarrier
}
},
{
translate: [
radial[0] * planetRadialMm * explode,
radial[1] * planetRadialMm * explode,
gearLift("planetGear")
]
},
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } }
]
});
}
// Output group at carrier speed: carrier, torque sensor, flange, and the
// cross-roller inner ring; the roller ring orbits at cage speed.
effects.transform("carrier", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle },
translate: [0, 0, lift("carrier")]
});
effects.transform("torqueSensor", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle },
translate: [0, 0, lift("torqueSensor")]
});
effects.transform("outputFlange", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle },
translate: [0, 0, lift("outputFlange")]
});
effects.transform("xrollerInner", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle },
translate: [0, 0, lift("crossRollerBearing")]
});
effects.transform("xrollerRollers", {
rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: rollerOrbitAngle },
translate: [0, 0, lift("crossRollerBearing")]
});
effects.transform("xrollerOuter", { translate: [0, 0, lift("crossRollerBearing")] });
// Static parts only take their exploded-station offsets.
effects.transform("housing", { translate: [0, 0, lift("housing")] });
effects.transform("ringGear", { translate: [0, 0, gearLift("ringGear")] });
effects.transform("stator", { translate: [0, 0, lift("stator")] });
effects.transform("frontRetainer", { translate: [0, 0, lift("frontRetainer")] });
effects.transform("retainerScrews", { translate: [0, 0, lift("retainerScrews")] });
effects.transform("rearCover", { translate: [0, 0, lift("rearCover")] });
effects.transform("connectorPower", { translate: [0, 0, lift("connectorPower")] });
effects.transform("connectorSignal", { translate: [0, 0, lift("connectorSignal")] });
effects.transform("driverPcb", { translate: [0, 0, lift("driverPcb")] });
effects.transform("encoderPcb", { translate: [0, 0, lift("encoderPcb")] });
effects.transform("cableTube", { translate: [0, 0, lift("cableTube")] });
}
};