From 9a3e09b9f51a4ac9dadd878afcbe82152c8b20fa Mon Sep 17 00:00:00 2001 From: ganjihong Date: Mon, 7 Sep 2026 16:44:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=A0=E4=BD=95=E5=BC=95=E6=93=8E=E6=96=B0?= =?UTF-8?q?=E5=A2=9Eopen=5Fslot=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/engine_service.py | 10 +++++- backend/engine/cdsl_engine/capabilities.py | 4 ++- backend/engine/cdsl_engine/cdsl_schema.json | 9 +++++- .../engine/cdsl_engine/profile_schema.json | 1 + backend/engine/cdsl_engine/runtime.py | 2 ++ .../test_engine_pattern_transform_contract.py | 31 +++++++++++++------ 6 files changed, 44 insertions(+), 13 deletions(-) diff --git a/backend/app/services/engine_service.py b/backend/app/services/engine_service.py index 1448057d..2ea2d5a1 100644 --- a/backend/app/services/engine_service.py +++ b/backend/app/services/engine_service.py @@ -156,7 +156,15 @@ def validate_cdsl(cdsl: dict[str, Any], engine: Any) -> None: for parameter_name in contract.get("required_params") or []: if params.get(parameter_name) is None: raise ValueError(f"Feature {fid} ({atomic_id}) is missing required parameter: {parameter_name}") - if atomic_id.startswith("extrude_") and float(params.get("distance_mm") or 0) <= 0: + # 距离检查只对"以 distance_mm 驱动"的 extrude 契约生效:extrude_cut_through + # 等贯通型契约的作者参数不含 distance_mm(以 end_condition 表达贯通), + # 不能按盲切假设要求距离。用契约声明判定,避免硬编码原子名单漂移。 + author_props = (contract.get("author_params_schema") or {}).get("properties") or {} + if ( + atomic_id.startswith("extrude_") + and "distance_mm" in author_props + and float(params.get("distance_mm") or 0) <= 0 + ): raise ValueError(f"Feature {fid} ({atomic_id}) requires distance_mm > 0 for runtime rebuild") if atomic_id.startswith("revolve_") and float(params.get("angle_deg") or 0) <= 0: raise ValueError(f"Feature {fid} ({atomic_id}) requires angle_deg > 0 for runtime rebuild") diff --git a/backend/engine/cdsl_engine/capabilities.py b/backend/engine/cdsl_engine/capabilities.py index 5516b186..ef22863d 100644 --- a/backend/engine/cdsl_engine/capabilities.py +++ b/backend/engine/cdsl_engine/capabilities.py @@ -20,18 +20,20 @@ _SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"}) _SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_") _PRIMARY_ATOMICS = frozenset({ "extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", + "extrude_cut_through", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", }) _HOLE_ATOMICS = frozenset({"hole_blind", "hole_countersink", "hole_counterbore", "hole_wizard"}) _ACTIVE_BODY_REQUIRED = frozenset({ - "extrude_cut_blind", "revolve_cut", *_HOLE_ATOMICS, "fillet", "chamfer", + "extrude_cut_blind", "extrude_cut_through", "revolve_cut", *_HOLE_ATOMICS, "fillet", "chamfer", # thread_cut 是 cut 型特征:必须在已有主体(宿主)上做布尔差,不能凭空 # 造实体;无宿主时按 active_body 前置阻止而非让 executor 在 None 上崩溃。 "thread_cut", }) _BODY_MUTATING_ATOMICS = frozenset({ "extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", + "extrude_cut_through", "revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", *_HOLE_ATOMICS, "fillet", "chamfer", }) diff --git a/backend/engine/cdsl_engine/cdsl_schema.json b/backend/engine/cdsl_engine/cdsl_schema.json index 90d4630e..6f251c8e 100644 --- a/backend/engine/cdsl_engine/cdsl_schema.json +++ b/backend/engine/cdsl_engine/cdsl_schema.json @@ -102,6 +102,12 @@ "required": ["distance_mm"], "additionalProperties": false }, + "extrudeCutThroughParams": { + "type": "object", + "properties": {"reverse": {"type": "boolean"}, "end_condition": {"$ref": "#/$defs/endCondition"}}, + "required": ["end_condition"], + "additionalProperties": false + }, "revolveParams": { "type": "object", "properties": {"angle_deg": {"type": "number", "minimum": 0, "maximum": 360}, "axis": {"$ref": "#/$defs/axis"}, "reverse": {"type": "boolean"}, "end_condition": {"$ref": "#/$defs/endCondition"}}, @@ -383,7 +389,7 @@ "required": ["type", "contours"], "additionalProperties": false }, - "feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "pattern_circular", "reference_plane", "reference_axis", "hole_wizard"]}, + "feature_atomic_ids": {"enum": ["extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_through", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "fillet", "chamfer", "pattern_linear", "pattern_mirror", "pattern_circular", "reference_plane", "reference_axis", "hole_wizard"]}, "feature": { "type": "object", "properties": { @@ -403,6 +409,7 @@ {"if": {"properties": {"atomic_id": {"const": "extrude_add_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/extrudeParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "extrude_add_two_sided"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/extrudeParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "extrude_cut_blind"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/extrudeParams"}}}}, + {"if": {"properties": {"atomic_id": {"const": "extrude_cut_through"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/extrudeCutThroughParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "revolve_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/revolveParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "revolve_cut"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/revolveParams"}}}}, {"if": {"properties": {"atomic_id": {"const": "sphere_add"}}}, "then": {"properties": {"params": {"$ref": "#/$defs/sphereParams"}}}}, diff --git a/backend/engine/cdsl_engine/profile_schema.json b/backend/engine/cdsl_engine/profile_schema.json index f519eeaf..4a38cba6 100644 --- a/backend/engine/cdsl_engine/profile_schema.json +++ b/backend/engine/cdsl_engine/profile_schema.json @@ -9,6 +9,7 @@ "extrude_add_blind": {"atomic_id":"extrude_add_blind","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"reverse":{"type":"boolean"}},"required":["distance_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["sketch_workplane","profile_non_self_intersecting"],"candidate_verifiers":["single_connected_body"]}, "extrude_add_two_sided": {"atomic_id":"extrude_add_two_sided","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"reverse_distance_mm":{"type":"number","exclusiveMinimum":0},"reverse":{"type":"boolean"},"end_condition":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":48},"solidworks_code":{"type":"integer"}},"required":["type","solidworks_code"],"additionalProperties":false},"reverse_end_condition":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":48},"solidworks_code":{"type":"integer"}},"required":["type","solidworks_code"],"additionalProperties":false}},"required":["distance_mm","reverse_distance_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["sketch_workplane","profile_non_self_intersecting"],"candidate_verifiers":["single_connected_body"]}, "extrude_cut_blind": {"atomic_id":"extrude_cut_blind","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"distance_mm":{"type":"number","exclusiveMinimum":0},"reverse":{"type":"boolean"}},"required":["distance_mm"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","sketch_workplane","profile_non_self_intersecting","cut_exit_distance"],"candidate_verifiers":["single_connected_body","volume_decreased"]}, + "extrude_cut_through": {"atomic_id":"extrude_cut_through","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"reverse":{"type":"boolean"},"end_condition":{"type":"object","properties":{"type":{"type":"string","minLength":1,"maxLength":48},"solidworks_code":{"type":"integer"}},"required":["type","solidworks_code"],"additionalProperties":false}},"required":["end_condition"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","sketch_workplane","profile_non_self_intersecting"],"candidate_verifiers":["single_connected_body","volume_decreased"]}, "revolve_add": {"atomic_id":"revolve_add","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"angle_deg":{"type":"number","exclusiveMinimum":0,"maximum":360},"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false},"reverse":{"type":"boolean"}},"required":["angle_deg","axis"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["sketch_workplane","revolve_axis_on_sketch"],"candidate_verifiers":["single_connected_body"]}, "revolve_cut": {"atomic_id":"revolve_cut","contract_version":"3.0","fragment_shape":{"sketch":"required","params":"required_object","selector_tokens":"forbidden"},"author_params_schema":{"type":"object","properties":{"angle_deg":{"type":"number","exclusiveMinimum":0,"maximum":360},"axis":{"type":"object","properties":{"origin_mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"direction":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["origin_mm","direction"],"additionalProperties":false},"reverse":{"type":"boolean"}},"required":["angle_deg","axis"],"additionalProperties":false},"selector_policy":{"slot":null,"token_kind":null,"min_items":0,"max_items":0,"snapshot_bound":false},"server_injected_paths":[],"reference_policy":{"mode":"none"},"semantic_preflight":["requires_active_solid","sketch_workplane","revolve_axis_on_sketch"],"candidate_verifiers":["single_connected_body","volume_decreased"]}, "hole_blind": {"atomic_id":"hole_blind","contract_version":"3.0","fragment_shape":{"sketch":"forbidden","params":"required_object","selector_tokens":"required"},"author_params_schema":{"type":"object","properties":{"diameter_mm":{"type":"number","exclusiveMinimum":0},"depth_mm":{"type":"number","exclusiveMinimum":0},"positions":{"type":"array","minItems":1,"maxItems":64,"items":{"type":"object","properties":{"mm":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["mm"],"additionalProperties":false}},"drill_angle_rad":{"type":"number","exclusiveMinimum":0,"maximum":3.141592653589793}},"required":["diameter_mm","depth_mm","positions"],"additionalProperties":false},"selector_policy":{"slot":"params.host_face","token_kind":"face","min_items":1,"max_items":1,"snapshot_bound":true},"server_injected_paths":["params.host_face"],"reference_policy":{"mode":"none"},"semantic_preflight":["host_face_exists","hole_positions_on_host_plane","cut_exit_distance"],"candidate_verifiers":["cylindrical_bore","through_cylindrical_bore"]}, diff --git a/backend/engine/cdsl_engine/runtime.py b/backend/engine/cdsl_engine/runtime.py index dc27b745..d91d6a04 100644 --- a/backend/engine/cdsl_engine/runtime.py +++ b/backend/engine/cdsl_engine/runtime.py @@ -21,6 +21,7 @@ from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches ALL_ATOMIC_IDS = frozenset({ "extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", + "extrude_cut_through", "revolve_add", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "sphere_add", "box_add", "cylinder_add", "reference_plane", "reference_axis", @@ -1346,6 +1347,7 @@ EXECUTORS: dict[str, ExecutorFunction] = { "extrude_add_blind": _primary_executor, "extrude_add_two_sided": _primary_executor, "extrude_cut_blind": _primary_executor, + "extrude_cut_through": _primary_executor, "revolve_add": _primary_executor, "revolve_cut": _primary_executor, "hole_blind": _hole_executor, diff --git a/backend/tests/test_engine_pattern_transform_contract.py b/backend/tests/test_engine_pattern_transform_contract.py index 098c0ca5..2aba2e95 100644 --- a/backend/tests/test_engine_pattern_transform_contract.py +++ b/backend/tests/test_engine_pattern_transform_contract.py @@ -191,19 +191,27 @@ class PatternTransformContractTests(unittest.TestCase): self.assertTrue(pattern.executable) self.assertNotIn("unsupported_pattern_selector_transform", [blocker.code for blocker in pattern.blockers]) - def test_mirror_source_stays_blocked(self) -> None: - """回归护栏:mirror-as-source(plane selector)仍阻塞。 + def test_mirror_source_is_replayed_by_linear_pattern(self) -> None: + """mirror-as-source 放行 + 平移几何正确(体积 3000 精确)。 - pattern_mirror 的镜像面是 reference_plane 引用;线性阵列重放镜像 - 特征需要把镜像面逐实例平移,当前执行器没有实例变换通道,放行会 - 产出"所有实例重合"的错误几何。因此 mirror 特征作为 pattern source - 必须继续保持 unsupported_pattern_selector_transform。 + 早期(#6 修复前)mirror 的镜像面是 reference_plane 引用,无实例变换 + 通道,linear 阵列重放 mirror 会退化成"所有实例重合",因此曾阻塞 + mirror-as-source。_translated_node 已把镜像面 frame 随实例平移内联、 + 源递归平移后重放(镜像副本 = reflect(源@t, 面@t))→ 该护栏已过时。 + 本测试把新语义固定下来:分析放行(无 + unsupported_pattern_selector_transform)+ 几何正确。 + + baseline 10×10×10(x/y ∈ [-5,5]、z 0..10)。mirror 跨平面 y=5 镜像 + base → box x ∈ [-5,5]、y ∈ [5,15](vol 1000,与 base 共面 fuse 为 + 2000)。linear 沿 +x 间距 12 重放 mirror 源 → 镜像面平移到 (12,5) 且 + 源 base 平移至 x ∈ [7,17],反射得 box x ∈ [7,17]、y ∈ [5,15] + (vol 1000)。期望总体积 = 1000 + 1000 + 1000 = 3000。 """ base = _base_block() base["features"].extend([ { "id": "plane_ctx", "atomic_id": "reference_plane", "depends_on": [], - "params": {"plane": _workplane(origin=[0, 0, 0], normal=[0, 1, 0])}, + "params": {"plane": _workplane(origin=[0, 5, 0], normal=[0, 1, 0])}, }, { "id": "mirror", "atomic_id": "pattern_mirror", "depends_on": ["base_add", "plane_ctx"], @@ -219,13 +227,16 @@ class PatternTransformContractTests(unittest.TestCase): { "id": "repeat", "atomic_id": "pattern_linear", "depends_on": ["mirror"], "params": {"source_feature_ids": ["mirror"], "direction_1": [1, 0, 0], - "spacing_1_mm": 10, "pattern_count_1": 2}, + "spacing_1_mm": 12, "pattern_count_1": 2}, }, ]) analysis = analyze_cdsl(base) pattern = next(item for item in analysis.feature_results if item.feature_id == "repeat") - self.assertFalse(pattern.executable) - self.assertIn("unsupported_pattern_selector_transform", [blocker.code for blocker in pattern.blockers]) + self.assertTrue(pattern.executable) + self.assertNotIn("unsupported_pattern_selector_transform", [blocker.code for blocker in pattern.blockers]) + with tempfile.TemporaryDirectory() as directory: + rebuilt = rebuild_cdsl(base, Path(directory) / "mirror-in-linear.step") + self.assertAlmostEqual(rebuilt["volume_mm3"], 3000.0, places=6) def test_pattern_replays_face_selector_hole_geometry(self) -> None: """几何契约:无 frame 孔 pattern → 源孔 + 实例孔各切一个圆柱。 -- 2.52.0