11、参数化三维产品设计组件 - /设计与仿真组件/parametric-3d-product-design

76个工业组件库示例汇总

参数化三维产品设计组件 (注塑模具与公差分析)

概述

这是一个交互式的 Web 组件,旨在演示简单的三维零件(如带凸台的方块)的参数化设计过程,并结合注塑模具设计(如开模动画)与公差分析(如可视化公差带)的概念性可视化。

主要功能

  • 参数化零件建模 :
    • 通过滑块实时修改零件的基础尺寸(宽度、高度、深度)。
    • 通过滑块实时修改特征(圆柱凸台)的尺寸(直径、高度)和相对位置。
  • 实时 3D 可视化 :
    • 使用 Three.js 渲染零件模型。
    • 支持通过鼠标进行视图交互(旋转、缩放、平移)。
  • 概念性分析与模拟 :
    • 公差可视化: 切换显示尺寸公差(最小/最大包络盒)和位置公差(特征允许范围)的理论边界。
    • 模拟开模: 播放简化的动画,展示模具上下型腔分离的过程。
  • 界面与风格 :
    • 采用苹果科技风格,界面简洁直观。
    • 响应式布局,适应不同屏幕尺寸。

如何使用

  1. 打开页面 : 在浏览器中打开 index.html 文件。
  2. 调整参数 :
    • 在左侧面板的 "基础零件尺寸""特征: 圆柱凸台" 部分,拖动滑块调整零件的几何参数。
    • 3D 模型会在释放滑块后更新。
  3. 交互视图 :
    • 在右侧 3D 视图区,按住鼠标 左键拖动 进行旋转。
    • 滚动鼠标滚轮 进行缩放。
    • 按住鼠标 右键 (或 Ctrl/Cmd + 左键) 拖动 进行平移。
  4. 公差分析 (概念) :
    • "公差分析" 部分调整公差值滑块。
    • 点击 "显示/隐藏公差范围" 按钮,切换显示红/绿/蓝色透明几何体,表示理论上的公差带。
  5. 模拟开模 (概念) :
    • 点击 "模拟开模 (概念)" 按钮,观看模具分离动画(零件会暂时隐藏)。
    • 再次点击(按钮变为 "复位模具") 可观看模具闭合动画,并重新显示零件。
  6. 重置视图 : 点击 "重置视图" 按钮,将相机恢复到默认位置和朝向。

文件结构

复制代码
parametric-3d-product-design/
├── index.html         # HTML 页面结构
├── styles.css         # CSS 样式定义
├── script.js          # JavaScript 交互与3D逻辑
└── README.md          # 本说明文件

技术栈

  • HTML5 / CSS3 (Flexbox, CSS Variables)
  • JavaScript (ES6+)
  • Three.js (r128)
  • Three.js OrbitControls

重要提示

  • 概念演示: 本组件主要用于演示原理,并非精确的工程工具。
  • 公差可视化 : 仅为理论边界的概念性展示,不执行 实际的公差叠加或统计分析。
  • 开模模拟 : 动画效果高度简化,不涉及真实模具的复杂结构(如滑块、顶针、分型面细节等)。
  • 几何模型: 零件由简单的几何体构成,未使用 CSG (构造实体几何) 进行精确合并,可能存在视觉穿插。如需精确模型,可考虑引入相关库。
  • 性能: 频繁更新参数(尤其在公差可视化开启时)可能影响性能。

效果展示

源码

index.html

javascript 复制代码
<!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 for camera interaction -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
    <!-- Optional: Add CSG library if needed for complex boolean operations -->
    <!-- <script src="path/to/three-csg.js"></script> -->
