news 2026/6/13 19:11:20

three-bvh-csg 球形卡扣 自动生成源代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
three-bvh-csg 球形卡扣 自动生成源代码

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Three.js 模型切割 - 点击就近选择(智能交互版)</title> <style> body { margin: 0; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } #info { position: absolute; top: 20px; left: 20px; background: rgba(0, 0, 0, 0.7); color: white; padding: 12px 20px; border-radius: 8px; backdrop-filter: blur(8px); pointer-events: none; z-index: 10; font-size: 14px; border-left: 4px solid #ff4757; } #controls-panel { position: absolute; bottom: 20px; left: 20px; right: 20px; background: rgba(30, 30, 40, 0.95); backdrop-filter: blur(12px); border-radius: 16px; padding: 15px 20px; color: white; display: flex; flex-wrap: wrap; gap: 15px; justify-content: space-between; align-items: center; z-index: 20; pointer-events: auto; border: 1px solid rgba(255, 255, 255, 0.2); font-family: monospace; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); } .group { display: flex; gap: 12px; align-items: center; background: rgba(0, 0, 0, 0.5); padding: 6px 16px; border-radius: 40px; } .group label { font-size: 13px; font-weight: bold; letter-spacing: 1px; } button { background: #ff4757; border: none; color: white; padding: 8px 20px; border-radius: 40px; cursor: pointer; font-weight: bold; transition: 0.2s; font-size: 14px; } button:hover { background: #ff6b81; transform: scale(1.02); } .reset-btn { background: #2ed573; } .reset-btn:hover { background: #5fdd9a; } .mode-btn { background: #1e90ff; } .mode-btn.active { background: #ffa502; box-shadow: 0 0 8px rgba(255, 165, 2, 0.5); } .select-btn { background: #9b59b6; } .select-btn.active { background: #e67e22; box-shadow: 0 0 12px rgba(230, 126, 34, 0.6); } .export-stl-btn { background: #00b894; } .export-stl-btn:hover { background: #55efc4; color: #2d3436; } .export-glb-btn { background: #0984e3; } .export-glb-btn:hover { background: #74b9ff; color: #2d3436; } .tenon-btn { background: #ff6348; } .tenon-btn:hover { background: #ff7f50; } .start-cut-btn { background: #00d2d3; } .start-cut-btn:hover { background: #48dbfb; color: #2d3436; } input { width: 140px; cursor: pointer; } .value-display { background: #000000aa; padding: 4px 12px; border-radius: 20px; min-width: 70px; text-align: center; font-size: 12px; } .selected-indicator { background: #e67e22; color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; } .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: none; justify-content: center; align-items: center; z-index: 1000; flex-direction: column; } .loading-spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #00b894; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-text { color: white; margin-top: 20px; font-size: 16px; } @media (max-width: 800px) { .group { padding: 4px 10px; } input { width: 80px; } button { padding: 6px 12px; font-size: 12px; } } .status { font-size: 11px; opacity: 0.7; } .auto-size { color: #ffa502; font-weight: bold; } .info-text { font-size: 11px; color: #aaa; margin-left: 10px; } .export-group { gap: 8px; } .export-group button { padding: 8px 16px; } .upload-label { background: #f39c12; padding: 8px 16px; border-radius: 40px; cursor: pointer; font-weight: bold; transition: 0.2s; font-size: 14px; display: inline-block; } .upload-label:hover { background: #f1c40f; transform: scale(1.02); } #file-input { display: none; } .model-selector { background: #2c3e50; padding: 4px 12px; border-radius: 20px; display: flex; gap: 8px; align-items: center; } .model-selector select { background: #34495e; color: white; border: none; padding: 6px 12px; border-radius: 20px; cursor: pointer; font-family: monospace; } .model-selector button { background: #e74c3c; padding: 4px 12px; font-size: 12px; } .click-hint { position: absolute; bottom: 80px; left: 20px; background: rgba(0, 0, 0, 0.5); color: #ffaa66; padding: 4px 12px; border-radius: 20px; font-size: 12px; pointer-events: none; z-index: 10; } </style> </head> <body> <div>实体切割+自动补面 | 点击模型自动就近选择 | 智能切换操控手柄</div> <div><div></div><div>正在导出优化模型...</div></div> <div> <div><span>🎯</span><button>📦 部件</button><button>🔪 切面</button><span>当前: 自动</span></div> <div> <span>📋 模型</span> <select></select> <button style="background:#e74c3c;">🗑️ 删除</button> </div> <div><span>🛠️</span><button>移动</button><button>旋转</button><button>缩放</button></div> <div><span>📏 切面</span><input type="range" min="0.5" max="4.0" step="0.01" value="1.5"><span>1.50</span></div> <div><span>📐 比例</span><input type="range" min="0.02" max="0.5" step="0.01" value="0.125"><span>1/8</span></div> <div><span>⚙️ 公差</span><input type="range" min="0.005" max="0.02" step="0.001" value="0.005"><span>0.005mm</span></div> <div><span>🔩 榫头</span><span>自动</span><span style="color:#ff6348;">卯眼+公差</span></div> <div> <button>🪚 插入切面</button> <button style="background:#ff6b6b;">✂️ 执行切割</button> <button>🔩 榫卯</button> <button>🔄 重置</button> </div> <div> <label for="file-input">📁 导入GLB</label> <input type="file" accept=".glb,.gltf"> <button>📄 导出 STL</button> <button>📦 导出 GLB</button> </div> <div>💡 点击任意模型/部件就近选中 | 切面可独立操控 | C/X/R/E</div> </div> <div>✨ 点击场景中任意模型即可自动选中并操控</div> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.128.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/", "three-bvh-csg": "https://unpkg.com/three-bvh-csg@0.0.18/index.module.js" } } </script> <script type="module"> import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { TransformControls } from 'three/addons/controls/TransformControls.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { STLExporter } from 'three/addons/exporters/STLExporter.js'; import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js'; import { Brush, Evaluator, INTERSECTION, SUBTRACTION, ADDITION } from 'three-bvh-csg'; import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js'; import { BufferAttribute } from 'three'; // ==================== 全局常量 ==================== const CUT_PLANE_THICKNESS = 0.002; const HALF_SPACE_SIZE = 10.0; const DEFAULT_PLANE_SIZE = 1.5; const DEFAULT_RATIO = 0.125; const DEFAULT_TOLERANCE = 0.002; const TENON_OFFSET_RATIO = 0.30; // ==================== 辅助函数 ==================== function cleanGeometry(geometry) { if (!geometry || !geometry.attributes.position) return geometry; let geo = geometry.clone(); try { geo = mergeVertices(geo, 0.0001); geo.computeVertexNormals(); } catch (e) { console.warn("几何体清理失败:", e); } return geo; } class TimeStr { constructor() { this.lastTime = ''; this.counter = 0; } /** * 格式化日期 * @param {string} fmt - 格式化模板,支持 YYYY MM DD HH mm ss * @returns {string} */ _formatDate(fmt) { const now = new Date(); const map = { 'YYYY': now.getFullYear(), 'MM': String(now.getMonth() + 1).padStart(2, '0'), 'DD': String(now.getDate()).padStart(2, '0'), 'HH': String(now.getHours()).padStart(2, '0'), 'mm': String(now.getMinutes()).padStart(2, '0'), 'ss': String(now.getSeconds()).padStart(2, '0'), }; let result = fmt; for (const [key, value] of Object.entries(map)) { result = result.replace(key, value); } return result; } getTime(fmt = "MMDD_HH_ss", baseFmt = "MMDDHH") { const nowTime = this._formatDate(fmt); const baseTime = this._formatDate(baseFmt); if (baseTime !== this.lastTime) { this.lastTime = baseTime; this.counter = 0; } else { this.counter++; } return `${baseTime}_${this.counter}`; } } function centerGeometry(mesh) { const g = mesh.geometry; if (!g?.attributes.position) return; const p = g.attributes.position.array; if (!p.length) return; let mn = [Infinity, Infinity, Infinity], mx = [-Infinity, -Infinity, -Infinity]; for (let i = 0; i < p.length; i += 3) for (let j = 0; j < 3; j++) { mn[j] = Math.min(mn[j], p[i + j]); mx[j] = Math.max(mx[j], p[i + j]); } const c = [(mn[0] + mx[0]) / 2, (mn[1] + mx[1]) / 2, (mn[2] + mx[2]) / 2]; for (let i = 0; i < p.length; i += 3) { p[i] -= c[0]; p[i + 1] -= c[1]; p[i + 2] -= c[2]; } g.attributes.position.needsUpdate = true; g.computeVertexNormals(); mesh.position.x += c[0]; mesh.position.y += c[1]; mesh.position.z += c[2]; mesh.updateMatrixWorld(); } function getPlaneAxes(cutPlaneMesh) { const pos = cutPlaneMesh.position.clone(); const quat = cutPlaneMesh.quaternion.clone(); const normal = new THREE.Vector3(0, 1, 0).applyQuaternion(quat).normalize(); const right = new THREE.Vector3(1, 0, 0).applyQuaternion(quat).normalize(); const up = new THREE.Vector3(0, 0, 1).applyQuaternion(quat).normalize(); return { pos, quat, normal, right, up }; } function worldToPlaneLocal(worldPoint, planeAxes) { const rel = worldPoint.clone().sub(planeAxes.pos); return { x: rel.dot(planeAxes.right), y: rel.dot(planeAxes.normal), z: rel.dot(planeAxes.up) }; } function obbIntersectsOBB(objBox, objMatrixWorld, planeAxes, planeSize) { const objPos = new THREE.Vector3(); const objQuat = new THREE.Quaternion(); const objScale = new THREE.Vector3(); objMatrixWorld.decompose(objPos, objQuat, objScale); const objSize = new THREE.Vector3(); objBox.getSize(objSize); const objHalfSize = objSize.clone().multiplyScalar(0.5); const planeHalfSize = new THREE.Vector3(planeSize / 2, CUT_PLANE_THICKNESS / 2, planeSize / 2); const centerDist = objPos.clone().sub(planeAxes.pos); const axes = [ new THREE.Vector3(1, 0, 0).applyQuaternion(objQuat), new THREE.Vector3(0, 1, 0).applyQuaternion(objQuat), new THREE .Vector3(0, 0, 1).applyQuaternion(objQuat), planeAxes.right, planeAxes.normal, planeAxes.up ]; for (let i = 0; i < 3; i++) for (let j = 3; j < 6; j++) { const cross = new THREE.Vector3().crossVectors(axes[i], axes[j]); if (cross.length() > 0.0001) axes.push(cross.normalize()); } for (const axis of axes) { if (axis.length() < 0.0001) continue; let objRadius = 0, planeRadius = 0; const [a, b, c] = [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)].map(v => v .applyQuaternion(objQuat)); for (let i = 0; i < 3; i++) { objRadius += Math.abs([a, b, c][i].dot(axis)) * objHalfSize.getComponent(i); planeRadius += Math.abs([planeAxes.right,
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 3:33:17

