trimesh 3d 切割 2026

trimesh 3d 切割 2026
fenge2.pyimport trimesh import numpy as np def prepare_mesh(mesh): 预处理网格使其适合布尔运算 mesh mesh.copy() mesh.merge_vertices() mesh.fix_normals() if not mesh.is_watertight: mesh mesh.fill_holes() if hasattr(mesh, remove_degenerate_faces): mesh.remove_degenerate_faces() mesh.remove_unreferenced_vertices() mesh.remove_infinite_values() return mesh def bounds_intersect(a_bounds, b_bounds): 检查两个AABB是否相交 return not (a_bounds[0][0] b_bounds[1][0] or a_bounds[1][0] b_bounds[0][0] or a_bounds[0][1] b_bounds[1][1] or a_bounds[1][1] b_bounds[0][1] or a_bounds[0][2] b_bounds[1][2] or a_bounds[1][2] b_bounds[0][2]) def is_valid_mesh(geom, check_volumeTrue, min_vertices3, min_faces1): 检查网格是否有效原函数不变 if not isinstance(geom, trimesh.Trimesh): return False if geom.vertices is None or geom.faces is None: return False if geom.vertices.shape[0] min_vertices: return False if geom.faces.shape[0] min_faces: return False if not np.all(np.isfinite(geom.vertices)): return False if not np.all(np.isfinite(geom.faces)): return False if geom.faces.max() geom.vertices.shape[0]: return False if geom.faces.min() 0: return False try: bounds geom.bounds if not np.all(np.isfinite(bounds)): return False size bounds[1] - bounds[0] if np.any(size 0): return False except: return False if check_volume: try: volume geom.volume if abs(volume) 1e-8: size geom.bounds[1] - geom.bounds[0] if np.all(size 1e-6): pass except: return False try: triangles geom.vertices[geom.faces] v0 triangles[:, 1] - triangles[:, 0] v1 triangles[:, 2] - triangles[:, 0] cross np.cross(v0, v1) areas 0.5 * np.linalg.norm(cross, axis1) if np.mean(areas) 1e-10: return False except: pass try: used_vertices np.unique(geom.faces.flatten()) if len(used_vertices) geom.vertices.shape[0] * 0.5: pass except: pass return True def cut_scene_geometries(scene, enginemanifold): 对场景中的所有部件进行两两切割后面的被前面的切并记录交面中心。 返回 (新场景, 交面信息列表) 交面信息: [(name_i, name_j, center_global, area), ...] # area 为交面总面积 if not isinstance(scene, trimesh.Scene): raise ValueError(输入必须是 trimesh.Scene) # 1. 将所有部件变换到世界坐标系 geom_names [] world_geoms [] for name, geom in scene.geometry.items(): if not isinstance(geom, trimesh.Trimesh): continue if name in scene.graph: transform scene.graph[name][0] else: transform np.eye(4) vertices trimesh.transformations.transform_points(geom.vertices, transform) world_geom trimesh.Trimesh(verticesvertices, facesgeom.faces, processFalse) try: world_geom prepare_mesh(world_geom) print(f预处理 {name} 成功) except Exception as e: print(f预处理几何体 {name} 失败: {e}) geom_names.append(name) world_geoms.append(world_geom) # 2. 两两切割并记录交面 intersections [] # 存储 (name_i, name_j, center_global, area) for i in range(len(world_geoms)): for j in range(i1, len(world_geoms)): A world_geoms[i] B world_geoms[j] if not bounds_intersect(A.bounds, B.bounds): continue print(f处理: {geom_names[i]} vs {geom_names[j]}) # --- 计算交集记录交面中心 --- try: inter trimesh.boolean.intersection([A, B], engineengine) if inter is not None: if isinstance(inter, list) and len(inter) 0: combined trimesh.util.concatenate(inter) else: combined inter if isinstance(combined, trimesh.Trimesh) and combined.vertices.shape[0] 0 and combined.faces.shape[0] 0: # 计算交面中心 face_centers combined.vertices[combined.faces].mean(axis1) center np.mean(face_centers, axis0) # 计算交面总面积所有三角形面积之和 area combined.area intersections.append((geom_names[i], geom_names[j], center, area)) print(f 记录交面中心: {center}, 面积: {area:.6f}) else: print( 交集为空或无效) else: print( 交集返回 None) except Exception as e: print(f 计算交集失败: {e}) # --- 用 A 切割 B: B B - A --- try: result trimesh.boolean.difference([B, A], engineengine) if result is not None: if isinstance(result, list) and len(result) 0: if len(result) 1: volumes [r.volume for r in result] result result[np.argmax(volumes)] else: result result[0] if isinstance(result, trimesh.Trimesh): world_geoms[j] result print(f 成功切割 {geom_names[j]}) else: print(f 切割结果类型异常: {type(result)}) except Exception as e: print(f 切割失败: {e}) # 3. 构建新场景所有部件已是世界坐标变换设为单位矩阵 new_scene trimesh.Scene() for name, geom in zip(geom_names, world_geoms): new_scene.add_geometry(geom, geom_namename, transformnp.eye(4)) return new_scene, intersections def cut_glb(input_path, output_path, enginemanifold): 加载 GLB切割所有部件返回 (切割后的场景, 交面信息) scene trimesh.load(input_path, forcescene) if not isinstance(scene, trimesh.Scene): mesh trimesh.load(input_path) if isinstance(mesh, trimesh.Trimesh): scene trimesh.Scene(mesh) else: raise ValueError(无法加载为场景或网格) # 检查几何体有效性仅打印不剔除 valid_geoms [] invalid_geoms [] empty_geoms [] for name, geom in scene.geometry.items(): if not isinstance(geom, trimesh.Trimesh): invalid_geoms.append((name, f类型错误: {type(geom)})) elif geom.vertices.shape[0] 0 or geom.faces.shape[0] 0: empty_geoms.append((name, f顶点:{geom.vertices.shape[0]}, 面:{geom.faces.shape[0]})) elif not is_valid_mesh(geom, check_volumeFalse): invalid_geoms.append((name, 几何结构无效)) else: valid_geoms.append(name) if not valid_geoms: raise ValueError(警告场景中没有有效的几何体) print(f有效几何体: {len(valid_geoms)} 个) if invalid_geoms: print(f⚠ 无效几何体: {len(invalid_geoms)} 个) for name, reason in invalid_geoms[:5]: print(f - {name}: {reason}) if len(invalid_geoms) 5: print(f ... 还有 {len(invalid_geoms) - 5} 个无效几何体) if empty_geoms: print(f⚠ 空几何体: {len(empty_geoms)} 个) print(f加载场景包含 {len(scene.geometry)} 个子部件) cut_scene, intersections cut_scene_geometries(scene, engineengine) cut_scene.export(output_path) print(f切割后的场景已保存至: {output_path}) return cut_scene, intersections def explode_mesh(mesh, intersectionsNone, explosion_scale0.4, area_threshold_ratio0.06): 生成爆炸图并在原本接触的部件之间绘制连接线。 intersections: 从 cut_glb 返回的交面信息列表 [(name_i, name_j, center_global, area), ...] area_threshold_ratio: 面积小于最大面积*ratio 的交面将被忽略不绘制连接线 if isinstance(mesh, trimesh.Scene): scene mesh elif isinstance(mesh, trimesh.Trimesh): print(Warning: Single mesh provided, cant create exploded view) scene trimesh.Scene(mesh) return scene else: print(fWarning: Unexpected mesh type: {type(mesh)}) scene mesh if len(scene.geometry) 1: print(Only one geometry found - nothing to explode) return scene print(f[EXPLODE_MESH] Starting mesh explosion with scale {explosion_scale}) print(f[EXPLODE_MESH] Processing {len(scene.geometry)} parts) exploded_scene trimesh.Scene() part_centers [] geometry_names [] for geometry_name, geometry in scene.geometry.items(): if hasattr(geometry, vertices): center np.mean(geometry.vertices, axis0) part_centers.append(center) geometry_names.append(geometry_name) print(f[EXPLODE_MESH] Part {geometry_name}: center {center}) if not part_centers: print(No valid geometries with vertices found) return scene part_centers np.array(part_centers) global_center np.mean(part_centers, axis0) print(f[EXPLODE_MESH] Global center: {global_center}) offsets {} for i, (geometry_name, geometry) in enumerate(scene.geometry.items()): if hasattr(geometry, vertices): if i len(part_centers): part_center part_centers[i] direction part_center - global_center direction_norm np.linalg.norm(direction) if direction_norm 1e-6: direction direction / direction_norm else: direction np.random.randn(3) direction direction / np.linalg.norm(direction) offset direction * explosion_scale offsets[geometry_name] offset else: offset np.zeros(3) offsets[geometry_name] offset transform np.eye(4) transform[:3, 3] offset exploded_scene.add_geometry(geometry, transformtransform, geom_namegeometry_name) print(f[EXPLODE_MESH] Part {geometry_name}: moved by {np.linalg.norm(offset):.4f}) # ---- 绘制连接线 ---- if intersections is not None and len(intersections) 0: # 1. 计算最大面积仅考虑有面积的项 areas [item[3] for item in intersections if len(item) 4] if areas: max_area max(areas) threshold max_area * area_threshold_ratio print(f[EXPLODE_MESH] 最大交面面积: {max_area:.6f}, 阈值({threshold:.6f})将保留连接线) else: max_area None threshold None # 收集所有线段的端点坐标和索引 all_points [] line_indices [] filtered_count 0 for item in intersections: if len(item) 3: name_i, name_j, center item[0], item[1], item[2] else: continue # 判断是否过滤 if max_area is not None and len(item) 4: area item[3] if area threshold: filtered_count 1 print(f[EXPLODE_MESH] 忽略小面积交面: {name_i} ∩ {name_j} (面积{area:.6f})) continue if name_i in offsets and name_j in offsets: p1 center offsets[name_i] p2 center offsets[name_j] idx1 len(all_points) all_points.append(p1) idx2 len(all_points) all_points.append(p2) line_indices.append([idx1, idx2]) print(f[EXPLODE_MESH] Line between {name_i} and {name_j}) if filtered_count 0: print(f[EXPLODE_MESH] 共过滤掉 {filtered_count} 个小面积交面) if line_indices: vertices np.array(all_points) # 为每条线段单独创建实体避免歧义 entities [] for idx_pair in line_indices: entities.append(trimesh.path.entities.Line(pointsnp.array(idx_pair))) path trimesh.path.Path3D(entitiesentities, verticesvertices) exploded_scene.add_geometry(path, geom_nameconnection_lines, transformnp.eye(4)) print(f[EXPLODE_MESH] Added {len(line_indices)} connection lines) else: print([EXPLODE_MESH] No connection lines to add (all filtered out or none)) print([EXPLODE_MESH] Mesh explosion complete) return exploded_scene if __name__ __main__: input_file rbaozha.glb output_cut output_cut.glb output_explode output_explode.glb cut_scene, intersections cut_glb(input_file, output_cut, enginemanifold) # 打印所有切割面的面积汇总 if intersections: total_area 0.0 print(\n 切割面面积统计 ) for item in intersections: if len(item) 4: name_i, name_j, center, area item print(f {name_i} ∩ {name_j}: 面积 {area:.6f}) total_area area else: # 兼容旧格式无面积 print(f {item[0]} ∩ {item[1]}: 面积 (未记录)) print(f总切割面积: {total_area:.6f}) print(\n) else: print(没有检测到切割面。) explode_scene explode_mesh(cut_scene, intersectionsintersections, explosion_scale0.1, area_threshold_ratio0.06) explode_scene.export(output_explode) print(f爆炸图已保存至: {output_explode})