</head>
<body>
    <div class="product-design-container">
        <header class="app-header">
            <h1>参数化三维产品设计</h1>
            <p>应用于注塑模具设计与公差分析</p>
        </header>

        <div class="main-content-area">
            <!-- 左侧参数与控制面板 -->
            <aside class="controls-panel">
                <h2>参数定义 & 分析</h2>

                <div class="parameter-section">
                    <h3>基础零件尺寸</h3>
                    <label for="partWidth">宽度 (X):</label>
                    <input type="range" id="partWidth" name="partWidth" min="20" max="100" value="50" step="1">
                    <span class="param-value" id="partWidthValue">50</span> mm

                    <label for="partHeight">高度 (Y):</label>
                    <input type="range" id="partHeight" name="partHeight" min="10" max="80" value="30" step="1">
                    <span class="param-value" id="partHeightValue">30</span> mm

                    <label for="partDepth">深度 (Z):</label>
                    <input type="range" id="partDepth" name="partDepth" min="20" max="100" value="40" step="1">
                    <span class="param-value" id="partDepthValue">40</span> mm
                </div>

                <div class="parameter-section">
                    <h3>特征: 圆柱凸台 (Boss)</h3>
                    <label for="bossDiameter">直径:</label>
                    <input type="range" id="bossDiameter" name="bossDiameter" min="5" max="25" value="15" step="0.5">
                    <span class="param-value" id="bossDiameterValue">15.0</span> mm

                    <label for="bossHeight">高度:</label>
                    <input type="range" id="bossHeight" name="bossHeight" min="2" max="20" value="10" step="0.5">
                    <span class="param-value" id="bossHeightValue">10.0</span> mm

                    <label for="bossPosX">X 位置 (%):</label> <!-- Position relative to width -->
                    <input type="range" id="bossPosX" name="bossPosX" min="10" max="90" value="50" step="1">
                    <span class="param-value" id="bossPosXValue">50</span> %

                    <label for="bossPosZ">Z 位置 (%):</label> <!-- Position relative to depth -->
                    <input type="range" id="bossPosZ" name="bossPosZ" min="10" max="90" value="50" step="1">
                    <span class="param-value" id="bossPosZValue">50</span> %
                </div>

                 <div class="parameter-section">
                    <h3>公差分析 (概念)</h3>
                     <label for="dimensionTolerance">尺寸公差 (+/-):</label>
                    <input type="range" id="dimensionTolerance" name="dimensionTolerance" min="0.0" max="1.0" value="0.1" step="0.05">
                    <span class="param-value" id="dimensionToleranceValue">0.10</span> mm

                    <label for="positionTolerance">位置公差 (圆域 +/-):</label> <!-- Tolerance zone for boss position -->
                    <input type="range" id="positionTolerance" name="positionTolerance" min="0.0" max="0.5" value="0.05" step="0.01">
                    <span class="param-value" id="positionToleranceValue">0.05</span> mm
                 </div>

                <div class="action-buttons">
                    <button id="analyzeToleranceButton">显示/隐藏公差范围</button>
                    <button id="simulateMoldButton">模拟开模 (概念)</button>
                    <button id="resetViewButton">重置视图</button>
                </div>

                 <div class="status-display">
                    <h3>状态</h3>
                    <p id="statusText">准备就绪。请调整参数。</p>
                 </div>
            </aside>

            <!-- 右侧 3D 可视化区域 -->
            <main class="visualization-area">
                <div id="rendererContainer"></div>
                <!-- Optional: Maybe add overlay for tolerance info -->
            </main>
        </div>

        <footer class="app-footer">
            <p>注塑模具设计与公差分析辅助组件</p>
        </footer>
    </div>

    <script src="script.js"></script>
</body>
</html> 

styles.css

javascript 复制代码
/* styles.css - Parametric 3D Product Design Component */

:root {
    --primary-bg: #ffffff;
    --secondary-bg: #f5f5f7;
    --controls-bg: #e8e8ed;
    --text-primary: #1d1d1f;
    --text-secondary: #515154;
    --accent-blue: #007aff;
    --accent-blue-hover: #005ec4;
    --border-color: #d2d2d7;
    --shadow-color: rgba(0, 0, 0, 0.08);
    --apple-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --tolerance-color-min: rgba(255, 0, 0, 0.3); /* Red tint for min */
    --tolerance-color-max: rgba(0, 255, 0, 0.3); /* Green tint for max */
}

