7、三维机械设计、装配与运动仿真组件 - /设计与仿真组件/3d-mechanical-designer

发布于:2025-05-11 ⋅ 阅读:(9) ⋅ 点赞:(0)

76个工业组件库示例汇总

三维机械设计、装配与运动仿真通用组件

这是一个基于Three.js开发的三维机械设计、装配与运动仿真通用组件,可以实现工业机器人关节结构设计与运动仿真功能。

功能特点

  • 直观的三维设计界面:提供基于WebGL的3D设计空间,实现机械结构的可视化设计
  • 参数化建模:通过属性面板精确控制每个部件的位置、旋转和缩放
  • 关节类型支持:支持旋转关节、移动关节和固定关节三种类型
  • 运动仿真:实时模拟机器人关节运动,支持调整运动范围和速度
  • 关键帧动画:提供时间轴功能,支持添加关键帧创建复杂动画
  • 模型保存与加载:支持将设计导出为JSON文件并重新加载
  • 苹果科技风格UI:采用简洁优雅的界面设计,操作直观友好
  • 响应式设计:适配不同屏幕尺寸,在各类设备上均可正常使用

基本操作

  • 添加关节:点击"添加关节"按钮,创建新的关节点
  • 添加连杆:点击"添加连杆"按钮,连接选定的关节
  • 选择部件:点击画布上的部件进行选择,属性面板将显示其参数
  • 调整参数:在属性面板中修改选中部件的各项参数
  • 删除部件:选中部件后点击"删除部件"按钮
  • 运行仿真:点击"运行仿真"按钮开始模拟机器人运动
  • 添加关键帧:在仿真过程中,点击"添加关键帧"记录当前状态
  • 保存/加载模型:使用对应按钮保存当前设计或加载已有模型

案例:工业机器人设计

该组件默认加载一个简单的三轴工业机器人模型,包含以下部分:

  1. 基座(底部圆柱体)
  2. 肩部关节(第一个旋转点)
  3. 上臂连杆(第一段连接件)
  4. 肘部关节(第二个旋转点)
  5. 前臂连杆(第二段连接件)
  6. 腕部关节(第三个旋转点)
  7. 末端执行器(机械手)

通过调整各关节的参数和运动范围,可以模拟不同类型工业机器人的运动特性。

技术说明

  • 核心渲染引擎: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);
  }
} 

网站公告

今日签到

点亮在社区的每一天
去签到