一次性医用耗材厂主要分布在哪里?

一次性医用耗材厂主要分布在哪里&#xff1f; 一次性医用耗材的生产工厂&#xff0c;主要集中在广东、江苏、浙江三省&#xff0c;其次是湖北&#xff08;仙桃&#xff09;、山东、河南等省份。不同品类的细分工厂分布有差异&#xff0c;以下从产品维度逐一拆解。 天下工厂是覆…

作者头像 李华
网站建设 2026/6/10 1:12:20

5分钟解锁百度网盘极速下载:完全免费的BaiduPCS-Web终极指南

5分钟解锁百度网盘极速下载&#xff1a;完全免费的BaiduPCS-Web终极指南 【免费下载链接】baidupcs-web 项目地址: https://gitcode.com/gh_mirrors/ba/baidupcs-web 还在为百度网盘的龟速下载而烦恼吗&#xff1f;每次下载大文件都要经历漫长的等待&#xff0c;甚至频…

作者头像 李华
网站建设 2026/6/10 1:10:55

G-Helper降压降温全攻略:让你的华硕游戏本安静运行还更凉爽

G-Helper降压降温全攻略&#xff1a;让你的华硕游戏本安静运行还更凉爽 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenboo…

作者头像 李华
网站建设 2026/6/10 1:08:06

使用 ventoy 安装WinToGo

参考视频 https://www.bilibili.com/video/BV1Gd4y1h7fC 1、Windows VHD 文件启动插件的安装 进入ventoy官网下载vhd的插件Plugin.WinVhdBoot . Ventoy 下载解压后&#xff0c;将Win10Based目录下的ventoy_vhdboot.img这个文件放入ventoy目录下&#xff0c;ventoy目录需要在i…

作者头像 李华
网站建设 2026/6/10 1:07:56

如何借助AI找到最划算的演出与机舱座位

购买机票时多花一笔钱抢靠窗位置&#xff0c;或者为了在演唱会和体育赛事中获得更好的视野而支付额外费用&#xff0c;向来是一件既私人、又充满情感纠结的事。被票价"割韭菜"的滋味&#xff0c;同样令人懊恼。有些人对特定区域甚至某个固定座位情有独钟&#xff0c;…

作者头像 李华