body {
    font-family: var(--apple-font);
    margin: 0;
    background-color: var(--secondary-bg);
    color: var(--text-primary);
    font-size: 14px;
    line-height: 1.5;
    overflow-x: hidden;
}

.product-design-container {
    width: 100%;
    max-width: 100%;
    min-height: calc(100vh - 40px);
    display: flex;
    flex-direction: column;
    background-color: var(--primary-bg);
    box-sizing: border-box;
}

.app-header {
    background-color: var(--primary-bg);
    text-align: center;
    padding: 15px 20px;
    border-bottom: 1px solid var(--border-color);
}

.app-header h1 {
    margin: 0 0 5px 0;
    font-size: 1.8em;
    font-weight: 600;
    color: var(--text-primary);
}

.app-header p {
    margin: 0;
    color: var(--text-secondary);
    font-size: 0.9em;
}

.main-content-area {
    flex-grow: 1;
    display: flex;
    width: 100%;
}

.controls-panel {
    width: 340px; /* Slightly wider for more controls */
    flex-shrink: 0;
    background-color: var(--controls-bg);
    padding: 20px;
    border-right: 1px solid var(--border-color);
    overflow-y: auto;
    box-sizing: border-box;
    max-height: calc(100vh - 100px); /* Adjust 100px based on header/footer */
}

.controls-panel h2 {
    margin-top: 0;
    margin-bottom: 25px;
    font-size: 1.3em;
    font-weight: 600;
    color: var(--text-primary);
    border-bottom: 1px solid #c8c8cc;
    padding-bottom: 10px;
}

.parameter-section {
    margin-bottom: 25px; /* Slightly less margin */
}

.parameter-section h3 {
    margin-top: 0;
    margin-bottom: 15px;
    font-size: 1.0em;
    font-weight: 600;
    color: var(--text-secondary);
}

label {
    display: block;
    margin-bottom: 5px;
    font-weight: 500;
    color: var(--text-primary);
    font-size: 0.95em;
}

input[type="range"] {
    width: 100%;
    height: 4px;
    cursor: pointer;
    appearance: none;
    background: #dcdce0;
    border-radius: 4px;
    outline: none;
    margin-bottom: 3px; /* Less space */
}
input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    width: 16px;
    height: 16px;
    background: var(--accent-blue);
    border-radius: 50%;
    cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
    width: 16px;
    height: 16px;
    background: var(--accent-blue);
    border-radius: 50%;
    cursor: pointer;
    border: none;
}

.param-value {
    display: inline-block;
    margin-right: 5px; /* Space before unit */
    font-size: 0.9em;
    color: var(--text-secondary);
    min-width: 30px; /* Align values a bit */
    text-align: right;
}

/* Style the unit text after span */
.parameter-section > span + span {
     font-size: 0.85em;
     color: var(--text-secondary);
     margin-left: -2px; /* Pull unit closer */
     margin-bottom: 15px;
     display: inline-block;
}
.parameter-section label + input + span + span {
     margin-bottom: 15px; /* Add bottom margin after unit */
}

select {
    /* ... (same as previous component) ... */
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    background-color: var(--primary-bg);
    font-family: inherit;
    font-size: 0.95em;
    margin-bottom: 20px;
    appearance: none;
    background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%208l5%205%205-5z%22%20fill%3D%22%23515154%22%2F%3E%3C%2Fsvg%3E');
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 12px;
}

.action-buttons button {
    display: block;
    width: 100%;
    padding: 10px 15px;
    margin-bottom: 10px;
    font-size: 0.95em;
    font-weight: 500;
    color: #fff;
    background-color: var(--accent-blue);
    border: none;
    border-radius: 6px;
    cursor: pointer;
    text-align: center;
    transition: background-color 0.2s ease, box-shadow 0.2s ease;
}

.action-buttons button:hover {
    background-color: var(--accent-blue-hover);
    box-shadow: 0 2px 5px rgba(0, 122, 255, 0.2);
}

.action-buttons button:disabled {
    background-color: #b0b0b5;
    cursor: not-allowed;
    box-shadow: none;
}

