Files
cdsl-cad/onshape_to_cdsl/tests/test_pipeline.py
T
likang 738934416e feat(cadfs): 扩展重建引擎能力并固化代表性模型回归
- 扩展 CDSL engine 的 shell、sweep、loft、reference plane、pattern 等运行时能力,
  支持新的实体结果模式、双向拉伸、曲线扫掠、镜像/圆周阵列及相关 selector 解析。
- 完善 Build123d 适配层的拓扑快照、Compound/ShapeList 兼容处理和旋转曲面识别,
  兼容 Python 3.12 / 当前 Build123d 缺少 axis_of_rotation 的合法曲面场景。
- 扩展 CDSL schema、profile schema、capability analysis、semantic validation 和
  sketch solver,使新增建模操作能够被校验、执行并保留可诊断的部分结果。
- 完善 CADFS FeatureScript lowering:
  支持 shell、sweep、surface/实体 loft、圆周阵列副本、镜像副本、删除阵列实例、
  新 body 操作、更多拉伸终止条件和 reference plane 变体。
- 补齐椭圆、B-spline、环形区域、imprint、SWEPT_FACE、CAP_FACE、OFFSET_FACE 等
  草图和拓扑引用的转换逻辑,改善后续特征的工作平面、轴线和 profile 定位精度。
- 改进 selector binding:支持 pattern 前缀复合 B-rep 快照、交集顶点引用、
  多面 match_mode=all、圆柱轴线/半径和面积下限等稳定匹配条件。
- 修复 MID_PLANE 法向统一后交线方向未同步的问题,恢复 00287955 基准面的正确位置;
  修复 00542223 sweep 路径反转后的切线契约和 00423838 的拓扑面数不稳定测试假设。
- 修正 CADFS 比较模块 import 路径,补充重建报告、批量重建脚本、目标文档和 README。
- 新增并扩展 engine、lowering、parser、selector binding、reports、integration 和
  Onshape pipeline 回归测试,覆盖代表性 CADFS 特征链及运行时兼容性。
2026-09-08 11:47:10 +08:00

