625b296e06
为 up_to_surface 终止条件新增裁剪语义,通过穿透拉伸与目标面布尔求交保留可达材料,修复非均匀相交时错误拒绝特征的问题。
210 lines
9.7 KiB
Python
210 lines
9.7 KiB
Python
"""#5 高级终止条件:up_to_surface 非均匀 profile 必须裁剪而非拒绝。
|
||
|
||
中文说明
|
||
--------
|
||
这个文件在测试什么(issue #5「高级终止条件要求整张 profile 同一距离」的回归测试):
|
||
|
||
1. 背景:up_to_surface(拉伸到面)等终止条件用"profile 采样点射线求交"
|
||
判定终止距离。修复前,只要 profile 与目标面**非均匀相交**——一部分
|
||
采样点到达目标、一部分悬空(profile 超出目标面范围),或目标面相对
|
||
profile 倾斜——runtime 就抛 non_uniform_extent_target 拒绝整个特征,
|
||
零件无法重建。而 CAD 的标准语义是**裁剪**:保留"从 profile 到目标面"
|
||
的可达材料,切掉悬空部分。
|
||
修复后:adapter 新增 extrude_trimmed(穿透拉伸 + 目标面体层布尔求交),
|
||
runtime 的 _targeted_extent_vector 在 up_to_surface 非均匀时返回
|
||
带 trim_to 的 ExtentVector,_shape_from_primary 改用裁剪拉伸。
|
||
up_to_vertex / up_to_body / offset_from_surface 没有可构造裁剪体层
|
||
的 face,仍保持显式拒绝(语义上无法裁剪)。
|
||
|
||
2. 本测试套件把"非均匀 up_to_surface 裁剪"合同固定下来:
|
||
- 单元几何契约(adapter):斜目标面 + 水平 profile 裁剪出楔形,
|
||
体积 = ∫斜顶 dxdy;
|
||
- 集成裁剪契约(rebuild):profile 悬空超出目标面时不再拒绝,
|
||
悬空部分被切掉,只保留与目标面之间的材料;
|
||
- 集成回归护栏(rebuild):profile 完全落在目标面内仍走精确
|
||
均匀拉伸路径,体积不变(防止裁剪回退把均匀路径也改坏)。
|
||
|
||
3. sys.path 说明:把 backend/engine 加入搜索路径,直接 import cdsl_engine
|
||
包做端到端测试(与既有测试风格一致)。
|
||
|
||
函数功能一览
|
||
------------
|
||
_workplane(origin, normal) 构造指定原点与法向的草图工作平面。
|
||
_rectangle(minimum, maximum) 构造 XY 平面内的矩形轮廓(2D 多边形)。
|
||
_base_block() 构造 10×10×10 拉伸主体(体积 1000,
|
||
顶面 z=10、范围 x/y ∈ [-5, 5])。
|
||
_top_face_selector(baseline) 从 baseline 拓扑记录里挑出顶面(法向 +z)
|
||
的几何快照,作为 up_to_surface 的 reference。
|
||
ExtentTrimContractTests 见各测试方法 docstring。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from copy import deepcopy
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT / "backend"))
|
||
sys.path.insert(0, str(ROOT / "backend" / "engine"))
|
||
|
||
from cdsl_engine.build123d_adapter import Build123dGeometryAdapter # noqa: E402
|
||
from cdsl_engine.runtime import rebuild_cdsl # noqa: E402
|
||
|
||
|
||
try:
|
||
from build123d import Face, Plane, Vector # noqa: F401
|
||
_HAS_BUILD123D = True
|
||
except ImportError:
|
||
_HAS_BUILD123D = False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 测试夹具
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _workplane(*, origin: list[float], normal: list[float]) -> dict:
|
||
"""构造草图工作平面:显式指定原点与法向(x_dir 固定 +X)。"""
|
||
return {"origin_mm": origin, "x_dir": [1, 0, 0], "normal": normal}
|
||
|
||
|
||
def _rectangle(minimum: list[float], maximum: list[float]) -> dict:
|
||
"""XY 平面内的矩形轮廓(2D 多边形),顶点逆时针。"""
|
||
return {"type": "polygon", "vertices": [
|
||
[minimum[0], minimum[1]], [maximum[0], minimum[1]],
|
||
[maximum[0], maximum[1]], [minimum[0], maximum[1]],
|
||
]}
|
||
|
||
|
||
def _base_block() -> dict:
|
||
"""10×10×10 拉伸主体:体积 1000,顶面位于 z=10、范围 x/y ∈ [-5, 5]。"""
|
||
return {
|
||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
||
"part_id": "extent-trim-contract", "meta": {"unit": "mm"},
|
||
"geometry": {"sketches": [{
|
||
"id": "base", "workplane": _workplane(origin=[0, 0, 0], normal=[0, 0, 1]),
|
||
"profile": _rectangle([-5, -5], [5, 5]),
|
||
}]},
|
||
"features": [{
|
||
"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [],
|
||
"params": {"distance_mm": 10}, "sketch_id": "base",
|
||
}],
|
||
}
|
||
|
||
|
||
def _top_face_selector(baseline: dict) -> dict:
|
||
"""从 baseline 拓扑记录里取顶面(法向 +z 的平面 face)的几何快照。
|
||
|
||
这个快照被 TopologyRegistry 用来做几何等价匹配,从而把 up_to_surface
|
||
的 reference 解析到重建主体上的真实 Face。
|
||
"""
|
||
return next(
|
||
item for item in baseline["topology_records"]
|
||
if item["kind"] == "face"
|
||
and item["geometry"]["surface_type"] == "plane"
|
||
and item["geometry"]["normal"][2] > 0.9
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 测试套件
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ExtentTrimContractTests(unittest.TestCase):
|
||
"""up_to_surface 非均匀相交「裁剪几何-行为-回归」三方合同测试。"""
|
||
|
||
@unittest.skipUnless(_HAS_BUILD123D, "build123d is not available")
|
||
def test_extrude_trimmed_slanted_target_produces_wedge(self) -> None:
|
||
"""单元几何契约:斜目标面 + 水平 profile 裁剪出楔形。
|
||
|
||
profile 是 z=0 的 10×10 矩形;目标面是斜面 z = 0.2x + 5
|
||
(z_dir=(0.2, 0, 0.98) 的平面)。采样点沿 +z 到斜面的距离从
|
||
4(x=-5)到 6(x=+5)变化 → uniform_intersection_distance 必然
|
||
判定非均匀。裁剪结果应是顶面贴斜面的楔形,体积 =
|
||
∫∫ (0.2x + 5) dxdy = 5 × 100 = 500。修复前该场景直接抛
|
||
non_uniform_extent_target(无实体可断言),本测试锁死裁剪几何。
|
||
"""
|
||
profile = Face.make_rect(10, 10) # 中心在原点,x/y ∈ [-5, 5],z=0
|
||
slanted = Face.make_rect(20, 20, Plane(origin=(0, 0, 5), z_dir=(0.2, 0, 0.98)))
|
||
|
||
trimmed = Build123dGeometryAdapter.extrude_trimmed(profile, slanted, (0, 0, 1))
|
||
|
||
self.assertAlmostEqual(trimmed.volume, 500.0, places=5)
|
||
|
||
@unittest.skipUnless(_HAS_BUILD123D, "build123d is not available")
|
||
def test_up_to_surface_hanging_profile_is_trimmed_not_rejected(self) -> None:
|
||
"""集成裁剪契约:profile 悬空超出目标面时不再拒绝,悬空部分被切掉。
|
||
|
||
baseline 是 10×10×10 主体(顶面 z=10、范围 [-5, 5]²)。第二个 add
|
||
特征在 z=12 平面、法向 -z,profile 为 16×16 矩形(x/y ∈ [-8, 8],
|
||
大部分悬空在顶面范围之外)。up_to_surface 目标 = 顶面:
|
||
- 中心采样点沿 -z 命中顶面(距离 2);
|
||
- 角落采样点(x/y = ±8)在顶面范围外,射线不命中 → 非均匀。
|
||
修复前抛 non_uniform_extent_target;修复后裁剪出
|
||
x/y ∈ [-5, 5]、z ∈ [10, 12] 的 10×10×2 体块,总体积 = 1000 + 200。
|
||
"""
|
||
base = _base_block()
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
root = Path(directory)
|
||
baseline = rebuild_cdsl(base, root / "baseline.step")
|
||
top_face = _top_face_selector(baseline)
|
||
|
||
with_cap = deepcopy(base)
|
||
with_cap["geometry"]["sketches"].append({
|
||
"id": "cap", "workplane": _workplane(origin=[0, 0, 12], normal=[0, 0, -1]),
|
||
"profile": _rectangle([-8, -8], [8, 8]),
|
||
})
|
||
with_cap["features"].append({
|
||
"id": "cap_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||
"sketch_id": "cap",
|
||
"params": {
|
||
"distance_mm": 0,
|
||
"end_condition": {"type": "up_to_surface", "reference": top_face},
|
||
},
|
||
})
|
||
rebuilt = rebuild_cdsl(with_cap, root / "trimmed.step")
|
||
|
||
self.assertAlmostEqual(rebuilt["volume_mm3"], 1000 + 200, places=5)
|
||
|
||
@unittest.skipUnless(_HAS_BUILD123D, "build123d is not available")
|
||
def test_up_to_surface_uniform_profile_keeps_exact_distance(self) -> None:
|
||
"""集成回归护栏:profile 完全落在目标面内仍走精确均匀拉伸。
|
||
|
||
与上一个测试同构,但 profile 缩到 6×6(x/y ∈ [-3, 3]),完全落在
|
||
顶面 [-5, 5]² 范围内 → 所有采样点沿 -z 都命中且距离一致(2)→
|
||
保持原精确拉伸路径(不触发裁剪),体积 = 1000 + 36×2 = 1072。
|
||
防止裁剪回退把均匀路径也改坏。
|
||
"""
|
||
base = _base_block()
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
root = Path(directory)
|
||
baseline = rebuild_cdsl(base, root / "baseline.step")
|
||
top_face = _top_face_selector(baseline)
|
||
|
||
with_cap = deepcopy(base)
|
||
with_cap["geometry"]["sketches"].append({
|
||
"id": "cap", "workplane": _workplane(origin=[0, 0, 12], normal=[0, 0, -1]),
|
||
"profile": _rectangle([-3, -3], [3, 3]),
|
||
})
|
||
with_cap["features"].append({
|
||
"id": "cap_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"],
|
||
"sketch_id": "cap",
|
||
"params": {
|
||
"distance_mm": 0,
|
||
"end_condition": {"type": "up_to_surface", "reference": top_face},
|
||
},
|
||
})
|
||
rebuilt = rebuild_cdsl(with_cap, root / "exact.step")
|
||
|
||
self.assertAlmostEqual(rebuilt["volume_mm3"], 1000 + 36 * 2, places=5)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|