/* Style specific buttons */
#analyzeToleranceButton, #simulateMoldButton {
    background-color: #5856d6; /* Purple accent */
    transition: background-color 0.2s ease;
}
#analyzeToleranceButton:hover, #simulateMoldButton:hover {
    background-color: #4341a0;
    box-shadow: 0 2px 5px rgba(88, 86, 214, 0.2);;
}

#resetViewButton {
    background-color: #6c757d;
}
#resetViewButton:hover {
    background-color: #5a6268;
    box-shadow: none;
}

.status-display {
    margin-top: 20px;
    padding-top: 15px;
    border-top: 1px solid #c8c8cc;
}

.status-display h3 {
    margin-top: 0;
    margin-bottom: 10px;
    font-size: 1.0em;
    font-weight: 600;
    color: var(--text-secondary);
}

#statusText {
    font-size: 0.9em;
    color: var(--text-primary);
    min-height: 3em;
}

.visualization-area {
    flex-grow: 1;
    position: relative;
    background-color: var(--secondary-bg);
    overflow: hidden;
}

#rendererContainer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.app-footer {
    text-align: center;
    padding: 10px 20px;
    border-top: 1px solid var(--border-color);
    background-color: var(--primary-bg);
    color: var(--text-secondary);
    font-size: 0.85em;
}

/* Responsive adjustments */
@media (max-width: 768px) {
    .main-content-area {
        flex-direction: column;
    }

    .controls-panel {
        width: 100%;
        border-right: none;
        border-bottom: 1px solid var(--border-color);
        max-height: 60vh; /* Allow a bit more height for controls */
    }

    .visualization-area {
       height: 40vh;
       min-height: 250px;
    }

    .app-header h1 {
        font-size: 1.5em;
    }
} 

script.js

javascript 复制代码
// script.js - Parametric 3D Product Design Component

