76个工业组件库示例汇总
参数化三维产品设计组件 (注塑模具与公差分析)
概述
这是一个交互式的 Web 组件,旨在演示简单的三维零件(如带凸台的方块)的参数化设计过程,并结合注塑模具设计(如开模动画)与公差分析(如可视化公差带)的概念性可视化。
主要功能
- 参数化零件建模:
- 通过滑块实时修改零件的基础尺寸(宽度、高度、深度)。
- 通过滑块实时修改特征(圆柱凸台)的尺寸(直径、高度)和相对位置。
- 实时 3D 可视化:
- 使用 Three.js 渲染零件模型。
- 支持通过鼠标进行视图交互(旋转、缩放、平移)。
- 概念性分析与模拟:
- 公差可视化: 切换显示尺寸公差(最小/最大包络盒)和位置公差(特征允许范围)的理论边界。
- 模拟开模: 播放简化的动画,展示模具上下型腔分离的过程。
- 界面与风格:
- 采用苹果科技风格,界面简洁直观。
- 响应式布局,适应不同屏幕尺寸。
如何使用
- 打开页面: 在浏览器中打开
index.html
文件。 - 调整参数:
- 在左侧面板的 “基础零件尺寸” 和 “特征: 圆柱凸台” 部分,拖动滑块调整零件的几何参数。
- 3D 模型会在释放滑块后更新。
- 交互视图:
- 在右侧 3D 视图区,按住鼠标 左键拖动 进行旋转。
- 滚动鼠标滚轮 进行缩放。
- 按住鼠标 右键 (或 Ctrl/Cmd + 左键) 拖动 进行平移。
- 公差分析 (概念):
- 在 “公差分析” 部分调整公差值滑块。
- 点击 “显示/隐藏公差范围” 按钮,切换显示红/绿/蓝色透明几何体,表示理论上的公差带。
- 模拟开模 (概念):
- 点击 “模拟开模 (概念)” 按钮,观看模具分离动画(零件会暂时隐藏)。
- 再次点击(按钮变为 “复位模具”) 可观看模具闭合动画,并重新显示零件。
- 重置视图: 点击 “重置视图” 按钮,将相机恢复到默认位置和朝向。
文件结构
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
<!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
/* 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
// 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>`;
}
});