1196 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>G1 3D Temperature Visualization</title>
<!-- Sanad: served from the dashboard static mount; socket.io dropped in
favour of a native WebSocket shim (see `const socket` below). -->
<script src="/static/temp3d/js/three.min.js"></script>
<script src="/static/temp3d/js/STLLoader.js"></script>
<script src="/static/temp3d/js/OrbitControls.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
color: #ffffff;
overflow: hidden;
height: 100vh;
}
#canvas-container {
width: 100%;
height: 100vh;
position: relative;
}
/* Header */
.header {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 15px 25px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.header h1 {
font-size: 1.5em;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.status {
display: flex;
align-items: center;
gap: 10px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ef4444;
animation: pulse 2s infinite;
}
.status-indicator.connected {
background: #10b981;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Stats Panel */
.stats-panel {
position: absolute;
top: 100px;
left: 20px;
z-index: 100;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
min-width: 250px;
}
.stats-panel h2 {
font-size: 1.1em;
margin-bottom: 15px;
font-weight: 600;
}
.stat-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-item:last-child {
border-bottom: none;
}
.stat-label {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9em;
}
.stat-value {
font-weight: 600;
font-size: 1.1em;
}
/* Temperature Legend */
.temp-legend {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.temp-legend h3 {
font-size: 1em;
margin-bottom: 12px;
font-weight: 600;
}
.gradient-bar {
width: 200px;
height: 20px;
background: linear-gradient(to right,
#3b82f6 0%,
#06b6d4 20%,
#10b981 40%,
#fbbf24 60%,
#f97316 80%,
#ef4444 100%);
border-radius: 10px;
margin-bottom: 8px;
}
.gradient-labels {
display: flex;
justify-content: space-between;
font-size: 0.85em;
color: rgba(255, 255, 255, 0.8);
}
/* Controls Panel */
.controls-panel {
position: absolute;
top: 100px;
right: 20px;
z-index: 100;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
width: 250px;
}
.controls-panel h2 {
font-size: 1.1em;
margin-bottom: 8px;
font-weight: 600;
}
.control-btn {
width: 100%;
padding: 7px 6px;
margin-bottom: 5px;
background: rgba(102, 126, 234, 0.3);
border: 1px solid rgba(102, 126, 234, 0.5);
border-radius: 6px;
color: white;
font-size: 0.9em;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
white-space: nowrap;
line-height: 1.2;
}
.control-btn:hover {
background: rgba(102, 126, 234, 0.5);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.control-btn.active {
background: rgba(102, 126, 234, 0.7);
border-color: rgba(102, 126, 234, 0.9);
}
/* Motor Info Panel */
.motor-info-panel {
position: absolute;
bottom: 20px;
right: 20px;
z-index: 100;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
min-width: 280px;
max-height: 300px;
overflow-y: auto;
display: none;
}
.motor-info-panel.visible {
display: block;
}
.motor-info-panel h3 {
font-size: 1.1em;
margin-bottom: 12px;
font-weight: 600;
color: #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-btn {
background: rgba(239, 68, 68, 0.3);
border: 1px solid rgba(239, 68, 68, 0.5);
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
font-size: 16px;
line-height: 1;
transition: all 0.3s ease;
}
.close-btn:hover {
background: rgba(239, 68, 68, 0.6);
transform: scale(1.1);
}
.motor-detail {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.motor-detail:last-child {
border-bottom: none;
}
.detail-label {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9em;
}
.detail-value {
font-weight: 600;
}
.temp-hot {
color: #ef4444;
}
.temp-warm {
color: #fbbf24;
}
.temp-normal {
color: #10b981;
}
/* Loading Screen */
.loading-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
transition: opacity 0.5s ease;
}
.loading-screen.hidden {
opacity: 0;
pointer-events: none;
}
.loader {
width: 60px;
height: 60px;
border: 4px solid rgba(102, 126, 234, 0.3);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
margin-top: 20px;
font-size: 1.2em;
color: rgba(255, 255, 255, 0.8);
}
.loading-progress {
margin-top: 10px;
font-size: 0.9em;
color: rgba(255, 255, 255, 0.6);
}
</style>
</head>
<body>
<!-- Loading Screen -->
<div class="loading-screen" id="loadingScreen">
<div class="loader"></div>
<div class="loading-text">Loading G1 Robot Model</div>
<div class="loading-progress" id="loadingProgress">0%</div>
</div>
<!-- Canvas Container -->
<div id="canvas-container"></div>
<!-- Header -->
<div class="header">
<h1>🤖 G1 3D Temperature Monitor</h1>
<div class="status">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">Connecting...</span>
</div>
</div>
<!-- Stats Panel -->
<div class="stats-panel">
<h2>📊 Statistics</h2>
<div class="stat-item">
<span class="stat-label">Total Motors</span>
<span class="stat-value" id="totalMotors">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Avg Temp</span>
<span class="stat-value" id="avgTemp">--°C</span>
</div>
<div class="stat-item">
<span class="stat-label">Max Temp</span>
<span class="stat-value" id="maxTemp">--°C</span>
</div>
<div class="stat-item">
<span class="stat-label">Min Temp</span>
<span class="stat-value" id="minTemp">--°C</span>
</div>
<div class="stat-item">
<span class="stat-label">Last Update</span>
<span class="stat-value" id="lastUpdate" style="font-size: 0.8em;">--</span>
</div>
</div>
<!-- Controls Panel -->
<div class="controls-panel">
<h2>🎮 Controls</h2>
<button class="control-btn" id="autoRotateBtn">Auto Rotate: OFF</button>
<button class="control-btn active" id="showPositionsBtn">Show Positions: ON</button>
<button class="control-btn" id="resetViewBtn">Reset Camera</button>
<button class="control-btn" id="wireframeBtn">Wireframe: OFF</button>
</div>
<!-- Temperature Legend -->
<div class="temp-legend">
<h3>🌡️ Temperature Scale</h3>
<div class="gradient-bar"></div>
<div class="gradient-labels">
<span>30°C</span>
<span>75°C</span>
<span>120°C</span>
</div>
</div>
<!-- Motor Info Panel -->
<div class="motor-info-panel" id="motorInfoPanel">
<h3 id="motorInfoTitle">
<span>Motor Information</span>
<button class="close-btn" id="closePanelBtn" title="Close">×</button>
</h3>
<div id="motorInfoContent"></div>
</div>
<script>
// Three.js Scene Setup
let scene, camera, renderer, controls;
let robotRoot; // Root object for the robot
let linkMeshes = {}; // Map of link names to meshes
let linkObjects = {}; // Map of link names to Three.js Group objects (for positioning)
let jointData = {}; // Map of joint names to joint information
let motorToJoint = {}; // Map of motor IDs to joint names
let motorData = {};
let selectedMesh = null; // Currently selected mesh
let autoRotate = false;
let wireframeMode = false;
let showPositions = true; // Enable position updates by default
// Sanad: native-WebSocket stand-in for socket.io. Exposes the same
// .on('connect'|'disconnect'|'motor_update', cb) API the rest of this
// file uses, backed by /ws/motor-temps (auto-reconnecting).
const socket = (function () {
const handlers = {};
const proto = (location.protocol === 'https:') ? 'wss' : 'ws';
let ws = null;
function fire(ev, arg) { (handlers[ev] || []).forEach(fn => fn(arg)); }
function connect() {
ws = new WebSocket(proto + '://' + location.host + '/ws/motor-temps');
ws.onopen = () => fire('connect');
ws.onclose = () => { fire('disconnect'); setTimeout(connect, 1500); };
ws.onerror = () => { try { ws.close(); } catch (e) {} };
ws.onmessage = (e) => {
let d; try { d = JSON.parse(e.data); } catch (_) { return; }
fire('motor_update', d);
};
}
connect();
return { on: (ev, cb) => { (handlers[ev] = handlers[ev] || []).push(cb); } };
})();
// Temperature color mapping
function getTemperatureColor(temp) {
const minTemp = 30;
const maxTemp = 120;
const normalizedTemp = Math.max(0, Math.min(1, (temp - minTemp) / (maxTemp - minTemp)));
let r, g, b;
if (normalizedTemp < 0.2) {
const t = normalizedTemp / 0.2;
r = 59 + (6 - 59) * t;
g = 130 + (182 - 130) * t;
b = 246 + (212 - 246) * t;
} else if (normalizedTemp < 0.4) {
const t = (normalizedTemp - 0.2) / 0.2;
r = 6 + (16 - 6) * t;
g = 182 + (185 - 182) * t;
b = 212 + (129 - 212) * t;
} else if (normalizedTemp < 0.6) {
const t = (normalizedTemp - 0.4) / 0.2;
r = 16 + (251 - 16) * t;
g = 185 + (191 - 185) * t;
b = 129 + (36 - 129) * t;
} else if (normalizedTemp < 0.8) {
const t = (normalizedTemp - 0.6) / 0.2;
r = 251 + (249 - 251) * t;
g = 191 + (115 - 191) * t;
b = 36 + (22 - 36) * t;
} else {
const t = (normalizedTemp - 0.8) / 0.2;
r = 249 + (239 - 249) * t;
g = 115 + (68 - 115) * t;
b = 22 + (68 - 22) * t;
}
return new THREE.Color(r / 255, g / 255, b / 255);
}
// Parse URDF XML
function parseURDF(urdfText) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(urdfText, "text/xml");
const links = {};
const joints = {};
// Parse links
const linkElements = xmlDoc.getElementsByTagName('link');
for (let i = 0; i < linkElements.length; i++) {
const link = linkElements[i];
const name = link.getAttribute('name');
links[name] = {
name: name,
visual: null
};
// Get visual mesh
const visual = link.getElementsByTagName('visual')[0];
if (visual) {
const geometry = visual.getElementsByTagName('geometry')[0];
if (geometry) {
const mesh = geometry.getElementsByTagName('mesh')[0];
if (mesh) {
const filename = mesh.getAttribute('filename');
links[name].visual = filename;
}
}
}
}
// Parse joints
const jointElements = xmlDoc.getElementsByTagName('joint');
for (let i = 0; i < jointElements.length; i++) {
const joint = jointElements[i];
const name = joint.getAttribute('name');
const type = joint.getAttribute('type');
const parent = joint.getElementsByTagName('parent')[0].getAttribute('link');
const child = joint.getElementsByTagName('child')[0].getAttribute('link');
const origin = joint.getElementsByTagName('origin')[0];
let xyz = [0, 0, 0];
let rpy = [0, 0, 0];
if (origin) {
const xyzStr = origin.getAttribute('xyz');
const rpyStr = origin.getAttribute('rpy');
if (xyzStr) {
xyz = xyzStr.split(' ').map(parseFloat);
}
if (rpyStr) {
rpy = rpyStr.split(' ').map(parseFloat);
}
}
// Get joint axis for revolute joints
let axis = [0, 0, 1]; // default axis
const axisElement = joint.getElementsByTagName('axis')[0];
if (axisElement) {
const axisStr = axisElement.getAttribute('xyz');
if (axisStr) {
axis = axisStr.split(' ').map(parseFloat);
}
}
joints[name] = {
name: name,
type: type,
parent: parent,
child: child,
xyz: xyz,
rpy: rpy,
axis: axis
};
}
return { links, joints };
}
// Initialize Three.js scene
function initScene() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f0c29);
scene.fog = new THREE.Fog(0x0f0c29, 10, 50);
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// Position camera closer and facing the front of the robot
camera.position.set(0, 1.2, 2.5);
camera.lookAt(0, 0.5, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = autoRotate;
controls.autoRotateSpeed = 1.0;
controls.target.set(0, 0.5, 0);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight1.position.set(5, 10, 5);
directionalLight1.castShadow = true;
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
directionalLight2.position.set(-5, 5, -5);
scene.add(directionalLight2);
const gridHelper = new THREE.GridHelper(10, 10, 0x667eea, 0x302b63);
gridHelper.position.y = -0.01;
scene.add(gridHelper);
window.addEventListener('resize', onWindowResize, false);
// Create robot root
robotRoot = new THREE.Group();
robotRoot.name = 'robot_root';
scene.add(robotRoot);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Build robot from URDF
async function loadRobotModel() {
document.getElementById('loadingProgress').textContent = 'Fetching URDF...';
// Fetch URDF
const urdfText = await fetch('/static/temp3d/g1/g1_29dof_rev_1_0.urdf').then(r => r.text());
const { links, joints } = parseURDF(urdfText);
document.getElementById('loadingProgress').textContent = 'Loading meshes...';
// Load all STL meshes
const loader = new THREE.STLLoader();
const meshCache = {};
let loadedCount = 0;
const totalLinks = Object.keys(links).filter(name => links[name].visual).length;
// Load meshes for all links
for (const linkName in links) {
const link = links[linkName];
if (!link.visual) continue;
const meshPath = link.visual.replace('meshes/', '/static/temp3d/g1/meshes/');
try {
const geometry = await new Promise((resolve, reject) => {
loader.load(meshPath, resolve, undefined, reject);
});
const material = new THREE.MeshPhongMaterial({
color: 0x888888,
specular: 0x111111,
shininess: 30,
emissive: 0x000000,
emissiveIntensity: 0.3
});
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.name = linkName;
meshCache[linkName] = mesh;
linkMeshes[linkName] = mesh;
loadedCount++;
const progress = Math.round((loadedCount / totalLinks) * 100);
document.getElementById('loadingProgress').textContent = `${progress}%`;
} catch (error) {
console.warn(`Could not load mesh for ${linkName}:`, error);
}
}
document.getElementById('loadingProgress').textContent = 'Assembling robot...';
// Build kinematic tree
// Create Three.js objects for each link
for (const linkName in meshCache) {
const linkObj = new THREE.Group();
linkObj.name = linkName;
linkObj.add(meshCache[linkName]);
linkObjects[linkName] = linkObj;
}
// Find root link (pelvis)
const rootLinkName = 'pelvis';
if (linkObjects[rootLinkName]) {
robotRoot.add(linkObjects[rootLinkName]);
}
// Attach children according to joints and store joint info
for (const jointName in joints) {
const joint = joints[jointName];
const parentObj = linkObjects[joint.parent];
const childObj = linkObjects[joint.child];
if (parentObj && childObj) {
// Create a joint pivot group for rotation
const jointPivot = new THREE.Group();
jointPivot.name = `${jointName}_pivot`;
jointPivot.position.set(joint.xyz[0], joint.xyz[1], joint.xyz[2]);
jointPivot.rotation.set(joint.rpy[0], joint.rpy[1], joint.rpy[2]);
// Store original transform for reference
jointPivot.userData.originalPosition = new THREE.Vector3(joint.xyz[0], joint.xyz[1], joint.xyz[2]);
jointPivot.userData.originalRotation = new THREE.Euler(joint.rpy[0], joint.rpy[1], joint.rpy[2]);
jointPivot.userData.axis = new THREE.Vector3(joint.axis[0], joint.axis[1], joint.axis[2]);
jointPivot.userData.jointType = joint.type;
jointPivot.userData.childLink = joint.child;
// Store joint data
jointData[jointName] = {
pivot: jointPivot,
axis: joint.axis,
type: joint.type,
childLink: joint.child
};
// Attach pivot to parent
parentObj.add(jointPivot);
// Reset child transform and attach to pivot
childObj.position.set(0, 0, 0);
childObj.rotation.set(0, 0, 0);
jointPivot.add(childObj);
}
}
// Create motor to joint mapping based on link names
// This maps motor IDs to the joint that controls that link
await fetch('/api/temp/mapping')
.then(r => r.json())
.then(mapping => {
const motorToMesh = mapping.motor_to_mesh;
// For each motor, find the joint that has this link as a child
for (const motorId in motorToMesh) {
const linkName = motorToMesh[motorId];
// Find the joint that controls this link
for (const jointName in jointData) {
if (jointData[jointName].childLink === linkName) {
motorToJoint[motorId] = jointName;
console.log(`Motor ${motorId} -> Joint ${jointName} -> Link ${linkName}`);
break;
}
}
}
});
// Position robot at ground level and rotate to stand upright facing forward
robotRoot.position.y = 0.78; // Height adjustment to place feet on ground
robotRoot.rotation.x = -Math.PI / 2; // Rotate -90 degrees to stand upright
robotRoot.rotation.z = -Math.PI / 2; // Rotate -90 degrees to face forward (camera)
document.getElementById('loadingScreen').classList.add('hidden');
console.log(`Loaded ${Object.keys(linkMeshes).length} link meshes`);
console.log(`Created ${Object.keys(jointData).length} joints`);
}
// Update motor positions
function updateMotorPositions(positions) {
if (!positions || positions.length === 0) return;
if (!showPositions) return; // Skip if positions are disabled
positions.forEach(posData => {
const motorId = posData.motor_id;
const position = posData.position;
const jointName = motorToJoint[motorId];
if (jointName && jointData[jointName]) {
const joint = jointData[jointName];
const pivot = joint.pivot;
const axis = joint.axis;
// Apply rotation around the joint axis
// The position value is in radians
if (joint.type === 'revolute' || joint.type === 'continuous') {
// Create rotation quaternion around the axis
const rotationAxis = new THREE.Vector3(axis[0], axis[1], axis[2]).normalize();
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(rotationAxis, position);
// Apply the rotation to the pivot
// First reset to original rotation
pivot.rotation.copy(pivot.userData.originalRotation);
// Then apply the motor position rotation
pivot.quaternion.multiply(quaternion);
}
}
});
}
// Reset all joints to their original positions
function resetAllJointPositions() {
for (const jointName in jointData) {
const joint = jointData[jointName];
const pivot = joint.pivot;
if (pivot && pivot.userData.originalRotation) {
pivot.rotation.copy(pivot.userData.originalRotation);
}
}
}
// Update motor temperatures
function updateMotorTemperatures(data) {
if (!data.temperatures || data.temperatures.length === 0) return;
const temps = data.temperatures;
document.getElementById('totalMotors').textContent = temps.length;
const avgTemps = temps.map(m => m.avg);
const avgTemp = (avgTemps.reduce((a, b) => a + b, 0) / avgTemps.length).toFixed(1);
const maxTemp = Math.max(...avgTemps).toFixed(1);
const minTemp = Math.min(...avgTemps).toFixed(1);
document.getElementById('avgTemp').textContent = avgTemp + '°C';
document.getElementById('maxTemp').textContent = maxTemp + '°C';
document.getElementById('minTemp').textContent = minTemp + '°C';
const now = new Date();
document.getElementById('lastUpdate').textContent = now.toLocaleTimeString();
// Update mesh colors
temps.forEach(motor => {
const meshName = motor.mesh_name;
if (meshName && linkMeshes[meshName]) {
const linkGroup = linkMeshes[meshName].parent;
const mesh = linkMeshes[meshName];
const temp = motor.avg;
const color = getTemperatureColor(temp);
// Update motor data
mesh.userData.motorData = motor;
// Only update colors if this mesh is not currently selected
if (mesh !== selectedMesh) {
mesh.material.color = color;
mesh.material.emissive = color;
mesh.material.emissiveIntensity = 0.2 + (temp / 120) * 0.3;
}
}
});
motorData = data;
}
// Handle mesh clicks
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function clearSelection() {
if (selectedMesh) {
// Restore original material properties based on temperature
const motorData = selectedMesh.userData.motorData;
if (motorData) {
const temp = motorData.avg;
const color = getTemperatureColor(temp);
selectedMesh.material.color = color;
selectedMesh.material.emissive = color;
selectedMesh.material.emissiveIntensity = 0.2 + (temp / 120) * 0.3;
}
selectedMesh = null;
}
}
function highlightMesh(mesh) {
// Clear previous selection
clearSelection();
// Highlight the new selection
selectedMesh = mesh;
// Get the temperature-based color and make it lighter/brighter
const motorData = mesh.userData.motorData;
if (motorData) {
const temp = motorData.avg;
const baseColor = getTemperatureColor(temp);
// Create a lighter version by lerping towards white
const lighterColor = baseColor.clone().lerp(new THREE.Color(1.0, 1.0, 1.0), 0.5);
// Apply the lighter color with high emissive intensity
selectedMesh.material.color = lighterColor;
selectedMesh.material.emissive = lighterColor;
selectedMesh.material.emissiveIntensity = 0.7;
}
}
function onMouseClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(Object.values(linkMeshes));
if (intersects.length > 0) {
const mesh = intersects[0].object;
if (mesh.userData.motorData) {
// Clear hover effect if we had one
if (hoveredMesh && hoveredMesh !== mesh) {
const motorData = hoveredMesh.userData.motorData;
if (motorData) {
const temp = motorData.avg;
const color = getTemperatureColor(temp);
hoveredMesh.material.emissive = color;
hoveredMesh.material.emissiveIntensity = 0.2 + (temp / 120) * 0.3;
}
}
hoveredMesh = null;
highlightMesh(mesh);
showMotorInfo(mesh.userData.motorData);
}
} else {
// Clicked on empty space - clear selection and hide panel
clearSelection();
document.getElementById('motorInfoPanel').classList.remove('visible');
}
}
function showMotorInfo(motor) {
const panel = document.getElementById('motorInfoPanel');
const title = document.getElementById('motorInfoTitle');
const content = document.getElementById('motorInfoContent');
// Update title (keeping the close button)
title.querySelector('span').textContent = motor.motor_name;
const temp1Class = motor.temp1 > 60 ? 'temp-hot' : motor.temp1 > 45 ? 'temp-warm' : 'temp-normal';
const temp2Class = motor.temp2 > 60 ? 'temp-hot' : motor.temp2 > 45 ? 'temp-warm' : 'temp-normal';
const avgClass = motor.avg > 60 ? 'temp-hot' : motor.avg > 45 ? 'temp-warm' : 'temp-normal';
// Format position in degrees and radians
let positionHTML = '';
if (motor.position !== undefined) {
const positionDeg = (motor.position * 180 / Math.PI).toFixed(1);
const positionRad = motor.position.toFixed(3);
positionHTML = `
<div class="motor-detail">
<span class="detail-label">Position</span>
<span class="detail-value">${positionDeg}° (${positionRad} rad)</span>
</div>
`;
}
// Format velocity if available
let velocityHTML = '';
if (motor.velocity !== undefined) {
const velocityDeg = (motor.velocity * 180 / Math.PI).toFixed(1);
velocityHTML = `
<div class="motor-detail">
<span class="detail-label">Velocity</span>
<span class="detail-value">${velocityDeg}°/s</span>
</div>
`;
}
// Format torque if available
let torqueHTML = '';
if (motor.torque !== undefined) {
torqueHTML = `
<div class="motor-detail">
<span class="detail-label">Torque</span>
<span class="detail-value">${motor.torque.toFixed(2)} Nm</span>
</div>
`;
}
content.innerHTML = `
<div class="motor-detail">
<span class="detail-label">Motor ID</span>
<span class="detail-value">${motor.motor_id}</span>
</div>
${positionHTML}
${velocityHTML}
${torqueHTML}
<div class="motor-detail">
<span class="detail-label">Surface Temp</span>
<span class="detail-value ${temp1Class}">${motor.temp1}°C</span>
</div>
<div class="motor-detail">
<span class="detail-label">Winding Temp</span>
<span class="detail-value ${temp2Class}">${motor.temp2}°C</span>
</div>
<div class="motor-detail">
<span class="detail-label">Average Temp</span>
<span class="detail-value ${avgClass}">${motor.avg.toFixed(1)}°C</span>
</div>
<div class="motor-detail">
<span class="detail-label">Mesh Part</span>
<span class="detail-value" style="font-size: 0.8em;">${motor.mesh_name}</span>
</div>
`;
panel.classList.add('visible');
}
window.addEventListener('click', onMouseClick, false);
// Close panel button
document.getElementById('closePanelBtn').addEventListener('click', function(event) {
event.stopPropagation(); // Prevent triggering the window click
// Clear hover effect if any
if (hoveredMesh) {
const motorData = hoveredMesh.userData.motorData;
if (motorData) {
const temp = motorData.avg;
const color = getTemperatureColor(temp);
hoveredMesh.material.emissive = color;
hoveredMesh.material.emissiveIntensity = 0.2 + (temp / 120) * 0.3;
}
hoveredMesh = null;
}
clearSelection();
document.getElementById('motorInfoPanel').classList.remove('visible');
});
// Add hover effect for meshes
let hoveredMesh = null;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(Object.values(linkMeshes));
let newHoveredMesh = null;
// Check if we're hovering over a mesh
if (intersects.length > 0) {
const mesh = intersects[0].object;
if (mesh.userData.motorData && mesh !== selectedMesh) {
newHoveredMesh = mesh;
}
}
// Only update if hover state changed
if (newHoveredMesh !== hoveredMesh) {
// Clear previous hover
if (hoveredMesh && hoveredMesh !== selectedMesh) {
const motorData = hoveredMesh.userData.motorData;
if (motorData) {
const temp = motorData.avg;
const color = getTemperatureColor(temp);
hoveredMesh.material.emissive = color;
hoveredMesh.material.emissiveIntensity = 0.2 + (temp / 120) * 0.3;
}
}
// Apply new hover
if (newHoveredMesh) {
const motorData = newHoveredMesh.userData.motorData;
if (motorData) {
const temp = motorData.avg;
const color = getTemperatureColor(temp);
// Make it brighter by increasing emissive intensity
newHoveredMesh.material.emissive = color;
newHoveredMesh.material.emissiveIntensity = 0.5;
}
document.body.style.cursor = 'pointer';
} else {
document.body.style.cursor = 'default';
}
hoveredMesh = newHoveredMesh;
} else if (newHoveredMesh) {
// Still hovering, keep cursor as pointer
document.body.style.cursor = 'pointer';
} else {
document.body.style.cursor = 'default';
}
}
window.addEventListener('mousemove', onMouseMove, false);
// Control buttons
document.getElementById('autoRotateBtn').addEventListener('click', function () {
autoRotate = !autoRotate;
controls.autoRotate = autoRotate;
this.textContent = `Auto Rotate: ${autoRotate ? 'ON' : 'OFF'}`;
this.classList.toggle('active', autoRotate);
});
document.getElementById('showPositionsBtn').addEventListener('click', function () {
showPositions = !showPositions;
this.textContent = `Show Positions: ${showPositions ? 'ON' : 'OFF'}`;
this.classList.toggle('active', showPositions);
if (!showPositions) {
// Reset all joints to original positions when disabled
resetAllJointPositions();
} else {
// Re-apply current positions when enabled
if (motorData.positions && motorData.positions.length > 0) {
updateMotorPositions(motorData.positions);
}
}
});
document.getElementById('resetViewBtn').addEventListener('click', function () {
camera.position.set(0, 1.2, 2.5);
controls.target.set(0, 0.5, 0);
controls.update();
});
document.getElementById('wireframeBtn').addEventListener('click', function () {
wireframeMode = !wireframeMode;
Object.values(linkMeshes).forEach(mesh => {
mesh.material.wireframe = wireframeMode;
});
this.textContent = `Wireframe: ${wireframeMode ? 'ON' : 'OFF'}`;
this.classList.toggle('active', wireframeMode);
});
// Socket.IO events
socket.on('connect', function () {
document.getElementById('statusIndicator').classList.add('connected');
document.getElementById('statusText').textContent = 'Connected';
});
socket.on('disconnect', function () {
document.getElementById('statusIndicator').classList.remove('connected');
document.getElementById('statusText').textContent = 'Disconnected';
});
socket.on('motor_update', function (data) {
updateMotorTemperatures(data);
if (data.positions && data.positions.length > 0) {
updateMotorPositions(data.positions);
}
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// Initialize
async function init() {
initScene();
await loadRobotModel();
animate();
// Fetch initial data
fetch('/api/temp/motors')
.then(response => response.json())
.then(data => {
if (data.temperatures && data.temperatures.length > 0) {
updateMotorTemperatures(data);
}
if (data.positions && data.positions.length > 0) {
updateMotorPositions(data.positions);
}
});
}
init();
</script>
</body>
</html>