Merge pull request 'Ganjihong' (#6) from ganjihong into main
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -102,6 +102,12 @@ def pattern_transform_blocker(source: FeaturePlanNode) -> str | None:
|
||||
# 孔宿主面:主体上的固定面,不随实例平移;实例位置由 positions
|
||||
# 平移决定(_translated_node),selector 原样保留即可正确 resolve。
|
||||
continue
|
||||
if selector.get("kind") == "plane" and source.atomic_id == "pattern_mirror" and selector.get("owner_feature_id"):
|
||||
# #6 pattern 引用重解析:pattern_mirror 的镜像面是对
|
||||
# reference_plane 的 plane 引用(带 owner_feature_id),重放时
|
||||
# 由运行时从 owner 解析显式 frame 并随实例变换
|
||||
# (_translated_node/_mirrored_node 内联 frame),放行。
|
||||
continue
|
||||
return "feature selector"
|
||||
host = source.params.get("host_face")
|
||||
if host is not None and not _has_explicit_host_frame(source.params):
|
||||
|
||||
@@ -664,7 +664,23 @@ def _translated_sketch(sketch: dict[str, Any], offset: Vector3) -> dict[str, Any
|
||||
return output
|
||||
|
||||
|
||||
def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3) -> FeaturePlanNode:
|
||||
def _owner_plane_frame(session: ExecutionSession, selector: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""解析 selector 的 owner 特征(reference_plane)注册的显式平面 frame。
|
||||
|
||||
#6 pattern 引用重解析:pattern 重放 source(pattern_mirror)时,镜像面
|
||||
是 selector,其 owner 是 reference_plane 特征;该特征执行时把显式
|
||||
PlaneSpec 登记为拓扑上下文,这里取出该 frame 供随实例变换使用。
|
||||
"""
|
||||
owner = selector.get("owner_feature_id")
|
||||
if not owner:
|
||||
return None
|
||||
for record in session.topology.records_for_feature(str(owner)):
|
||||
if record.kind == "plane" and isinstance(record.value, PlaneSpec):
|
||||
return record.value.as_dict()
|
||||
return None
|
||||
|
||||
|
||||
def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3, session: ExecutionSession) -> FeaturePlanNode:
|
||||
params = deepcopy(node.params)
|
||||
components = offset
|
||||
if isinstance(params.get("plane"), dict) and params["plane"].get("origin_mm"):
|
||||
@@ -683,6 +699,45 @@ def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3) -
|
||||
axis = params.get("axis") or {}
|
||||
if axis.get("origin_mm"):
|
||||
axis["origin_mm"] = [float(axis["origin_mm"][index]) + components[index] for index in range(3)]
|
||||
mirror_plane = params.get("mirror_plane")
|
||||
if isinstance(mirror_plane, dict) and node.atomic_id == "pattern_mirror":
|
||||
# #6 pattern 引用重解析:镜像面是 reference_plane 引用,随实例平移
|
||||
# 到新位置后内联为显式 frame;否则重放时 resolve 到原始面,镜像
|
||||
# 副本会错误地重合在源特征附近。同时源特征也必须平移后重放:镜像
|
||||
# 副本 = reflect(源@t, 面@t),只平移面不平移源会落在 2P+t-x 处
|
||||
# 而非正确位置 2P-x+t。
|
||||
frame = _owner_plane_frame(session, mirror_plane)
|
||||
if frame is None:
|
||||
raise ValueError("mirror plane reference cannot be transformed for pattern replay")
|
||||
cloned_selector = deepcopy(mirror_plane)
|
||||
cloned_selector["frame"] = {
|
||||
"origin_mm": [frame["origin_mm"][index] + components[index] for index in range(3)],
|
||||
"x_dir": list(frame["x_dir"]),
|
||||
"normal": list(frame["normal"]),
|
||||
}
|
||||
params["mirror_plane"] = cloned_selector
|
||||
transformed_ids: list[str] = []
|
||||
for source_id in node.params.get("source_feature_ids") or []:
|
||||
source_node = session.replay_definitions.get(str(source_id))
|
||||
if source_node is None:
|
||||
raise ValueError(f"mirror pattern source feature {source_id} has no replay definition")
|
||||
temp_id = f"{instance_id}.src.{source_id}"
|
||||
shifted = _translated_node(source_node, temp_id, offset, session)
|
||||
if shifted.sketch_id:
|
||||
source_sketch = session.sketches.get(str(source_node.sketch_id))
|
||||
if source_sketch is not None:
|
||||
temp_sketch_id = f"{temp_id}.sk"
|
||||
session.sketches[temp_sketch_id] = _translated_sketch(source_sketch, offset)
|
||||
shifted = FeaturePlanNode(
|
||||
shifted.feature_id, shifted.atomic_id, shifted.name, shifted.depends_on,
|
||||
shifted.params, shifted.selectors, temp_sketch_id,
|
||||
shifted.declared_status, shifted.source_feature,
|
||||
)
|
||||
# 临时 replay 定义同样进入 nodes 表(replay_sources 以此过滤)。
|
||||
session.nodes[temp_id] = shifted
|
||||
session.replay_definitions[temp_id] = shifted
|
||||
transformed_ids.append(temp_id)
|
||||
params["source_feature_ids"] = transformed_ids
|
||||
return FeaturePlanNode(instance_id, node.atomic_id, node.name, (), params, node.selectors, node.sketch_id, node.declared_status, node.source_feature)
|
||||
|
||||
|
||||
@@ -712,7 +767,7 @@ def _execute_linear_pattern(node: FeaturePlanNode, session: ExecutionSession, ex
|
||||
dependency = pattern_transform_blocker(source)
|
||||
if dependency:
|
||||
raise ValueError(f"pattern source uses an unsupported {dependency}")
|
||||
cloned = _translated_node(source, f"{node.feature_id}.p{first}_{second}.{source.feature_id}", offset)
|
||||
cloned = _translated_node(source, f"{node.feature_id}.p{first}_{second}.{source.feature_id}", offset, session)
|
||||
sketch = session.sketches.get(str(source.sketch_id))
|
||||
execute(cloned, session, _translated_sketch(sketch, offset) if sketch else None)
|
||||
# 5. 记录本阵列的 replay 定义:后续阵列若选中本阵列,按定义递归重放,
|
||||
@@ -779,7 +834,7 @@ def _mirrored_sketch(sketch: dict[str, Any], plane: PlaneSpec) -> dict[str, Any]
|
||||
return output
|
||||
|
||||
|
||||
def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec) -> FeaturePlanNode:
|
||||
def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec, session: ExecutionSession) -> FeaturePlanNode:
|
||||
params = deepcopy(node.params)
|
||||
if isinstance(params.get("plane"), dict):
|
||||
for key in ("origin_mm", "x_dir", "y_dir", "normal"):
|
||||
@@ -820,6 +875,44 @@ def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec) ->
|
||||
axis["origin_mm"] = _reflect_point(axis["origin_mm"], plane)
|
||||
if axis.get("direction"):
|
||||
axis["direction"] = _reflect_point(axis["direction"], plane, vector=True)
|
||||
mirror_plane = params.get("mirror_plane")
|
||||
if isinstance(mirror_plane, dict) and node.atomic_id == "pattern_mirror":
|
||||
# #6 pattern 引用重解析:镜像重放 mirror source 时,其镜像面引用
|
||||
# 随本实例的镜像面一起反射(内联为显式 frame),否则重放 resolve
|
||||
# 到原始面,嵌套镜像会退化成与源镜像重合的错误几何。源特征同样
|
||||
# 反射后重放:镜像副本 = reflect(源@P_B, reflect(面,P_B))。
|
||||
frame = _owner_plane_frame(session, mirror_plane)
|
||||
if frame is None:
|
||||
raise ValueError("mirror plane reference cannot be transformed for pattern replay")
|
||||
cloned_selector = deepcopy(mirror_plane)
|
||||
cloned_selector["frame"] = {
|
||||
"origin_mm": _reflect_point(frame["origin_mm"], plane),
|
||||
"x_dir": _reflect_point(frame["x_dir"], plane, vector=True),
|
||||
"normal": _reflect_point(frame["normal"], plane, vector=True),
|
||||
}
|
||||
params["mirror_plane"] = cloned_selector
|
||||
transformed_ids: list[str] = []
|
||||
for source_id in node.params.get("source_feature_ids") or []:
|
||||
source_node = session.replay_definitions.get(str(source_id))
|
||||
if source_node is None:
|
||||
raise ValueError(f"mirror pattern source feature {source_id} has no replay definition")
|
||||
temp_id = f"{instance_id}.src.{source_id}"
|
||||
shifted = _mirrored_node(source_node, temp_id, plane, session)
|
||||
if shifted.sketch_id:
|
||||
source_sketch = session.sketches.get(str(source_node.sketch_id))
|
||||
if source_sketch is not None:
|
||||
temp_sketch_id = f"{temp_id}.sk"
|
||||
session.sketches[temp_sketch_id] = _mirrored_sketch(source_sketch, plane)
|
||||
shifted = FeaturePlanNode(
|
||||
shifted.feature_id, shifted.atomic_id, shifted.name, shifted.depends_on,
|
||||
shifted.params, shifted.selectors, temp_sketch_id,
|
||||
shifted.declared_status, shifted.source_feature,
|
||||
)
|
||||
# 临时 replay 定义同样进入 nodes 表(replay_sources 以此过滤)。
|
||||
session.nodes[temp_id] = shifted
|
||||
session.replay_definitions[temp_id] = shifted
|
||||
transformed_ids.append(temp_id)
|
||||
params["source_feature_ids"] = transformed_ids
|
||||
return FeaturePlanNode(instance_id, node.atomic_id, node.name, (), params, node.selectors, node.sketch_id, node.declared_status, node.source_feature)
|
||||
|
||||
|
||||
@@ -835,7 +928,7 @@ def _execute_mirror_pattern(node: FeaturePlanNode, session: ExecutionSession) ->
|
||||
dependency = pattern_transform_blocker(source)
|
||||
if dependency:
|
||||
raise ValueError(f"mirror pattern source uses an unsupported {dependency}")
|
||||
cloned = _mirrored_node(source, f"{node.feature_id}.m.{source.feature_id}", resolution.record.value)
|
||||
cloned = _mirrored_node(source, f"{node.feature_id}.m.{source.feature_id}", resolution.record.value, session)
|
||||
sketch = session.sketches.get(str(source.sketch_id))
|
||||
_execute_node(cloned, session, _mirrored_sketch(sketch, resolution.record.value) if sketch else None)
|
||||
session.replay_definitions[node.feature_id] = node
|
||||
|
||||
@@ -719,6 +719,39 @@ class TopologyRegistry:
|
||||
]
|
||||
if owner:
|
||||
candidates = [record for record in candidates if owner in record.owners]
|
||||
if kind == "plane" and selector.get("frame") is not None:
|
||||
# #6 pattern 引用重解析:pattern 重放 source(pattern_mirror)时,
|
||||
# mirror_plane 的 plane 引用由运行时随实例变换后内联为显式 frame
|
||||
# (_translated_node / _mirrored_node),这里直接构造 PlaneSpec,
|
||||
# 不再走 stable_id / 几何匹配,避免解析到未随实例变换的原始面。
|
||||
try:
|
||||
plane = PlaneSpec.from_mapping(selector.get("frame") or {})
|
||||
except (TypeError, ValueError):
|
||||
plane = None
|
||||
if plane is not None:
|
||||
record = TopologyRecord(
|
||||
record_id=f"inline:{id(plane)}",
|
||||
kind="plane",
|
||||
feature_id=str(owner or "inline"),
|
||||
geometry=plane.as_dict(),
|
||||
value=plane,
|
||||
)
|
||||
return SelectorResolution(
|
||||
selector=selector,
|
||||
status="resolved",
|
||||
record=record,
|
||||
candidates=({"score": 1.0, **record.public_dict()},),
|
||||
)
|
||||
return SelectorResolution(
|
||||
selector=selector,
|
||||
status="not_found",
|
||||
candidates=(),
|
||||
diagnostic=RuntimeDiagnostic(
|
||||
code="selector_frame_incomplete",
|
||||
message="An inline plane frame requires origin_mm, x_dir and normal",
|
||||
detail={"frame": selector.get("frame")},
|
||||
),
|
||||
)
|
||||
geometry = normalize_selector_geometry(selector.get("geometry"))
|
||||
if selector.get("snapshot_id") and not owner:
|
||||
return SelectorResolution(
|
||||
|
||||
@@ -174,6 +174,44 @@ class EvidenceV2ToCdslTests(unittest.TestCase):
|
||||
self.assertTrue(comparison["numeric_geometry_match"])
|
||||
self.assertTrue(comparison["topology_counts_match"])
|
||||
|
||||
def _chamfer_only_fixture(self, dimensions: list[float], props: dict | None = None) -> dict:
|
||||
return {
|
||||
"schema": "solidworks.cad_evidence.v2",
|
||||
"status": "complete_with_blockers",
|
||||
"self_validation": {"feature_contracts": []},
|
||||
"features": [{
|
||||
"sequence": 1, "name": "Chamfer", "effective_type": "Chamfer", "stable_id": "chamfer",
|
||||
"definition_properties": {"values": props or {}},
|
||||
"definition_methods": {"values": {}}, "parents": [], "selections": [],
|
||||
"dimensions": [{"system_value": value} for value in dimensions],
|
||||
}],
|
||||
}
|
||||
|
||||
def test_chamfer_exports_radian_angle_instead_of_silent_equidistant_distance(self) -> None:
|
||||
# Distance-Angle 倒角:dimensions=[0.001m, 0.785398],无 EdgeChamferAngle 属性,
|
||||
# 走 raw 兜底路径。角度 system value 为弧度,distance_mm 按米→毫米换算。
|
||||
fixture = self._chamfer_only_fixture([0.001, 0.785398])
|
||||
cdsl, _ = convert_evidence(fixture, source_name="chamfer-angle.solidworks_evidence_v2.json")
|
||||
params = next(item for item in cdsl["features"] if item["atomic_id"] == "chamfer")["params"]
|
||||
self.assertAlmostEqual(params["distance_mm"], 1.0, places=9)
|
||||
self.assertNotIn("distance_2_mm", params)
|
||||
self.assertAlmostEqual(params["angle_rad"], 0.785398, places=5)
|
||||
|
||||
def test_chamfer_prefers_edge_chamfer_angle_property(self) -> None:
|
||||
fixture = self._chamfer_only_fixture([0.001, 0.785398], props={"EdgeChamferAngle": 1.047197551})
|
||||
cdsl, _ = convert_evidence(fixture, source_name="chamfer-prop.solidworks_evidence_v2.json")
|
||||
params = next(item for item in cdsl["features"] if item["atomic_id"] == "chamfer")["params"]
|
||||
self.assertAlmostEqual(params["distance_mm"], 1.0, places=9)
|
||||
self.assertAlmostEqual(params["angle_rad"], 1.047197551, places=6)
|
||||
|
||||
def test_chamfer_keeps_equidistant_form_when_angle_is_missing(self) -> None:
|
||||
fixture = self._chamfer_only_fixture([0.001])
|
||||
cdsl, _ = convert_evidence(fixture, source_name="chamfer-eq.solidworks_evidence_v2.json")
|
||||
params = next(item for item in cdsl["features"] if item["atomic_id"] == "chamfer")["params"]
|
||||
self.assertAlmostEqual(params["distance_mm"], 1.0, places=9)
|
||||
self.assertNotIn("angle_rad", params)
|
||||
self.assertNotIn("distance_2_mm", params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -611,8 +611,14 @@ def _deferred_params(family: str, feature: dict[str, Any], props: dict[str, Any]
|
||||
params: dict[str, Any] = {"distance_mm": distance}
|
||||
angle = _number(props.get("EdgeChamferAngle"))
|
||||
if angle <= 0 and len(raw_dimensions) > 1:
|
||||
# 兜底:EdgeChamferAngle 缺失时,第二个 raw 值按角度(弧度)解释。
|
||||
# 注意:SolidWorks 角度 system value 为弧度;Distance-Distance 倒角的
|
||||
# 第二距离(米)也会落入该兜底,但真实数据中倒角均为 Distance-Angle
|
||||
# (第二值为 π/4 等弧度值),runtime 通过 distance_2_mm/angle_rad
|
||||
# 契约消费第二参数,不会把角度静默当等距。
|
||||
angle = raw_dimensions[1]
|
||||
if angle > 0:
|
||||
# Distance-Angle 倒角:角度单位为弧度(build123d 侧由 runtime 换算)。
|
||||
params["angle_rad"] = angle
|
||||
return params, []
|
||||
if family == "LPattern":
|
||||
|
||||
Reference in New Issue
Block a user