feat(engine): 支持 up_to_surface 非均匀 profile 裁剪拉伸

为 up_to_surface 终止条件新增裁剪语义,通过穿透拉伸与目标面布尔求交保留可达材料,修复非均匀相交时错误拒绝特征的问题。
This commit is contained in:
2026-08-27 15:40:58 +08:00
parent c84fc3ade4
commit 625b296e06
3 changed files with 310 additions and 19 deletions
@@ -5,7 +5,7 @@ from __future__ import annotations
import math
from typing import Any, Iterable
from build123d import Axis, Edge, Face, Plane, Solid, Vector, Wire, export_step
from build123d import Axis, Compound, Edge, Face, Plane, ShapeList, Solid, Vector, Wire, export_step
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
@@ -151,6 +151,43 @@ class Build123dGeometryAdapter:
# 沿给定方向向量拉伸一个面,生成实体。
return Solid.extrude(face, _vector(direction))
@staticmethod
def extrude_trimmed(face: Face, target: Any, direction: Vector3) -> Any:
"""Extrude the profile to the target face, trimming unreached regions.
Issue #5: when a profile intersects the up_to_surface target
non-uniformly (part of the profile reaches the face, part hangs
outside it), a plain vector extrusion is wrong. The CAD semantics is
to keep only the material between the profile and the target. We
pierce the profile through the target, push the target face backward
by the same margin to build a slab, and keep their boolean common
(intersection) as the trimmed solid.
"""
# 1. 采样点到目标的最远命中距离决定穿透余量;没有任何采样点命中
# 说明 profile 与目标面无交叠,无法裁剪(保留 extent_target_not_reached)。
unit = _vector(direction).normalized()
hits = [
Build123dGeometryAdapter._forward_intersection_distance(target, point, unit)
for point in Build123dGeometryAdapter.profile_sample_points(face)
]
distances = [value for value in hits if value is not None]
if not distances:
raise ValueError("extent target is not reached by the profile")
margin = max(distances) + 2.0
# 2. 穿透拉伸 profile,同时把目标面向回推生成体层,二者求交即裁剪体。
# build123d 的布尔交方法名是 intersect(不是 OCC 的 common),
# 且多实体结果返回 ShapeList,需要规整为单个 Solid / Compound。
pierced = Solid.extrude(face, unit * margin)
slab = Solid.extrude(target, -unit * margin)
trimmed = pierced.intersect(slab)
if isinstance(trimmed, ShapeList):
members = list(trimmed)
# build123d 类型桩未声明 make_compound,但运行时存在(宽泛类型桩噪音)。
trimmed = members[0] if len(members) == 1 else Compound.make_compound(members) # pyright: ignore[reportAttributeAccessIssue]
if trimmed is None or (hasattr(trimmed, "is_empty") and trimmed.is_empty()):
raise ValueError("extent target produced an empty trimmed solid")
return trimmed
@staticmethod
def body_center(body: Any) -> Vector3:
# 取主体包围盒的中心坐标,作为体心的近似。
@@ -236,8 +273,9 @@ class Build123dGeometryAdapter:
return Solid.revolve(face, angle_deg, Build123dGeometryAdapter.axis(axis))
@staticmethod
def fuse(body: Any | None, solid: Solid) -> Any:
def fuse(body: Any | None, solid: Any) -> Any:
# 布尔并:没有既有主体时,直接以该实体作为新主体。
# 实参类型放宽为 Any:build123d 的布尔结果可能是 Solid 或 Compound。
return solid if body is None else body.fuse(solid)
@staticmethod