152 lines
8.5 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import shutil
import tempfile
import unittest
from onshape_to_cdsl.compare import _bbox, _distances, _surface_points, strict_compare
from onshape_to_cdsl.convert import convert_one
from onshape_to_cdsl.issues import write_issue_register
from onshape_to_cdsl.merge_scans import merge_scans
from onshape_to_cdsl.select import select
from onshape_to_cdsl.sketches import sketch_to_cdsl, workplane_from_matrix
from onshape_to_cdsl.source_urls import read_url_file
from onshape_to_cdsl.units import length_mm
class PipelineTests(unittest.TestCase):
def test_url_file_and_units(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
mapping = Path(temporary) / "objects.yml"
mapping.write_text("{'00000352': 'https://cad.onshape.com/documents/doc/w/work/e/element'}\n", encoding="utf-8")
reference = read_url_file(mapping)[0]
self.assertEqual(reference.sample_id, "00000352")
self.assertEqual(reference.did, "doc")
self.assertAlmostEqual(length_mm(".063 in"), 1.6002)
self.assertAlmostEqual(length_mm("1.2*cm"), 12.0)
with self.assertRaises(ValueError):
length_mm("width")
def test_solved_rectangle_becomes_closed_analytic_contour(self) -> None:
matrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, .002, 0, 0, 0, 1]
self.assertEqual(workplane_from_matrix(matrix)["origin_mm"], [0.0, 0.0, 2.0])
point = lambda x, y: {"x": x / 1000, "y": y / 1000, "z": .002}
entities = []
for start, end in [((0, 0), (10, 0)), ((10, 0), (10, 5)), ((10, 5), (0, 5)), ((0, 5), (0, 0))]:
entities.append({"sketchEntityType": "skLineSegment", "isConstruction": False, "startPosition3d": point(*start), "endPosition3d": point(*end)})
cdsl = sketch_to_cdsl({"featureId": "sketch_one", "sketchMatrix": matrix, "entities": entities})
contour = cdsl["profile"]["contours"][0]
self.assertTrue(contour["closed"])
self.assertEqual(len(contour["segments"]), 4)
self.assertEqual(contour["segments"][0]["type"], "line")
def test_conversion_rejects_unsupported_history(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary) / "raw" / "sample_01"
root.mkdir(parents=True)
(root / "features.json").write_text(json.dumps({"features": [{"featureId": "x", "featureType": "loft", "parameters": []}]}), encoding="utf-8")
(root / "sketches.json").write_text(json.dumps({"sketches": []}), encoding="utf-8")
result = convert_one(root, Path(temporary) / "converted")
self.assertEqual(result["status"], "rejected")
self.assertEqual(result["diagnostics"][0]["status"], "deferred")
def test_issue_register_summarizes_engine_capability_gaps(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
converted = Path(temporary) / "converted"
sample = converted / "sample_01"
sample.mkdir(parents=True)
(sample / "conversion.json").write_text(json.dumps({"sample_id": "sample_01", "status": "rejected", "diagnostics": [{"feature_id": "loft_1", "feature_type": "loft", "status": "deferred", "reason": "loft executor is not implemented by the engine"}]}), encoding="utf-8")
register = write_issue_register(converted, [{"sample_id": "sample_01", "status": "rejected", "failure_reason": "conversion_not_executable"}])
self.assertEqual(register["summary"], {"engine_capability_gap": 1})
document = (Path(temporary) / "reconstruction_issues.md").read_text(encoding="utf-8")
self.assertIn("Engine Work Queue", document)
self.assertIn("loft executor", document)
def test_deterministic_stratified_selection(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
scan = Path(temporary) / "scan.json"
records = [{"sample_id": str(index), "status": "scanned", "feature_count": index, "feature_types": {"extrude": 1}, "curve_types": {"line": 4}} for index in range(8)]
scan.write_text(json.dumps({"records": records}), encoding="utf-8")
one = select(scan, Path(temporary) / "one.json", count=4, seed=7)
two = select(scan, Path(temporary) / "two.json", count=4, seed=7)
self.assertEqual([item["sample_id"] for item in one["records"]], [item["sample_id"] for item in two["records"]])
def test_merge_scan_batches_rejects_duplicate_ids(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
first, second = root / "first.json", root / "second.json"
first.write_text(json.dumps({"records": [{"sample_id": "0001", "status": "scanned"}]}), encoding="utf-8")
second.write_text(json.dumps({"records": [{"sample_id": "0002", "status": "failed"}]}), encoding="utf-8")
merged = merge_scans([first, second], root / "merged.json")
self.assertEqual([item["sample_id"] for item in merged["records"]], ["0001", "0002"])
second.write_text(json.dumps({"records": [{"sample_id": "0001", "status": "failed"}]}), encoding="utf-8")
with self.assertRaises(ValueError):
merge_scans([first, second], root / "bad.json")
def test_strict_comparator_accepts_identical_and_rejects_translation(self) -> None:
from build123d import Box, export_step
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source, same, shifted = root / "source.step", root / "same.step", root / "shifted.step"
box = Box(10, 10, 10)
export_step(box, source)
export_step(box, same)
moved = Box(10, 10, 10).translate((0.02, 0, 0))
export_step(moved, shifted)
self.assertTrue(strict_compare(source, same)["passed"])
rejection = strict_compare(source, shifted)
self.assertFalse(rejection["passed"])
self.assertIn("bbox_exceeds_0.01mm", rejection["failure_reasons"])
def test_strict_comparator_can_fast_reject_an_exact_mismatch(self) -> None:
from build123d import Box, export_step
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source, changed = root / "source.step", root / "changed.step"
export_step(Box(10, 10, 10), source)
export_step(Box(20, 10, 10), changed)
rejection = strict_compare(source, changed, fast_reject_relative_error=.005)
self.assertFalse(rejection["passed"])
self.assertEqual(rejection["surface"]["status"], "skipped_after_exact_mismatch")
self.assertIsNone(rejection["metrics"]["bbox_max_delta_mm"])
def test_strict_comparator_samples_points_on_the_source_brep(self) -> None:
from build123d import Cylinder
cylinder = Cylinder(21.3, 50)
samples = _surface_points(cylinder, 0.05)
self.assertGreater(len(samples), 0)
self.assertLess(max(_distances(samples, cylinder)), 1e-6)
def test_strict_comparator_bounds_use_the_sampled_brep_surface(self) -> None:
points = [(-1.0, -2.0, -3.0), (4.0, 5.0, 6.0), (0.0, 1.0, 2.0)]
self.assertEqual(_bbox(points), [-1.0, -2.0, -3.0, 4.0, 5.0, 6.0])
def test_00000352_offline_end_to_end_when_fixture_is_available(self) -> None:
fixture = Path.cwd() / "json_to_cdsl/input/onshape_complete/00000352"
if not (fixture / "model.step").exists():
self.skipTest("optional local 00000352 raw fixture is not installed")
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
raw = root / "raw" / "00000352"
raw.parent.mkdir()
shutil.copytree(fixture, raw)
result = convert_one(raw, root / "converted")
self.assertEqual(result["status"], "converted")
from cdsl_engine.runtime import rebuild_cdsl
from cdsl_engine.semantic_validation import validate_semantic_cdsl
cdsl_path = root / "converted/00000352/candidate.cdsl.json"
cdsl = json.loads(cdsl_path.read_text(encoding="utf-8"))
validate_semantic_cdsl(cdsl)
rebuilt = root / "rebuild.step"
rebuild_cdsl(cdsl, rebuilt, strict=True)
comparison = strict_compare(raw / "model.step", rebuilt)
self.assertTrue(comparison["passed"], comparison["failure_reasons"])
if __name__ == "__main__":
unittest.main()