Public
Edited
Aug 22, 2024
Paused
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
x_cube_A.value = x_cube_A_value
Insert cell
Insert cell
y_cube_A.value = y_cube_A_value
Insert cell
Insert cell
z_cube_A.value = z_cube_A_value
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
theta_1_B.value = theta_1_B_IK[IK_solution_B.indices[0]]
Insert cell
Insert cell
theta_2_B.value = theta_2_B_IK[IK_solution_B.indices[1]]
Insert cell
Insert cell
theta_3_B.value = theta_3_B_IK[IK_solution_B.indices[2]]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
position = Inputs.radio(new Map([["A (FK)", "A"], ["B (IK)", "B"] ]), {label: htl.html`<b>Position</b>`, value: "A"})
Insert cell
theta_1_A = Inputs.range([MinAngle1, MaxAngle1], {value: 0, step: 1, label: htl.html` &nbsp; &nbsp; <b>Hip</b> (J1), θ<sub>1</sub> [°]`})
Insert cell
theta_2_A = Inputs.range([MinAngle2, MaxAngle2], {value: -40, step: 1, label: htl.html`<b>Thigh</b> (J2), θ<sub>2</sub> [°]`})
Insert cell
theta_3_A = Inputs.range([MinAngle3, MaxAngle3], {value: 20, step: 1, label: htl.html`&nbsp;&nbsp; <b>Calf</b> (J3), θ<sub>3</sub> [°]`})
Insert cell
x_cube_A = Inputs.range([xMin_A, xMax_A], {value: 0, step: 0.1, label: htl.html`&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <b>x</b> [mm]`, disabled: true})
Insert cell
y_cube_A = Inputs.range([yMin_A, yMax_A], {value: 0, step: 0.1, label: htl.html`&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <b>y</b> [mm]`, disabled: true})
Insert cell
z_cube_A = Inputs.range([zMin_A, zMax_A], {value: 0, step: 0.1, label: htl.html`&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <b>z</b> [mm]`, disabled: true})
Insert cell
x_cube_B = Inputs.range([xMin_B, xMax_B], {value: L4, step: 1, label: htl.html`&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <b>x</b> [mm]`})

Insert cell
y_cube_B = Inputs.range([yMin_B, yMax_B], {value: L2, step: 1, label: htl.html`&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <b>y</b> [mm]`})
Insert cell
z_cube_B = Inputs.range([zMin_B, zMax_B], {value: L1+L3, step: 1, label: htl.html`&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <b>z</b> [mm]`})
Insert cell
theta_1_B = Inputs.range([MinAngle1, MaxAngle1], {value: 0, step: 0.1, label: htl.html` &nbsp; &nbsp; <b>Hip</b> (J1), θ<sub>1</sub> [°]`, disabled: true})
Insert cell
theta_2_B = Inputs.range([MinAngle2, MaxAngle2], {value: -40, step: 0.1, label: htl.html`<b>Thigh</b> (J2), θ<sub>2</sub> [°]`, disabled: true})
Insert cell
theta_3_B = Inputs.range([MinAngle3, MaxAngle3], {value: 20, step: 0.1, label: htl.html`&nbsp;&nbsp; <b>Calf</b> (J3), θ<sub>3</sub> [°]`, disabled: true})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
paw_ID = "Empty_foot_16"
Insert cell
Insert cell
Insert cell
Insert cell
Sketchfab = require(await FileAttachment("sketchfab-viewer-1.11.0.js").url())
Insert cell
loadSketchfab(uid, "api-frame");
Insert cell
loadSketchfab = (sceneuid, elementId) => {
const iframe = document.getElementById(elementId);
const client = new Sketchfab("1.11.0", iframe);

client.init(sceneuid, {
success: load_success,
error: () => console.error("Sketchfab API error"),
ui_controls: 0,
ui_infos: 0,
ui_watermark: 0,
ui_stop: 0,
preload: 1,
autostart: 1,
orbit_constraint_pan: 1,
});
};
Insert cell
getNodeByName = (nodemap, nodename) => {
return Object.values(nodemap).find((node) => {
if (node.type === "MatrixTransform" && node.name === nodename) {
return node;
}
});
};
Insert cell
function getNodeCoordinates(api, instanceID) {
api.getMatrix(instanceID, function(err, matrix) {

// Log the entire matrix for debugging
console.log("Matrix:", matrix);

// Extract the translation part of the matrix
const x = matrix.world[12];
const y = matrix.world[13];
const z = matrix.world[14];

// Update the coordinates display
updateCoordinatesDisplay(x, y, z);

});
}
Insert cell
// Function to update the coordinates display in the <div>
function updateCoordinatesDisplay(x, y, z) {
const displayDiv = document.getElementById('coordinates-display');
displayDiv.innerHTML = `Coordinates: X=${x.toFixed(2)}, Y=${y.toFixed(2)}, Z=${z.toFixed(2)}`;
}
Insert cell
// Rotate a node by a certain angle (in radians)
rotateNode = (api, instanceID, angle, RX, RY, RZ) => {
api.rotate(
instanceID,
[angle, RX, RY, RZ],
{
duration: 0.0,
easing: "linear"
}
);
};
Insert cell
translateNode = (api, instanceID, translationVector) => {
api.translate(
instanceID,
translationVector,
{
duration: 0.0,
easing: "linear"
}
);
};
Insert cell
load_success = (api) => {
api.start(function () {
api.addEventListener("viewerready", function () {
api.getNodeMap(function (err, nodes) {
// console.log(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);

// Update once at load:
// Directly invoke the function with the current value on page load
(function initialize() {
const wireframeEnabled = wireframe.value;
api.setWireframe(wireframeEnabled);
updateOpacity();
cubeVisibility();
updateMatrix();
})();
// Enable/disable wireframe
wireframe.addEventListener('input', function (event) {
const wireframeEnabled = wireframe.value;
api.setWireframe(wireframeEnabled);
});

// Hide/show cube
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);
}
});
});
});
};
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more