三维机械设计、装配与运动仿真通用组件
这是一个基于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);
}
}