目录
[加载glb stl代码:](#加载glb stl代码:)
切割失败:
bash
const negPart = csgEvaluator.evaluate(sb, positiveBox, SUBTRACTION);
const posPart = csgEvaluator.evaluate(sb, negativeBox, SUBTRACTION);
bash
function sanitizeGeometry(g) {
const clean = new THREE.BufferGeometry();
clean.setAttribute('position', g.getAttribute('position'));
clean.setAttribute('normal', g.getAttribute('normal'));
clean.clearGroups();
clean.addGroup(0, clean.attributes.position.count, 0);
return clean;
}
sb.geometry = sanitizeGeometry(sb.geometry);
positiveBox.geometry = sanitizeGeometry(positiveBox.geometry);
negativeBox.geometry = sanitizeGeometry(negativeBox.geometry);
console.log(
Object.keys(sb.geometry.attributes)
);
console.log(
Object.keys(positiveBox.geometry.attributes)
);
console.log(`sb.geometry.index`);
const pos = sb.geometry.attributes.position.array;
let bad = 0;
for (let i = 0; i < pos.length; i++) {
if (!Number.isFinite(pos[i])) {
console.log("Bad:", i, pos[i]);
bad++;
break;
}
}
const positions = sb.geometry.attributes.position.array;
for (let i = 0; i < positions.length; i++) {
if (!isFinite(positions[i])) {
console.error(`${name}: 发现无效顶点数据 at index ${i}`);
positions[i] = 0; // 修复无效数据
}
}
加载glb stl代码:
html
// new GLTFLoader().load(MODEL_PATH, gltf => {
// const model = gltf.scene;
// model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
// model.position.set(0, -0.3, 0); model.scale.set(0.8, 0.8, 0.8); scene.add(model);
// originalModelGroup = model.clone(); originalModelGroup.visible = false; scene.add(originalModelGroup);
// currentModelObject = model;
// cutPlaneMesh = createVisualCutPlane(currentCutPlaneSize);
// cutPlaneMesh.position.set(0, 0, 0);
// selectObject('model');
// const box = new THREE.Box3().setFromObject(model); const ct = box.getCenter(new THREE.Vector3()); const sz = box.getSize(new THREE.Vector3()); const md = Math.max(sz.x, sz.y, sz.z);
// camera.position.set(ct.x + md * 1.3, ct.y + md * 0.9, ct.z + md * 1.6); orbitControls.target.copy(ct); orbitControls.update();
// }, undefined, err => {
// console.error("默认模型加载失败:", err);
// const fg = new THREE.BoxGeometry(1.6, 1.6, 1.6); const fm = new THREE.Mesh(fg, new THREE.MeshStandardMaterial({ color: 0x88aaff, metalness: 0.5, roughness: 0.3 }));
// fm.position.y = 0; scene.add(fm);
// originalModelGroup = fm.clone(); originalModelGroup.visible = false; scene.add(originalModelGroup);
// currentModelObject = fm;
// cutPlaneMesh = createVisualCutPlane(currentCutPlaneSize);
// cutPlaneMesh.position.set(0, 0, 0);
// selectObject('model');
// });
new STLLoader().load(MODEL_PATH, geometry => {
// STL 返回的是 BufferGeometry,需要手动创建 Mesh
const material = new THREE.MeshStandardMaterial({ color: 0x88aaff, metalness: 0.5, roughness: 0.3 });
const model = new THREE.Mesh(geometry, material);
model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
model.position.set(0, -0.3, 0);
model.scale.set(0.8, 0.8, 0.8);
scene.add(model);
// 保存原始模型(注意:STL 没有 clone 层级结构,需要用 geometry 单独处理)
originalModelGroup = model.clone();
originalModelGroup.visible = false;
scene.add(originalModelGroup);
currentModelObject = model;
cutPlaneMesh = createVisualCutPlane(currentCutPlaneSize);
cutPlaneMesh.position.set(0, 0, 0);
selectObject('model');
// 计算包围盒并调整相机
geometry.computeBoundingBox();
const box = geometry.boundingBox;
const ct = new THREE.Vector3(
(box.min.x + box.max.x) / 2,
(box.min.y + box.max.y) / 2,
(box.min.z + box.max.z) / 2
);
const sz = new THREE.Vector3(
box.max.x - box.min.x,
box.max.y - box.min.y,
box.max.z - box.min.z
);
const md = Math.max(sz.x, sz.y, sz.z);
camera.position.set(ct.x + md * 1.3, ct.y + md * 0.9, ct.z + md * 1.6);
orbitControls.target.copy(ct);
orbitControls.update();
}, undefined, err => {
console.error("STL 模型加载失败:", err);
// fallback 逻辑保持不变
});
复杂模型报错:
E:\project\3d_label\three-bvh-csg-main\examples\demo.html
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Stable GLB BVH CSG Cut</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"three-bvh-csg": "https://unpkg.com/three-bvh-csg@0.0.18/build/index.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { Brush, Evaluator, INTERSECTION, SUBTRACTION } from "three-bvh-csg";
/* ===================== 基础场景 ===================== */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(3, 3, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 5);
scene.add(light);
/* ===================== CSG ===================== */
const evaluator = new Evaluator();
/* ===================== GLB ===================== */
const loader = new GLTFLoader();
function cleanGeometry(mesh) {
mesh.updateWorldMatrix(true, false);
let geo = mesh.geometry.clone();
// ❗ 1. 转 world space
geo.applyMatrix4(mesh.matrixWorld);
// ❗ 2. 确保有 position attribute
if (!geo.attributes || !geo.attributes.position) {
console.warn("Invalid geometry skipped");
return null;
}
// ❗ 3. 转 indexed(关键)
if (!geo.index) {
geo = THREE.BufferGeometryUtils.mergeVertices(geo);
}
// ❗ 4. 清理 NaN / 冗余
geo.computeVertexNormals();
geo.computeBoundingBox();
geo.computeBoundingSphere();
return geo;
}
loader.load("yotown_clear.glb", (gltf) => {
const model = gltf.scene;
scene.add(model);
model.updateMatrixWorld(true);
/* ===================== 计算包围盒 ===================== */
const box = new THREE.Box3().setFromObject(model);
const center = new THREE.Vector3();
const size = new THREE.Vector3();
box.getCenter(center);
box.getSize(size);
console.log("center:", center);
console.log("size:", size);
/* ===================== 创建切割平面 ===================== */
const planeGeo = new THREE.PlaneGeometry(200, 200);
const planeMesh = new THREE.Mesh(
planeGeo,
new THREE.MeshBasicMaterial()
);
planeMesh.position.copy(center);
planeMesh.rotation.x = Math.PI / 2; // 水平切割(XZ 平面)
/* ===================== 收集 meshes ===================== */
const meshes = [];
model.traverse(o => {
if (o.isMesh) {
o.geometry.computeVertexNormals();
meshes.push(o);
}
});
const upperGroup = new THREE.Group();
const lowerGroup = new THREE.Group();
/* ===================== 执行切割 ===================== */
meshes.forEach(mesh => {
const geometry = cleanGeometry(mesh);
if (!geometry) return;
const brushA = new Brush(geometry);
/* ===== 切割体 ===== */
const upperPlane = new Brush(new THREE.BoxGeometry(1000, 1000, 1000));
upperPlane.position.set(center.x, center.y + 1000, center.z);
upperPlane.updateMatrixWorld(true);
const lowerPlane = new Brush(new THREE.BoxGeometry(1000, 1000, 1000));
lowerPlane.position.set(center.x, center.y - 1000, center.z);
lowerPlane.updateMatrixWorld(true);
/* ===== 上半 ===== */
const upper = evaluator.evaluate(
brushA,
upperPlane,
INTERSECTION
);
if (upper?.geometry?.attributes?.position) {
upperGroup.add(new THREE.Mesh(upper.geometry, mesh.material.clone()));
}
/* ===== 下半 ===== */
const lower = evaluator.evaluate(
brushA,
lowerPlane,
INTERSECTION
);
if (lower?.geometry?.attributes?.position) {
const m = new THREE.Mesh(lower.geometry, mesh.material.clone());
m.position.x += size.x * 1.2;
lowerGroup.add(m);
}
});
scene.add(upperGroup);
scene.add(lowerGroup);
});
/* ===================== 渲染 ===================== */
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>