几何引擎新增open_slot功能。
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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"}}}},
|
||||
|
||||
@@ -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"]},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 → 源孔 + 实例孔各切一个圆柱。
|
||||
|
||||
Reference in New Issue
Block a user