346 lines
14 KiB
JavaScript
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")] });
|
|
}
|
|
};
|