document.addEventListener('DOMContentLoaded', () => {
    // --- DOM Elements ---
    const rendererContainer = document.getElementById('rendererContainer');
    const partWidthSlider = document.getElementById('partWidth');
    const partHeightSlider = document.getElementById('partHeight');
    const partDepthSlider = document.getElementById('partDepth');
    const bossDiameterSlider = document.getElementById('bossDiameter');
    const bossHeightSlider = document.getElementById('bossHeight');
    const bossPosXSlider = document.getElementById('bossPosX');
    const bossPosZSlider = document.getElementById('bossPosZ');
    const dimensionToleranceSlider = document.getElementById('dimensionTolerance');
    const positionToleranceSlider = document.getElementById('positionTolerance');

    const partWidthValueSpan = document.getElementById('partWidthValue');
    const partHeightValueSpan = document.getElementById('partHeightValue');
    const partDepthValueSpan = document.getElementById('partDepthValue');
    const bossDiameterValueSpan = document.getElementById('bossDiameterValue');
    const bossHeightValueSpan = document.getElementById('bossHeightValue');
    const bossPosXValueSpan = document.getElementById('bossPosXValue');
    const bossPosZValueSpan = document.getElementById('bossPosZValue');
    const dimensionToleranceValueSpan = document.getElementById('dimensionToleranceValue');
    const positionToleranceValueSpan = document.getElementById('positionToleranceValue');

    const analyzeToleranceButton = document.getElementById('analyzeToleranceButton');
    const simulateMoldButton = document.getElementById('simulateMoldButton');
    const resetViewButton = document.getElementById('resetViewButton');
    const statusText = document.getElementById('statusText');

    // --- Three.js Setup ---
    let scene, camera, renderer, controls, partGroup, baseMesh, bossMesh;
    let toleranceVizGroup, dimTolMeshMin, dimTolMeshMax, posTolViz;
    let moldHalfTop, moldHalfBottom; // For mold simulation
    let material, toleranceMaterialMin, toleranceMaterialMax, positionToleranceMaterial;
    let isToleranceVisible = false;
    let isMoldOpen = false;
    let isAnimating = false;

    // Conversion factor (e.g., if sliders are mm, and Three.js unit is meters)
    // Let's work directly in 'mm' like units in Three.js for simplicity here.
    const scaleFactor = 1;

    function initThreeJS() {
        // Scene
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xf5f5f7);

        // Camera
        const aspect = rendererContainer.clientWidth / rendererContainer.clientHeight;
        camera = new THREE.PerspectiveCamera(50, aspect, 1, 2000); // Adjusted near/far
        camera.position.set(100, 80, 150); // Adjusted for 'mm' scale
        camera.lookAt(scene.position);

        // Renderer
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(rendererContainer.clientWidth, rendererContainer.clientHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        rendererContainer.appendChild(renderer.domElement);

        // Lights
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
        scene.add(ambientLight);
        const keyLight = new THREE.DirectionalLight(0xffffff, 0.6);
        keyLight.position.set(-50, 80, 50);
        scene.add(keyLight);
        const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
        fillLight.position.set(50, 40, -30);
        scene.add(fillLight);

        // Controls
        controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.1;

        // Materials
        material = new THREE.MeshStandardMaterial({
            color: 0x99999f, // Lighter gray plastic
            metalness: 0.1,
            roughness: 0.6,
            polygonOffset: true, // Helps prevent z-fighting with tolerance viz
            polygonOffsetFactor: 1,
            polygonOffsetUnits: 1
        });

        // Tolerance Materials (Semi-transparent)
        toleranceMaterialMin = new THREE.MeshBasicMaterial({
            color: 0xff0000,
            transparent: true,
            opacity: 0.2,
            side: THREE.DoubleSide,
            depthWrite: false // Render after main object
        });
        toleranceMaterialMax = new THREE.MeshBasicMaterial({
            color: 0x00ff00,
            transparent: true,
            opacity: 0.2,
            side: THREE.DoubleSide,
            depthWrite: false
        });
         positionToleranceMaterial = new THREE.MeshBasicMaterial({
            color: 0x0000ff,
            transparent: true,
            opacity: 0.3,
            side: THREE.DoubleSide,
            depthWrite: false
        });

        // Part Group
        partGroup = new THREE.Group();
        scene.add(partGroup);

        // Tolerance Visualization Group
        toleranceVizGroup = new THREE.Group();
        toleranceVizGroup.visible = false;
        scene.add(toleranceVizGroup);

        // Initial Part Creation
        createPart();

        // Start animation loop
        animate();
    }

    // --- Parametric Part Generation ---
    function createPart() {
        // Clear previous part within the group
        while (partGroup.children.length > 0) {
            const child = partGroup.children[0];
            partGroup.remove(child);
            if (child.geometry) child.geometry.dispose();
        }

        // Get parameters
        const width = parseFloat(partWidthSlider.value) * scaleFactor;
        const height = parseFloat(partHeightSlider.value) * scaleFactor;
        const depth = parseFloat(partDepthSlider.value) * scaleFactor;
        const bossD = parseFloat(bossDiameterSlider.value) * scaleFactor;
        const bossH = parseFloat(bossHeightSlider.value) * scaleFactor;
        const bossPosXRatio = parseFloat(bossPosXSlider.value) / 100;
        const bossPosZRatio = parseFloat(bossPosZSlider.value) / 100;

        // Create Base Box
        const baseGeom = new THREE.BoxGeometry(width, height, depth);
        baseMesh = new THREE.Mesh(baseGeom, material);
        // Position base so its bottom is at y=0
        baseMesh.position.y = height / 2;
        partGroup.add(baseMesh);

        // Create Boss (Cylinder)
        const bossGeom = new THREE.CylinderGeometry(bossD / 2, bossD / 2, bossH, 32);
        bossMesh = new THREE.Mesh(bossGeom, material);

        // Position Boss on top surface of the base
        // Calculate position relative to the base center
        const bossX = (bossPosXRatio - 0.5) * width;
        const bossZ = (bossPosZRatio - 0.5) * depth;
        bossMesh.position.set(bossX, height + bossH / 2, bossZ); // Y is base height + half boss height
        bossMesh.rotation.x = 0; // Ensure cylinder is upright
        partGroup.add(bossMesh);

        // --- Update Tolerance Visualizations ---
        // (Do this separately, only when needed, to avoid recreating on every param change)
        if (isToleranceVisible) {
             createToleranceVisualization(); // Recreate tolerance based on new nominal part
        }

        updateStatus("零件模型已更新。");
    }

    // --- Tolerance Visualization ---
    function createToleranceVisualization() {
        // Clear previous visualization
        while (toleranceVizGroup.children.length > 0) {
            const child = toleranceVizGroup.children[0];
            toleranceVizGroup.remove(child);
            if (child.geometry) child.geometry.dispose();
            // Dispose materials if specific to tolerance viz? No, using shared ones.
        }

        const dimTol = parseFloat(dimensionToleranceSlider.value) * scaleFactor;
        const posTol = parseFloat(positionToleranceSlider.value) * scaleFactor;

        // Get current nominal dimensions
        const width = parseFloat(partWidthSlider.value) * scaleFactor;
        const height = parseFloat(partHeightSlider.value) * scaleFactor;
        const depth = parseFloat(partDepthSlider.value) * scaleFactor;
        const bossD = parseFloat(bossDiameterSlider.value) * scaleFactor;
        const bossH = parseFloat(bossHeightSlider.value) * scaleFactor;
        const bossX = (parseFloat(bossPosXSlider.value)/100 - 0.5) * width;
        const bossZ = (parseFloat(bossPosZSlider.value)/100 - 0.5) * depth;
        const bossNominalY = height + bossH / 2;

        // 1. Dimension Tolerance (Min/Max Boxes for Base) - Conceptual
        const baseMinGeom = new THREE.BoxGeometry(width - 2 * dimTol, height - 2 * dimTol, depth - 2 * dimTol);
        dimTolMeshMin = new THREE.Mesh(baseMinGeom, toleranceMaterialMin);
        dimTolMeshMin.position.y = (height - 2 * dimTol) / 2; // Adjust position for new height
        toleranceVizGroup.add(dimTolMeshMin);

        const baseMaxGeom = new THREE.BoxGeometry(width + 2 * dimTol, height + 2 * dimTol, depth + 2 * dimTol);
        dimTolMeshMax = new THREE.Mesh(baseMaxGeom, toleranceMaterialMax);
        dimTolMeshMax.position.y = (height + 2 * dimTol) / 2;
        toleranceVizGroup.add(dimTolMeshMax);

        // 2. Position Tolerance (Cylindrical Zone for Boss Centerline) - Conceptual
        // Create a thin cylinder representing the tolerance zone diameter
        const posTolGeom = new THREE.CylinderGeometry(posTol, posTol, height + bossH + dimTol * 2, 32); // Height spans base + boss + tolerance
        posTolViz = new THREE.Mesh(posTolGeom, positionToleranceMaterial);
        // Position it at the nominal boss X, Z, centered vertically within its height
        posTolViz.position.set(bossX, (height + bossH + dimTol*2) / 2, bossZ);
        toleranceVizGroup.add(posTolViz);

        updateStatus("公差范围已更新。");
    }

    // --- Animation & Rendering Loop ---
    function animate() {
        requestAnimationFrame(animate);
        controls.update();
        renderer.render(scene, camera);
    }

    // --- Event Listeners ---
    function setupEventListeners() {
        // Parameter Sliders
        const sliders = [partWidthSlider, partHeightSlider, partDepthSlider,
                         bossDiameterSlider, bossHeightSlider, bossPosXSlider, bossPosZSlider,
                         dimensionToleranceSlider, positionToleranceSlider];
        sliders.forEach(slider => {
            slider.addEventListener('input', handleSliderInput);
            slider.addEventListener('change', handleSliderChange); // Update part only on release
        });

        // Action Buttons
        analyzeToleranceButton.addEventListener('click', toggleToleranceAnalysis);
        simulateMoldButton.addEventListener('click', simulateMoldOpening);
        resetViewButton.addEventListener('click', resetCameraView);

        // Window Resize
        window.addEventListener('resize', onWindowResize);
    }

    function handleSliderInput(event) {
        const sliderId = event.target.id;
        const valueSpan = document.getElementById(sliderId + 'Value');
        let value = parseFloat(event.target.value);
        let unit = '';

        // Determine unit and formatting
        if (sliderId.includes('Tolerance')) {
            valueSpan.textContent = value.toFixed(2);
            unit = 'mm';
        } else if (sliderId.includes('Pos')) {
             valueSpan.textContent = value.toFixed(0);
             unit = '%';
        } else if (sliderId.includes('Diameter') || sliderId.includes('bossHeight')) {
            valueSpan.textContent = value.toFixed(1);
            unit = 'mm';
        } else {
            valueSpan.textContent = value.toFixed(0);
            unit = 'mm';
        }

        // Find the next sibling span (if exists) to update unit? No, hardcoded in HTML.

        // Live update for tolerance sliders if tolerance is visible
        if (isToleranceVisible && (sliderId.includes('Tolerance') || sliderId.includes('Pos'))) {
           createToleranceVisualization();
        }
    }

    function handleSliderChange(event) {
         // Recreate the main part geometry only when slider drag finishes
         if (!isAnimating && !event.target.id.includes('Tolerance') && !event.target.id.includes('Pos')) {
             createPart();
         } else if (!isAnimating && (event.target.id.includes('Pos'))) {
             // If only position changed, update part and tolerance if visible
             createPart();
         }
         // Tolerance slider changes already handled live in input if visible
    }


    // --- Button Actions ---
    function toggleToleranceAnalysis() {
        isToleranceVisible = !isToleranceVisible;
        if (isToleranceVisible) {
            createToleranceVisualization();
            toleranceVizGroup.visible = true;
            analyzeToleranceButton.textContent = "隐藏公差范围";
            updateStatus("显示公差范围 (概念性)。");
        } else {
            toleranceVizGroup.visible = false;
            analyzeToleranceButton.textContent = "显示公差范围";
            updateStatus("公差范围已隐藏。");
        }
    }

    function simulateMoldOpening() {
        if (isAnimating) return;
        isAnimating = true;
        simulateMoldButton.disabled = true;
        resetViewButton.disabled = true;
        analyzeToleranceButton.disabled = true;

        // Simple simulation: create two halves and move them apart
        if (!moldHalfTop || !moldHalfBottom) {
            // Create conceptual mold halves (simple boxes)
            const width = parseFloat(partWidthSlider.value) * scaleFactor + 20; // Mold bigger than part
            const height = parseFloat(partHeightSlider.value) * scaleFactor / 2 + 20;
            const depth = parseFloat(partDepthSlider.value) * scaleFactor + 20;
            const moldMaterial = new THREE.MeshStandardMaterial({ color: 0x555555, metalness: 0.8, roughness: 0.5 });

            const moldGeom = new THREE.BoxGeometry(width, height, depth);
            moldHalfBottom = new THREE.Mesh(moldGeom, moldMaterial);
            moldHalfTop = new THREE.Mesh(moldGeom, moldMaterial);

            // Position halves relative to the part's center plane (y=height/2)
            const partHeight = parseFloat(partHeightSlider.value) * scaleFactor;
            moldHalfBottom.position.y = partHeight/2 - height/2; // Center of bottom mold half at part center plane - half mold height
            moldHalfTop.position.y = partHeight/2 + height/2;    // Center of top mold half at part center plane + half mold height

            scene.add(moldHalfBottom);
            scene.add(moldHalfTop);
            partGroup.visible = false; // Hide original part
        }

        const targetSeparation = parseFloat(partHeightSlider.value) * scaleFactor * 1.5;
        const startYTop = moldHalfTop.position.y;
        const startYBottom = moldHalfBottom.position.y;
        const targetYTop = startYTop + targetSeparation / 2;
        const targetYBottom = startYBottom - targetSeparation / 2;
        const duration = 1000; // ms
        let startTime = null;

        function moldStep(timestamp) {
            if (!startTime) startTime = timestamp;
            const elapsed = timestamp - startTime;
            const progress = Math.min(elapsed / duration, 1);
            const easedProgress = 0.5 - 0.5 * Math.cos(progress * Math.PI);

            moldHalfTop.position.y = startYTop + (targetYTop - startYTop) * easedProgress;
            moldHalfBottom.position.y = startYBottom + (targetYBottom - startYBottom) * easedProgress;

            if (progress < 1) {
                requestAnimationFrame(moldStep);
            } else {
                 updateStatus("模拟开模完成。再次点击可复位。");
                 isAnimating = false;
                 simulateMoldButton.disabled = false;
                 resetViewButton.disabled = false;
                 analyzeToleranceButton.disabled = false;
                 isMoldOpen = true;
                 simulateMoldButton.textContent = "复位模具";
            }
        }

        function resetMold() {
             // Animation to close the mold
             isAnimating = true;
             simulateMoldButton.disabled = true;
             resetViewButton.disabled = true;
             analyzeToleranceButton.disabled = true;

             const currentYTop = moldHalfTop.position.y;
             const currentYBottom = moldHalfBottom.position.y;
             // Calculate original positions correctly based on part height and mold height
             const partHeight = parseFloat(partHeightSlider.value) * scaleFactor;
             const moldHeight = parseFloat(partHeightSlider.value) * scaleFactor / 2 + 20;
             const originalYTop = partHeight / 2 + moldHeight / 2;
             const originalYBottom = partHeight / 2 - moldHeight / 2;
             const durationClose = 800;
             let startTimeClose = null;

             function closeStep(timestamp) {
                 if (!startTimeClose) startTimeClose = timestamp;
                 const elapsed = timestamp - startTimeClose;
                 const progress = Math.min(elapsed / durationClose, 1);
                 const easedProgress = 0.5 - 0.5 * Math.cos(progress * Math.PI);

                 moldHalfTop.position.y = currentYTop + (originalYTop - currentYTop) * easedProgress;
                 moldHalfBottom.position.y = currentYBottom + (originalYBottom - currentYBottom) * easedProgress;

                 if (progress < 1) {
                     requestAnimationFrame(closeStep);
                 } else {
                     scene.remove(moldHalfTop);
                     scene.remove(moldHalfBottom);
                     moldHalfTop = null;
                     moldHalfBottom = null;
                     partGroup.visible = true; // Show part again
                     updateStatus("模具已复位。");
                     isAnimating = false;
                     simulateMoldButton.disabled = false;
                     resetViewButton.disabled = false;
                     analyzeToleranceButton.disabled = false;
                     isMoldOpen = false;
                     simulateMoldButton.textContent = "模拟开模 (概念)";
                 }
             }
             requestAnimationFrame(closeStep);
        }

        if (isMoldOpen) {
            resetMold();
        } else {
             updateStatus("模拟开模过程...");
             requestAnimationFrame(moldStep);
        }
    }

    function resetCameraView() {
        controls.reset();
        // Adjust position based on current part size? Or fixed reset?
        const currentHeight = parseFloat(partHeightSlider.value) * scaleFactor;
        camera.position.set(100, 80 + currentHeight/2, 150);
        camera.lookAt(0, currentHeight / 2, 0); // Look at center of base
        controls.update();
        updateStatus("视图已重置。");
    }

    // --- Utility Functions ---
    function updateStatus(message) {
        statusText.textContent = message;
        console.log("Status:", message);
    }

    function onWindowResize() {
        if (!renderer || !camera) return;
        const width = rendererContainer.clientWidth;
        const height = rendererContainer.clientHeight;

        camera.aspect = width / height;
        camera.updateProjectionMatrix();
        renderer.setSize(width, height);
    }

    // --- Initialization Call ---
    try {
        initThreeJS();
        setupEventListeners();
        updateStatus("参数化产品设计组件初始化成功。");
    } catch (error) {
        console.error("初始化失败:", error);
        updateStatus(`错误: ${error.message}`);
        rendererContainer.innerHTML = `<p style='color: red; padding: 20px;'>无法加载3D视图。错误: ${error.message}</p>`;
    }
}); 
相关推荐
荔枝味啊~22 分钟前
相机位姿估计
人工智能·计算机视觉·3d
Boilermaker19921 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y2 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁2 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart