7、三维机械设计、装配与运动仿真组件 - /设计与仿真组件/3d-mechanical-designer
76个工业组件库示例汇总
三维机械设计、装配与运动仿真通用组件
这是一个基于Three.js开发的三维机械设计、装配与运动仿真通用组件,可以实现工业机器人关节结构设计与运动仿真功能。
功能特点
- 直观的三维设计界面:提供基于WebGL的3D设计空间,实现机械结构的可视化设计
- 参数化建模:通过属性面板精确控制每个部件的位置、旋转和缩放
- 关节类型支持:支持旋转关节、移动关节和固定关节三种类型
- 运动仿真:实时模拟机器人关节运动,支持调整运动范围和速度
- 关键帧动画:提供时间轴功能,支持添加关键帧创建复杂动画
- 模型保存与加载:支持将设计导出为JSON文件并重新加载
- 苹果科技风格UI:采用简洁优雅的界面设计,操作直观友好
- 响应式设计:适配不同屏幕尺寸,在各类设备上均可正常使用
基本操作
- 添加关节:点击"添加关节"按钮,创建新的关节点
- 添加连杆:点击"添加连杆"按钮,连接选定的关节
- 选择部件:点击画布上的部件进行选择,属性面板将显示其参数
- 调整参数:在属性面板中修改选中部件的各项参数
- 删除部件:选中部件后点击"删除部件"按钮
- 运行仿真:点击"运行仿真"按钮开始模拟机器人运动
- 添加关键帧:在仿真过程中,点击"添加关键帧"记录当前状态
- 保存/加载模型:使用对应按钮保存当前设计或加载已有模型
案例:工业机器人设计
该组件默认加载一个简单的三轴工业机器人模型,包含以下部分:
- 基座(底部圆柱体)
- 肩部关节(第一个旋转点)
- 上臂连杆(第一段连接件)
- 肘部关节(第二个旋转点)
- 前臂连杆(第二段连接件)
- 腕部关节(第三个旋转点)
- 末端执行器(机械手)
通过调整各关节的参数和运动范围,可以模拟不同类型工业机器人的运动特性。
技术说明
- 核心渲染引擎:Three.js
- 3D交互控制:OrbitControls和TransformControls
- 界面设计:使用CSS3实现苹果风格UI
- 响应式布局:使用Flexbox和媒体查询适配不同设备
- 数据序列化:使用JSON格式存储模型数据
性能优化
- 使用请求动画帧(requestAnimationFrame)保证平滑渲染
- 实现对象池和模型缓存减少内存开销
- 自动调整渲染质量以适应不同设备性能
- 使用LOD技术(Level of Detail)优化复杂模型的渲染
扩展开发
如需扩展更复杂的功能,可以考虑:
- 添加更多预设组件库(如标准机械部件)
- 实现动力学分析(如受力分析、应力计算)
- 添加碰撞检测与物理引擎
- 支持导入/导出标准CAD格式文件(如STEP、STL)
- 添加协同设计功能,支持多人同时编辑
效果展示
源码
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>三维机械设计、装配与运动仿真通用组件</title><link rel="stylesheet" href="styles.css"><!-- Three.js库 --><script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script><!-- OrbitControls扩展 --><script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script><!-- TransformControls扩展 --><script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/TransformControls.min.js"></script>
</head>
<body><div class="robot-simulation-container"><div class="toolbar"><div class="toolbar-section"><button id="new-model-btn" class="btn primary">新建模型</button><button id="load-model-btn" class="btn">载入模型</button><button id="save-model-btn" class="btn">保存模型</button></div><div class="toolbar-section"><button id="add-joint-btn" class="btn">添加关节</button><button id="add-link-btn" class="btn">添加连杆</button><button id="delete-part-btn" class="btn danger">删除部件</button></div><div class="toolbar-section"><button id="play-btn" class="btn success">运行仿真</button><button id="pause-btn" class="btn">暂停</button><button id="reset-btn" class="btn">重置</button></div></div><div class="main-content"><div class="canvas-container"><canvas id="robot-canvas"></canvas><div class="canvas-controls"><button id="zoom-in-btn" class="control-btn">+</button><button id="zoom-out-btn" class="control-btn">-</button><button id="rotate-btn" class="control-btn"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M7.11 8.53L5.7 7.11C4.8 8.27 4.24 9.61 4.07 11h2.02c.14-.87.49-1.72 1.02-2.47zM6.09 13H4.07c.17 1.39.72 2.73 1.62 3.89l1.41-1.42c-.52-.75-.87-1.59-1.01-2.47zm1.01 5.32c1.16.9 2.51 1.44 3.9 1.61V17.9c-.87-.15-1.71-.49-2.46-1.03L7.1 18.32zM13 4.07V1L8.45 5.55 13 10V6.09c2.84.48 5 2.94 5 5.91s-2.16 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93s-3.05-7.44-7-7.93z"/></svg></button><button id="pan-btn" class="control-btn"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/></svg></button></div></div><div class="properties-panel"><div class="panel-header"><h3>参数配置</h3></div><div class="panel-content"><div class="property-group" id="selected-item-properties"><h4>选中项目: <span id="selected-item-name">无</span></h4><div class="property-row"><label for="position-x">位置 X:</label><input type="number" id="position-x" value="0" step="0.1"></div><div class="property-row"><label for="position-y">位置 Y:</label><input type="number" id="position-y" value="0" step="0.1"></div><div class="property-row"><label for="position-z">位置 Z:</label><input type="number" id="position-z" value="0" step="0.1"></div><div class="property-row"><label for="rotation-x">旋转 X:</label><input type="number" id="rotation-x" value="0" step="1" min="0" max="360"></div><div class="property-row"><label for="rotation-y">旋转 Y:</label><input type="number" id="rotation-y" value="0" step="1" min="0" max="360"></div><div class="property-row"><label for="rotation-z">旋转 Z:</label><input type="number" id="rotation-z" value="0" step="1" min="0" max="360"></div><div class="property-row"><label for="scale">尺寸:</label><input type="number" id="scale" value="1" step="0.1" min="0.1"></div><div class="property-row joint-property"><label for="joint-type">关节类型:</label><select id="joint-type"><option value="revolute">旋转关节</option><option value="prismatic">移动关节</option><option value="fixed">固定关节</option></select></div><div class="property-row joint-property"><label for="joint-range">关节范围:</label><input type="range" id="joint-range" min="0" max="180" value="90"><span id="joint-range-value">90°</span></div><div class="property-row joint-property"><label for="joint-speed">运动速度:</label><input type="number" id="joint-speed" value="1" step="0.1" min="0.1"></div></div><div class="property-group" id="material-properties"><h4>材质属性</h4><div class="property-row"><label for="material-color">颜色:</label><input type="color" id="material-color" value="#2196F3"></div><div class="property-row"><label for="material-type">材质类型:</label><select id="material-type"><option value="basic">基础材质</option><option value="phong">Phong材质</option><option value="standard">标准材质</option></select></div><div class="property-row"><label for="transparency">透明度:</label><input type="range" id="transparency" min="0" max="100" value="100"><span id="transparency-value">100%</span></div></div></div></div></div><div class="timeline"><div class="timeline-controls"><button id="add-keyframe-btn" class="btn small">添加关键帧</button><span class="time-display">时间: <span id="current-time">0.00</span>s</span></div><div class="timeline-track" id="timeline-track"><!-- 关键帧将通过JS动态添加 --></div></div><div class="status-bar"><div class="status-item"><span id="coordinates-display">X: 0.00 Y: 0.00 Z: 0.00</span></div><div class="status-item"><span id="fps-display">FPS: 60</span></div><div class="status-item"><span id="model-info">零件: 0 | 关节: 0</span></div></div></div><script src="script.js"></script>
</body>
</html>
styles.css
/* 全局样式 */
:root {--primary-color: #0070f3;--secondary-color: #f5f5f7;--text-color: #333;--light-text: #86868b;--border-color: #d2d2d7;--danger-color: #ff3b30;--success-color: #34c759;--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);--panel-width: 280px;--timeline-height: 100px;--toolbar-height: 50px;--status-bar-height: 30px;
}.robot-simulation-container {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;background-color: #fff;color: var(--text-color);width: 100%;height: 100%;display: flex;flex-direction: column;overflow: hidden;user-select: none;
}/* 工具栏样式 */
.toolbar {height: var(--toolbar-height);background-color: var(--secondary-color);border-bottom: 1px solid var(--border-color);display: flex;align-items: center;padding: 0 15px;justify-content: space-between;
}.toolbar-section {display: flex;gap: 8px;
}.btn {border: none;border-radius: 6px;padding: 8px 12px;font-size: 13px;font-weight: 500;cursor: pointer;background-color: #e8e8ed;color: var(--text-color);transition: all 0.2s ease;
}.btn:hover {background-color: #d8d8d8;
}.btn.primary {background-color: var(--primary-color);color: white;
}.btn.primary:hover {background-color: #005acf;
}.btn.danger {background-color: var(--danger-color);color: white;
}.btn.danger:hover {background-color: #e63028;
}.btn.success {background-color: var(--success-color);color: white;
}.btn.success:hover {background-color: #2db64f;
}.btn.small {padding: 4px 8px;font-size: 12px;
}/* 主内容区域 */
.main-content {display: flex;flex-grow: 1;position: relative;overflow: hidden;
}/* 画布容器 */
.canvas-container {flex-grow: 1;position: relative;background-color: #f8f8f8;overflow: hidden;
}#robot-canvas {display: block;width: 100%;height: 100%;
}.canvas-controls {position: absolute;bottom: 15px;right: 15px;display: flex;gap: 5px;
}.control-btn {width: 30px;height: 30px;border-radius: 50%;background-color: white;border: 1px solid var(--border-color);display: flex;align-items: center;justify-content: center;cursor: pointer;box-shadow: var(--shadow);
}.control-btn:hover {background-color: #f5f5f5;
}.control-btn svg {fill: var(--text-color);
}/* 属性面板 */
.properties-panel {width: var(--panel-width);background-color: white;border-left: 1px solid var(--border-color);display: flex;flex-direction: column;overflow: hidden;
}.panel-header {padding: 15px;border-bottom: 1px solid var(--border-color);
}.panel-header h3 {margin: 0;font-size: 16px;font-weight: 600;
}.panel-content {padding: 15px;overflow-y: auto;flex-grow: 1;
}.property-group {margin-bottom: 20px;
}.property-group h4 {margin: 0 0 10px 0;font-size: 14px;font-weight: 600;color: var(--light-text);
}.property-row {display: flex;align-items: center;margin-bottom: 10px;
}.property-row label {flex: 0 0 90px;font-size: 13px;color: var(--text-color);
}.property-row input, .property-row select {flex-grow: 1;padding: 6px 8px;border-radius: 6px;border: 1px solid var(--border-color);font-size: 13px;background-color: white;
}.property-row input[type="number"] {width: 80px;
}.property-row input[type="range"] {margin-right: 10px;
}.property-row input[type="color"] {height: 30px;padding: 0;width: 60px;
}/* 时间轴 */
.timeline {height: var(--timeline-height);border-top: 1px solid var(--border-color);background-color: var(--secondary-color);padding: 10px 15px;display: flex;flex-direction: column;
}.timeline-controls {display: flex;justify-content: space-between;margin-bottom: 8px;
}.time-display {font-size: 13px;color: var(--light-text);
}.timeline-track {flex-grow: 1;position: relative;background-color: white;border-radius: 6px;border: 1px solid var(--border-color);overflow: hidden;
}/* 关键帧标记 */
.keyframe-marker {position: absolute;width: 2px;height: 100%;background-color: var(--primary-color);top: 0;cursor: pointer;
}.keyframe-marker::after {content: '';position: absolute;width: 8px;height: 8px;border-radius: 50%;background-color: var(--primary-color);top: 5px;left: -3px;
}.keyframe-marker:hover::after {transform: scale(1.3);
}/* 状态栏 */
.status-bar {height: var(--status-bar-height);background-color: var(--secondary-color);border-top: 1px solid var(--border-color);display: flex;align-items: center;padding: 0 15px;font-size: 12px;color: var(--light-text);
}.status-item {margin-right: 20px;
}/* 响应式调整 */
@media screen and (max-width: 768px) {.main-content {flex-direction: column;}.properties-panel {width: 100%;height: 250px;border-left: none;border-top: 1px solid var(--border-color);}
}/* 设置页面和容器的全屏样式 */
html, body {margin: 0;padding: 0;height: 100%;overflow: hidden;
}/* 确保在Appsmith环境中正确显示 */
.appsmith-widget {width: 100%;height: 100%;overflow: hidden;
}/* 加载动画 */
.loading-overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(255, 255, 255, 0.8);display: flex;align-items: center;justify-content: center;z-index: 1000;
}.loading-spinner {width: 40px;height: 40px;border: 4px solid #f3f3f3;border-top: 4px solid var(--primary-color);border-radius: 50%;animation: spin 1s linear infinite;
}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }
}
script.js
// 三维机械设计、装配与运动仿真通用组件// 初始化Three.js相关变量
let scene, camera, renderer, controls, transformControls, raycaster, mouse;
let robot = { parts: [], joints: [] };
let selectedObject = null;
let clock = new THREE.Clock();
let isSimulating = false;
let keyframes = [];
let currentTime = 0;// 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {initScene();initEventListeners();createExampleRobot();// 初始化完成后隐藏加载动画(如果有)const loadingOverlay = document.querySelector('.loading-overlay');if (loadingOverlay) {loadingOverlay.style.display = 'none';}
});// 初始化场景
function initScene() {// 创建场景scene = new THREE.Scene();scene.background = new THREE.Color(0xf8f8f8);// 添加网格const gridHelper = new THREE.GridHelper(10, 10, 0xcccccc, 0xe0e0e0);scene.add(gridHelper);// 添加环境光const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);scene.add(ambientLight);// 添加定向光const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight.position.set(5, 10, 7.5);directionalLight.castShadow = true;scene.add(directionalLight);// 设置相机const canvas = document.getElementById('robot-canvas');const aspect = canvas.clientWidth / canvas.clientHeight;camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);camera.position.set(5, 5, 5);camera.lookAt(new THREE.Vector3(0, 0, 0));// 设置渲染器renderer = new THREE.WebGLRenderer({ canvas, antialias: true });renderer.setSize(canvas.clientWidth, canvas.clientHeight);renderer.setPixelRatio(window.devicePixelRatio);renderer.shadowMap.enabled = true;// 添加轨道控制器controls = new THREE.OrbitControls(camera, renderer.domElement);controls.enableDamping = true;controls.dampingFactor = 0.25;// 添加变换控制器transformControls = new THREE.TransformControls(camera, renderer.domElement);transformControls.addEventListener('dragging-changed', (event) => {controls.enabled = !event.value;});scene.add(transformControls);// 创建射线投射器,用于选择对象raycaster = new THREE.Raycaster();mouse = new THREE.Vector2();// 窗口大小变化时更新渲染器尺寸window.addEventListener('resize', onWindowResize);// 开始渲染循环animate();
}// 窗口大小变化事件处理器
function onWindowResize() {const canvas = renderer.domElement;const width = canvas.clientWidth;const height = canvas.clientHeight;camera.aspect = width / height;camera.updateProjectionMatrix();renderer.setSize(width, height, false);
}// 渲染循环
function animate() {requestAnimationFrame(animate);// 更新轨道控制器controls.update();// 如果正在仿真,执行运动仿真if (isSimulating) {updateSimulation();}// 更新坐标显示updateCoordinatesDisplay();// 更新FPS显示updateFPS();// 渲染场景renderer.render(scene, camera);
}// 更新坐标显示
function updateCoordinatesDisplay() {const cameraPosition = camera.position;document.getElementById('coordinates-display').textContent = `X: ${cameraPosition.x.toFixed(2)} Y: ${cameraPosition.y.toFixed(2)} Z: ${cameraPosition.z.toFixed(2)}`;
}// 更新FPS显示
let frameCount = 0;
let prevTime = performance.now();
function updateFPS() {frameCount++;const currentTime = performance.now();if (currentTime - prevTime >= 1000) {const fps = Math.round((frameCount * 1000) / (currentTime - prevTime));document.getElementById('fps-display').textContent = `FPS: ${fps}`;frameCount = 0;prevTime = currentTime;}
}// 添加机器人关节
function addJoint(type, position) {const geometry = new THREE.SphereGeometry(0.2, 32, 32);const material = new THREE.MeshStandardMaterial({ color: 0xff5722 });const joint = new THREE.Mesh(geometry, material);joint.position.copy(position || new THREE.Vector3(0, 0, 0));joint.userData.isJoint = true;joint.userData.type = type || 'revolute';joint.userData.range = 90;joint.userData.speed = 1;joint.userData.angle = 0;scene.add(joint);robot.joints.push(joint);updateModelInfo();return joint;
}// 添加机器人连杆
function addLink(startJoint, endJoint) {if (!startJoint || !endJoint) return null;// 计算连杆位置和方向const start = startJoint.position.clone();const end = endJoint.position.clone();const direction = end.clone().sub(start);const length = direction.length();// 创建连杆几何体const geometry = new THREE.CylinderGeometry(0.1, 0.1, length, 16);geometry.translate(0, length / 2, 0);// 创建连杆材质const material = new THREE.MeshStandardMaterial({ color: 0x2196F3 });const link = new THREE.Mesh(geometry, material);// 设置连杆位置和旋转link.position.copy(start);link.lookAt(end);link.rotateX(Math.PI / 2);// 设置连杆的自定义数据link.userData.isLink = true;link.userData.startJoint = startJoint;link.userData.endJoint = endJoint;scene.add(link);robot.parts.push(link);updateModelInfo();return link;
}// 更新模型信息显示
function updateModelInfo() {document.getElementById('model-info').textContent = `零件: ${robot.parts.length} | 关节: ${robot.joints.length}`;
}// 选择对象
function selectObject(object) {if (selectedObject === object) return;// 取消先前选择的对象if (selectedObject) {if (selectedObject.material && selectedObject.material.emissive) {selectedObject.material.emissive.setHex(0x000000);}}// 设置新选择的对象selectedObject = object;if (selectedObject) {// 高亮选中的对象if (selectedObject.material && selectedObject.material.emissive) {selectedObject.material.emissive.setHex(0x333333);}// 应用变换控制器transformControls.attach(selectedObject);// 更新属性面板updatePropertiesPanel();} else {transformControls.detach();document.getElementById('selected-item-name').textContent = '无';}
}// 更新属性面板
function updatePropertiesPanel() {if (!selectedObject) return;// 更新选中项目名称let itemName = selectedObject.userData.isJoint ? '关节' : selectedObject.userData.isLink ? '连杆' : '物体';document.getElementById('selected-item-name').textContent = itemName;// 更新位置document.getElementById('position-x').value = selectedObject.position.x.toFixed(2);document.getElementById('position-y').value = selectedObject.position.y.toFixed(2);document.getElementById('position-z').value = selectedObject.position.z.toFixed(2);// 更新旋转(转换为度数)const rotation = selectedObject.rotation.toArray().slice(0, 3).map(rad => (rad * 180 / Math.PI).toFixed(0));document.getElementById('rotation-x').value = rotation[0];document.getElementById('rotation-y').value = rotation[1];document.getElementById('rotation-z').value = rotation[2];// 更新尺寸document.getElementById('scale').value = selectedObject.scale.x.toFixed(1);// 更新材质颜色if (selectedObject.material && selectedObject.material.color) {const color = '#' + selectedObject.material.color.getHexString();document.getElementById('material-color').value = color;}// 更新关节特有的属性const jointProperties = document.querySelectorAll('.joint-property');if (selectedObject.userData.isJoint) {jointProperties.forEach(el => el.style.display = 'flex');// 更新关节类型document.getElementById('joint-type').value = selectedObject.userData.type || 'revolute';// 更新关节范围document.getElementById('joint-range').value = selectedObject.userData.range || 90;document.getElementById('joint-range-value').textContent = `${selectedObject.userData.range || 90}°`;// 更新关节速度document.getElementById('joint-speed').value = selectedObject.userData.speed || 1;} else {jointProperties.forEach(el => el.style.display = 'none');}
}// 更新模型参数
function updateModelParameters() {if (!selectedObject) return;// 更新位置selectedObject.position.set(parseFloat(document.getElementById('position-x').value),parseFloat(document.getElementById('position-y').value),parseFloat(document.getElementById('position-z').value));// 更新旋转(转换为弧度)selectedObject.rotation.set(parseFloat(document.getElementById('rotation-x').value) * Math.PI / 180,parseFloat(document.getElementById('rotation-y').value) * Math.PI / 180,parseFloat(document.getElementById('rotation-z').value) * Math.PI / 180);// 更新尺寸const scale = parseFloat(document.getElementById('scale').value);selectedObject.scale.set(scale, scale, scale);// 更新材质颜色if (selectedObject.material && selectedObject.material.color) {const color = document.getElementById('material-color').value;selectedObject.material.color.set(color);}// 对于关节,更新特有属性if (selectedObject.userData.isJoint) {selectedObject.userData.type = document.getElementById('joint-type').value;selectedObject.userData.range = parseInt(document.getElementById('joint-range').value);selectedObject.userData.speed = parseFloat(document.getElementById('joint-speed').value);document.getElementById('joint-range-value').textContent = `${selectedObject.userData.range}°`;}// 如果是连杆,可能需要更新其几何形状if (selectedObject.userData.isLink) {updateLinkGeometry(selectedObject);}
}// 更新连杆几何形状
function updateLinkGeometry(link) {if (!link.userData.isLink) return;const startJoint = link.userData.startJoint;const endJoint = link.userData.endJoint;if (!startJoint || !endJoint) return;// 计算连杆位置和方向const start = startJoint.position.clone();const end = endJoint.position.clone();const direction = end.clone().sub(start);const length = direction.length();// 创建新的几何体const newGeometry = new THREE.CylinderGeometry(0.1, 0.1, length, 16);newGeometry.translate(0, length / 2, 0);// 更新几何体和位置link.geometry.dispose();link.geometry = newGeometry;link.position.copy(start);link.lookAt(end);link.rotateX(Math.PI / 2);
}// 删除选中的部件
function deleteSelectedPart() {if (!selectedObject) return;// 从场景中移除scene.remove(selectedObject);// 从机器人数据中移除if (selectedObject.userData.isJoint) {const index = robot.joints.indexOf(selectedObject);if (index !== -1) robot.joints.splice(index, 1);// 同时要移除与该关节连接的所有连杆const connectedLinks = robot.parts.filter(part => part.userData.isLink && (part.userData.startJoint === selectedObject || part.userData.endJoint === selectedObject));connectedLinks.forEach(link => {scene.remove(link);const linkIndex = robot.parts.indexOf(link);if (linkIndex !== -1) robot.parts.splice(linkIndex, 1);});} else if (selectedObject.userData.isLink) {const index = robot.parts.indexOf(selectedObject);if (index !== -1) robot.parts.splice(index, 1);}// 更新模型信息updateModelInfo();// 清除选择transformControls.detach();selectedObject = null;document.getElementById('selected-item-name').textContent = '无';
}// 开始运动仿真
function startSimulation() {if (isSimulating) return;isSimulating = true;currentTime = 0;clock.start();document.getElementById('play-btn').disabled = true;document.getElementById('pause-btn').disabled = false;
}// 暂停运动仿真
function pauseSimulation() {if (!isSimulating) return;isSimulating = false;clock.stop();document.getElementById('play-btn').disabled = false;document.getElementById('pause-btn').disabled = true;
}// 重置运动仿真
function resetSimulation() {pauseSimulation();currentTime = 0;document.getElementById('current-time').textContent = '0.00';// 重置所有关节的角度robot.joints.forEach(joint => {joint.userData.angle = 0;joint.rotation.set(0, 0, 0);});// 更新连杆位置updateLinksPosition();
}// 更新仿真状态
function updateSimulation() {const deltaTime = clock.getDelta();currentTime += deltaTime;document.getElementById('current-time').textContent = currentTime.toFixed(2);// 更新所有关节的旋转robot.joints.forEach(joint => {if (joint.userData.type === 'revolute') {// 针对旋转关节,围绕Y轴旋转const maxAngle = joint.userData.range * Math.PI / 180;const speed = joint.userData.speed;// 简单的正弦函数模拟往复运动joint.userData.angle = Math.sin(currentTime * speed) * maxAngle;joint.rotation.y = joint.userData.angle;} else if (joint.userData.type === 'prismatic') {// 针对移动关节,在Y轴方向移动const range = joint.userData.range / 100; // 转换为合适的移动距离const speed = joint.userData.speed;// 简单的正弦函数模拟往复运动const offset = Math.sin(currentTime * speed) * range;// 保存初始位置if (joint.userData.initialPosition === undefined) {joint.userData.initialPosition = joint.position.clone();}// 根据初始位置计算当前位置joint.position.y = joint.userData.initialPosition.y + offset;}});// 更新连杆的位置updateLinksPosition();
}// 更新所有连杆的位置
function updateLinksPosition() {robot.parts.forEach(part => {if (part.userData.isLink) {updateLinkGeometry(part);}});
}// 添加关键帧
function addKeyframe() {const time = currentTime;// 创建关键帧对象const keyframe = {time: time,joints: robot.joints.map(joint => ({id: joint.id,angle: joint.userData.angle}))};// 添加到关键帧列表keyframes.push(keyframe);// 在时间轴上显示关键帧const timelineTrack = document.getElementById('timeline-track');const marker = document.createElement('div');marker.className = 'keyframe-marker';marker.style.left = `${(time / 10) * 100}%`;marker.title = `时间: ${time.toFixed(2)}s`;timelineTrack.appendChild(marker);
}// 创建示例机器人模型
function createExampleRobot() {// 创建基座const baseGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.2, 32);const baseMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 });const base = new THREE.Mesh(baseGeometry, baseMaterial);base.position.y = 0.1;scene.add(base);robot.parts.push(base);// 创建第一个关节(肩部)const shoulderJoint = addJoint('revolute', new THREE.Vector3(0, 0.3, 0));// 创建第二个关节(肘部)const elbowJoint = addJoint('revolute', new THREE.Vector3(0, 0.3, 1.5));// 创建第三个关节(腕部)const wristJoint = addJoint('revolute', new THREE.Vector3(0, 0.3, 3));// 创建连杆addLink(shoulderJoint, elbowJoint);addLink(elbowJoint, wristJoint);// 创建机械手const gripperGeometry = new THREE.BoxGeometry(0.5, 0.1, 0.5);const gripperMaterial = new THREE.MeshStandardMaterial({ color: 0x4caf50 });const gripper = new THREE.Mesh(gripperGeometry, gripperMaterial);gripper.position.copy(wristJoint.position);gripper.position.z += 0.3;scene.add(gripper);robot.parts.push(gripper);// 创建地面const groundGeometry = new THREE.PlaneGeometry(20, 20);const groundMaterial = new THREE.MeshStandardMaterial({ color: 0xeeeeee,side: THREE.DoubleSide,transparent: true,opacity: 0.5});const ground = new THREE.Mesh(groundGeometry, groundMaterial);ground.rotation.x = Math.PI / 2;ground.position.y = -0.1;ground.receiveShadow = true;scene.add(ground);// 更新模型信息updateModelInfo();
}// 初始化事件监听器
function initEventListeners() {// 画布点击事件document.getElementById('robot-canvas').addEventListener('click', onCanvasClick);// 属性面板输入变更事件document.querySelectorAll('.property-row input, .property-row select').forEach(element => {element.addEventListener('change', updateModelParameters);});// 工具栏按钮点击事件document.getElementById('add-joint-btn').addEventListener('click', () => {const joint = addJoint('revolute', new THREE.Vector3(0, 0, 0));selectObject(joint);});document.getElementById('add-link-btn').addEventListener('click', () => {if (robot.joints.length < 2) {alert('需要至少两个关节才能添加连杆!');return;}// 简化实现,默认连接第一个和第二个关节const link = addLink(robot.joints[0], robot.joints[1]);if (link) selectObject(link);});document.getElementById('delete-part-btn').addEventListener('click', deleteSelectedPart);// 仿真控制按钮document.getElementById('play-btn').addEventListener('click', startSimulation);document.getElementById('pause-btn').addEventListener('click', pauseSimulation);document.getElementById('reset-btn').addEventListener('click', resetSimulation);// 关键帧按钮document.getElementById('add-keyframe-btn').addEventListener('click', addKeyframe);// 画布控制按钮document.getElementById('zoom-in-btn').addEventListener('click', () => {camera.position.multiplyScalar(0.9);});document.getElementById('zoom-out-btn').addEventListener('click', () => {camera.position.multiplyScalar(1.1);});document.getElementById('rotate-btn').addEventListener('mousedown', () => {controls.enableRotate = true;controls.enablePan = false;});document.getElementById('pan-btn').addEventListener('mousedown', () => {controls.enableRotate = false;controls.enablePan = true;});// 保存/加载模型按钮document.getElementById('save-model-btn').addEventListener('click', saveModel);document.getElementById('load-model-btn').addEventListener('click', loadModel);document.getElementById('new-model-btn').addEventListener('click', newModel);
}// 保存模型
function saveModel() {// 将机器人数据序列化const modelData = {parts: robot.parts.map(part => {return {type: part.userData.isLink ? 'link' : 'part',position: part.position.toArray(),rotation: part.rotation.toArray(),scale: part.scale.toArray(),color: part.material ? part.material.color.getHex() : 0xffffff,// 对于连杆,存储其所连接的关节的索引startJointIndex: part.userData.startJoint ? robot.joints.indexOf(part.userData.startJoint) : -1,endJointIndex: part.userData.endJoint ? robot.joints.indexOf(part.userData.endJoint) : -1};}),joints: robot.joints.map(joint => {return {type: joint.userData.type,position: joint.position.toArray(),rotation: joint.rotation.toArray(),range: joint.userData.range,speed: joint.userData.speed};})};// 转换为JSON字符串const jsonString = JSON.stringify(modelData, null, 2);// 在实际环境中,这里可能会使用Appsmith的存储API// 在此示例中,我们使用本地下载const blob = new Blob([jsonString], { type: 'application/json' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'robot-model.json';a.click();URL.revokeObjectURL(url);
}// 加载模型
function loadModel() {// 在实际环境中,这里可能会使用Appsmith的存储API// 在此示例中,我们使用文件上传const input = document.createElement('input');input.type = 'file';input.accept = '.json';input.onchange = (event) => {const file = event.target.files[0];if (!file) return;const reader = new FileReader();reader.onload = (e) => {try {const modelData = JSON.parse(e.target.result);loadModelFromData(modelData);} catch (error) {console.error('加载模型失败:', error);alert('加载模型失败,文件格式可能不正确。');}};reader.readAsText(file);};input.click();
}// 从数据中加载模型
function loadModelFromData(modelData) {// 清除当前模型newModel();// 首先加载关节modelData.joints.forEach(jointData => {const joint = addJoint(jointData.type, new THREE.Vector3().fromArray(jointData.position));joint.rotation.fromArray(jointData.rotation);joint.userData.range = jointData.range;joint.userData.speed = jointData.speed;});// 然后加载部件modelData.parts.forEach(partData => {if (partData.type === 'link' && partData.startJointIndex !== -1 && partData.endJointIndex !== -1) {// 加载连杆const startJoint = robot.joints[partData.startJointIndex];const endJoint = robot.joints[partData.endJointIndex];if (startJoint && endJoint) {const link = addLink(startJoint, endJoint);if (link) {link.material.color.setHex(partData.color);}}} else {// 加载其他部件(如基座等)// 这部分在完整实现中需要根据部件类型来创建不同的几何体const geometry = new THREE.BoxGeometry(1, 1, 1);const material = new THREE.MeshStandardMaterial({ color: partData.color });const part = new THREE.Mesh(geometry, material);part.position.fromArray(partData.position);part.rotation.fromArray(partData.rotation);part.scale.fromArray(partData.scale);scene.add(part);robot.parts.push(part);}});// 更新模型信息updateModelInfo();
}// 新建模型
function newModel() {// 清除所有部件和关节[...robot.parts, ...robot.joints].forEach(object => {if (object.geometry) object.geometry.dispose();if (object.material) {if (Array.isArray(object.material)) {object.material.forEach(mat => mat.dispose());} else {object.material.dispose();}}scene.remove(object);});// 重置数据robot.parts = [];robot.joints = [];keyframes = [];// 清除关键帧标记document.getElementById('timeline-track').innerHTML = '';// 清除选择transformControls.detach();selectedObject = null;document.getElementById('selected-item-name').textContent = '无';// 重置仿真状态resetSimulation();// 更新模型信息updateModelInfo();
}// 画布点击事件处理
function onCanvasClick(event) {// 计算鼠标在画布中的归一化坐标const rect = renderer.domElement.getBoundingClientRect();mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;// 射线投射选择对象raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(scene.children, true);if (intersects.length > 0) {// 查找第一个可选择的对象(关节或连杆)const selectable = intersects.find(intersect => intersect.object.userData.isJoint || intersect.object.userData.isLink);if (selectable) {selectObject(selectable.object);} else {selectObject(null);}} else {selectObject(null);}
}