load_success = (api) => {
api.start(function () {
api.addEventListener("viewerready", function () {
api.getNodeMap(function (err, nodes) {
const hip = getNodeByName(nodes, hip_ID);
const thigh = getNodeByName(nodes, thigh_ID);
const gear1 = getNodeByName(nodes, gear1_ID);
const gear2 = getNodeByName(nodes, gear2_ID);
const lever = getNodeByName(nodes, lever_ID);
const calf = getNodeByName(nodes, calf_ID);
const paw = getNodeByName(nodes, paw_ID);
(function initialize() {
const wireframeEnabled = wireframe.value;
api.setWireframe(wireframeEnabled);
updateOpacity();
cubeVisibility();
updateMatrix();
})();
wireframe.addEventListener('input', function (event) {
const wireframeEnabled = wireframe.value;
api.setWireframe(wireframeEnabled);
});
Hide_cube.addEventListener('input', cubeVisibility);
function cubeVisibility() {
const visibilityValue = (Hide_cube.value || animation_loop.value)? 0 : 1;
api.getMaterialList(function (err, materials) {
const cubeMaterial_1 = materials.find(material => material.name === "cube");
const cubeMaterial_2 = materials.find(material => material.name === "cube_red");
const cubeMaterial_3 = materials.find(material => material.name === "cube_green");
const cubeMaterial_4 = materials.find(material => material.name === "cube_blue");
cubeMaterial_1.channels["Opacity"].factor = visibilityValue;
cubeMaterial_2.channels["Opacity"].factor = visibilityValue;
cubeMaterial_3.channels["Opacity"].factor = visibilityValue;
cubeMaterial_4.channels["Opacity"].factor = visibilityValue;
api.setMaterial(cubeMaterial_1);
api.setMaterial(cubeMaterial_2);
api.setMaterial(cubeMaterial_3);
api.setMaterial(cubeMaterial_4);
})
}
// Retrieve the materials from the model and set opacity of the thigh
opacity.addEventListener('input', updateOpacity);
function updateOpacity() {
const opacityValue = opacity.value;
api.getMaterialList(function (err, materials) {
const thighMaterial_1 = materials.find(material => material.name === "Thigh");
const thighMaterial_2 = materials.find(material => material.name === "thigh_protection");
thighMaterial_1.channels["Opacity"].factor = opacityValue;
thighMaterial_2.channels["Opacity"].factor = opacityValue;
api.setMaterial(thighMaterial_1);
api.setMaterial(thighMaterial_2);
})
}
// 3D model is reactive to the slider value changes
[position, theta_1_A, theta_2_A, theta_3_A, x_cube_B, y_cube_B, z_cube_B].forEach(slider => {
slider.addEventListener('input', updateMatrix);
});
// For IK, we recalculate the transformation on mouse events with a slight delay.
// This ensures the 3D model updates correctly, even if the slider is clicked rather than dragged.
[x_cube_B, y_cube_B, z_cube_B].forEach(slider => {
slider.addEventListener("mouseup", () => {
setTimeout(updateMatrix, 100); // Adjust the delay (ms) as needed
});
slider.addEventListener("change", updateMatrix); // Improves the behavior if the slider is changed via text input
});
function updateMatrix() {
const pos_A = (position.value == "A")? 1: 0;
const T01_cube = pos_A? T01(theta_1_A.value) : T01(theta_1_B.value);
const T12_cube = pos_A? T12(theta_2_A.value) : T12(theta_2_B.value);
const T02_cube = multiplyMatrices4x4(T01_cube,T12_cube);
const T23_cube = pos_A? T23(theta_3_A.value) : T23(theta_3_B.value);
const T03_cube = multiplyMatrices4x4(T02_cube,T23_cube);
const T04 = multiplyMatrices4x4(T03_cube,T34);
pos_A? mutable T04_cube_A = T04 : mutable T04_cube_B = T04;
const angle1 = pos_A? -degToRad(theta_1_A.value) : -degToRad(theta_1_B.value);
const angle2 = pos_A? -degToRad(theta_2_A.value) : -degToRad(theta_2_B.value);
const angle3 = pos_A? -degToRad(theta_3_A.value) : -degToRad(theta_3_B.value);
// Orientation of the 3D model differs from notebook: (x,y,z) -> (z,x,y) <=> (0,1,2) -> (2,0,1)
translateNode(api, paw.instanceID, [mm_to_inch*T04[2][3], mm_to_inch*T04[0][3], mm_to_inch*T04[1][3]]);
// Hip rotation (Joint J1): Action on the node "hip" affects all children objects.
rotateNode(api, hip.instanceID, angle1, 1, 0, 0);
// Thigh rotation (Joint J2): Action on the node "thigh" affects all children objects.
rotateNode(api, thigh.instanceID, angle2, 0, 1, 0);
// Calf rotation (Joint J3): The mechanism consists of
// (i) Two gears (gear 1, gear 2 / gear ratio 2:1); (ii) Lever; (iii) Calf
// Workaround for GLB format limitation:
// The lever is defined as a child of gear 2.
// However, we need to compensate for the rotation, as we only want to translate the lever.
rotateNode(api, lever.instanceID, -angle3, 0, 1, 0);
rotateNode(api, gear1.instanceID, -2 * angle3, 0, 1, 0);
rotateNode(api, gear2.instanceID, angle3, 0, 1, 0);
rotateNode(api, calf.instanceID, angle3, 0, 1, 0);
}
// Event listener to start the animation loop
animation_loop.addEventListener('input', function (event) {
cubeVisibility(); // We hide/show the cube
animation(); // We start/stop the animation loop
});
// Animation loop setup
const n = 150;
let step = 0; // Declare step at the top level for scope visibility
let isAnimating = false; // Flag to control the animation loop
function animation() {
// Start the animation only if not already running
if (!isAnimating) {
isAnimating = true;
const startTime = performance.now();
const animate = (currentTime) => {
// Check if the animation_loop is still active
if (animation_loop.value) {
// Calculate elapsed time and progress
const elapsedTime = currentTime - startTime;
const progress = (elapsedTime / (n * 15)) % 1;
// Calculate angles based on the current step
const angle1 = updateTheta(theta_1_A.value, theta_1_B.value, progress);
const angle2 = updateTheta(theta_2_A.value, theta_2_B.value, progress);
const angle3 = updateTheta(theta_3_A.value, theta_3_B.value, progress);
// Apply transformations
applyTransformations(angle1, angle2, angle3);
// Continue the animation loop
requestAnimationFrame(animate);
} else {
// Stop the animation when animation_loop is unchecked
isAnimating = false;
updateMatrix(); // At the end of animation, reset the model to selected position (A or B)
}
};
// Start the animation
requestAnimationFrame(animate);
}
}
// Function to update theta with smooth behavior
function updateTheta(theta_A, theta_B, progress) {
let phase = (progress < 0.5) ? 2 * progress : 2 * (1 - progress);
let easingValue = 3 * Math.pow(phase, 2) - 2 * Math.pow(phase, 3);
return -degToRad(theta_A + (theta_B - theta_A) * easingValue);
}
// Function to apply transformations to the 3D model
function applyTransformations(angle1, angle2, angle3) {
rotateNode(api, hip.instanceID, angle1, 1, 0, 0);
rotateNode(api, thigh.instanceID, angle2, 0, 1, 0);
rotateNode(api, lever.instanceID, -angle3, 0, 1, 0);
rotateNode(api, gear1.instanceID, -2 * angle3, 0, 1, 0);
rotateNode(api, gear2.instanceID, angle3, 0, 1, 0);
rotateNode(api, calf.instanceID, angle3, 0, 1, 0);
}
});
});
});
};