修改了一些之前增加功能的bug
This commit is contained in:
@@ -22,8 +22,10 @@ from .sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches
|
||||
ALL_ATOMIC_IDS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
|
||||
"revolve_add", "revolve_cut", "hole_blind", "hole_countersink",
|
||||
"hole_counterbore", "sphere_add", "reference_plane", "reference_axis",
|
||||
"hole_counterbore", "sphere_add", "box_add", "cylinder_add",
|
||||
"reference_plane", "reference_axis",
|
||||
"hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror",
|
||||
"pattern_circular",
|
||||
"thread_add", "thread_cut",
|
||||
})
|
||||
|
||||
@@ -513,6 +515,56 @@ def _execute_sphere(node: FeaturePlanNode, session: ExecutionSession) -> Feature
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _execute_box(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
|
||||
# 长方体特征(box_add)执行入口:以几何中心 center_mm 与三向尺寸生成原生长方体。
|
||||
# 1. 解析并校验尺寸与中心,非法输入抛出带具体原因的 ValueError。
|
||||
try:
|
||||
length = float(node.params.get("length_mm") or 0.0)
|
||||
width = float(node.params.get("width_mm") or 0.0)
|
||||
height = float(node.params.get("height_mm") or 0.0)
|
||||
center = node.params.get("center_mm") or []
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError("box dimensions must be numeric") from error
|
||||
if length <= 0 or width <= 0 or height <= 0 or len(center) != 3:
|
||||
raise ValueError("box_add requires positive length_mm/width_mm/height_mm and a three-dimensional center_mm")
|
||||
# 2. 生成世界轴对齐的 plane frame:plane 原点是长方体的最小角点(中心减去半
|
||||
# 尺寸),长/宽/高分别沿世界 x/y/z 生长(build123d Solid.make_box 语义)。
|
||||
corner = (
|
||||
float(center[0]) - length / 2,
|
||||
float(center[1]) - width / 2,
|
||||
float(center[2]) - height / 2,
|
||||
)
|
||||
plane = PlaneSpec.from_mapping({"origin_mm": corner, "x_dir": [1, 0, 0], "normal": [0, 0, 1]})
|
||||
solid = session.adapter.box(length, width, height, plane)
|
||||
# 3. 与当前主体做布尔并后登记为新主体,并返回该特征的结果对象。
|
||||
session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node)
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _execute_cylinder(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
|
||||
# 圆柱特征(cylinder_add)执行入口:axis 的原点是底面圆心、方向为轴向;
|
||||
# axis 缺省为世界 +Z 过原点(底面圆心落在 (0,0,0))。
|
||||
# 1. 解析并校验半径与高度,非法输入抛出带具体原因的 ValueError。
|
||||
try:
|
||||
radius = float(node.params.get("radius_mm") or 0.0)
|
||||
height = float(node.params.get("height_mm") or 0.0)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError("cylinder dimensions must be numeric") from error
|
||||
if radius <= 0 or height <= 0:
|
||||
raise ValueError("cylinder_add requires positive radius_mm and height_mm")
|
||||
raw_axis = node.params.get("axis")
|
||||
if raw_axis is not None and not (
|
||||
isinstance(raw_axis, dict) and raw_axis.get("origin_mm") is not None and raw_axis.get("direction") is not None
|
||||
):
|
||||
raise ValueError("cylinder_add axis must define origin_mm and direction")
|
||||
axis = AxisSpec.from_mapping(raw_axis) if isinstance(raw_axis, dict) else None
|
||||
# 2. 由适配器创建原生圆柱(axis=None 即世界 +Z 过原点)。
|
||||
solid = session.adapter.cylinder(radius, height, axis)
|
||||
# 3. 与当前主体做布尔并后登记为新主体,并返回该特征的结果对象。
|
||||
session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node)
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _execute_thread(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
|
||||
# 螺纹特征(thread_add)执行入口:按规格生成参数化螺纹段并并入当前主体。
|
||||
# 1. 解析并校验尺寸/牙距/轴,非法输入抛出带具体原因的 ValueError。
|
||||
@@ -754,6 +806,11 @@ def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3, s
|
||||
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)]
|
||||
center = params.get("center_mm")
|
||||
if center:
|
||||
# box_add/sphere_add 以世界坐标几何中心定位;平移重放必须随实例移动该中心,
|
||||
# 否则阵列副本会静默重合在原位置。
|
||||
params["center_mm"] = [float(center[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 引用,随实例平移
|
||||
@@ -930,6 +987,12 @@ def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec, se
|
||||
axis["origin_mm"] = _reflect_point(axis["origin_mm"], plane)
|
||||
if axis.get("direction"):
|
||||
axis["direction"] = _reflect_point(axis["direction"], plane, vector=True)
|
||||
center = params.get("center_mm")
|
||||
if isinstance(center, list) and len(center) == 3:
|
||||
# box_add/sphere_add 以世界坐标几何中心定位:反射该中心即可。box_add 固定
|
||||
# 世界轴对齐,跨坐标平面镜像后仍保持朝向(斜镜像面在 _execute_mirror_pattern
|
||||
# 中已被显式拒绝)。
|
||||
params["center_mm"] = _reflect_point(center, plane)
|
||||
mirror_plane = params.get("mirror_plane")
|
||||
if isinstance(mirror_plane, dict) and node.atomic_id == "pattern_mirror":
|
||||
# #6 pattern 引用重解析:镜像重放 mirror source 时,其镜像面引用
|
||||
@@ -971,6 +1034,15 @@ def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec, se
|
||||
return FeaturePlanNode(instance_id, node.atomic_id, node.name, (), params, node.selectors, node.sketch_id, node.declared_status, node.source_feature)
|
||||
|
||||
|
||||
def _normal_is_coordinate_axis(normal: Any) -> bool:
|
||||
# 判断单位法向是否平行于任一世界坐标轴:跨这样的平面镜像会保持轴对齐朝向。
|
||||
return (
|
||||
isinstance(normal, (list, tuple))
|
||||
and len(normal) == 3
|
||||
and any(abs(float(normal[index])) > 1 - 1e-9 for index in range(3))
|
||||
)
|
||||
|
||||
|
||||
def _execute_mirror_pattern(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult:
|
||||
mirror = node.params.get("mirror_plane") or {}
|
||||
resolution = session.resolve(mirror)
|
||||
@@ -983,6 +1055,11 @@ 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}")
|
||||
if source.atomic_id == "box_add" and not _normal_is_coordinate_axis(resolution.record.value.normal):
|
||||
# box_add 是固定世界轴对齐的原生图元:跨非坐标平面镜像会产生倾斜朝向,
|
||||
# 当前参数语义无法表达,静默重放会得到错误几何 → 明确拒绝。跨坐标平面
|
||||
# (法向平行于任一坐标轴)的镜像仍然精确。
|
||||
raise ValueError("box_add mirror is exact only across coordinate-aligned mirror planes")
|
||||
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)
|
||||
@@ -990,6 +1067,200 @@ def _execute_mirror_pattern(node: FeaturePlanNode, session: ExecutionSession) ->
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _coordinate_axis_direction(direction: Any) -> bool:
|
||||
# 判断方向是否平行于任一世界坐标轴(circular 的 box 限制用)。
|
||||
# 不依赖输入已是单位向量:非零向量至多一个分量非零即为坐标轴方向
|
||||
# (_box_circular_is_exact 对任意长度/含小残差的 direction 都稳健)。
|
||||
if not isinstance(direction, (list, tuple)) or len(direction) != 3:
|
||||
return False
|
||||
return sum(1 for component in direction if abs(float(component)) > 1e-9) == 1
|
||||
|
||||
|
||||
def _rotated_vector(value: Vector3, axis: AxisSpec, angle_rad: float) -> Vector3:
|
||||
# Rodrigues 旋转公式:绕单位轴 axis.direction 旋转向量(无平移项)。
|
||||
cosine = math.cos(angle_rad)
|
||||
sine = math.sin(angle_rad)
|
||||
axis_direction = axis.direction
|
||||
cross = vector_cross(axis_direction, value)
|
||||
dot = vector_dot(axis_direction, value)
|
||||
return tuple( # type: ignore[return-value]
|
||||
value[index] * cosine + cross[index] * sine + axis_direction[index] * dot * (1.0 - cosine)
|
||||
for index in range(3)
|
||||
)
|
||||
|
||||
|
||||
def _rotated_point(point: Any, axis: AxisSpec, angle_rad: float) -> list[float]:
|
||||
# 绕轴旋转三维点:先平移到轴原点、旋转向量、再平移回。
|
||||
value = tuple(float(component) for component in point)
|
||||
relative = vector_subtract(value, axis.origin_mm)
|
||||
rotated = _rotated_vector(relative, axis, angle_rad)
|
||||
return [axis.origin_mm[index] + rotated[index] for index in range(3)]
|
||||
|
||||
|
||||
def _rotated_sketch(sketch: dict[str, Any], axis: AxisSpec, angle_rad: float) -> dict[str, Any]:
|
||||
# 环形阵列实例的草图:工作平面 frame(原点为点、x/y/normal 为向量)绕轴旋转;
|
||||
# 2D 局部实体坐标不动(frame 旋转后由草图求解器映射到新世界位置)。与
|
||||
# _translated_sketch 对"世界坐标轮廓点"的处理对称,这里把 start/end/center
|
||||
# 世界坐标点绕轴旋转。
|
||||
output = deepcopy(sketch)
|
||||
workplane = output.get("workplane") or {}
|
||||
if workplane.get("origin_mm"):
|
||||
workplane["origin_mm"] = _rotated_point(workplane["origin_mm"], axis, angle_rad)
|
||||
for key in ("x_dir", "y_dir", "normal"):
|
||||
if workplane.get(key):
|
||||
workplane[key] = list(_rotated_vector(tuple(float(v) for v in workplane[key]), axis, angle_rad))
|
||||
output["workplane"] = workplane
|
||||
|
||||
def rotate(value: Any) -> None:
|
||||
if isinstance(value, dict):
|
||||
for point_key in ("start_mm", "end_mm", "center_mm"):
|
||||
if point_key in value:
|
||||
value[point_key] = _rotated_point(value[point_key], axis, angle_rad)
|
||||
for child in value.values():
|
||||
rotate(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
rotate(child)
|
||||
|
||||
for key in ("contour_edges_mm", "contour_regions_mm"):
|
||||
rotate(output.get(key))
|
||||
return output
|
||||
|
||||
|
||||
def _rotated_node(node: FeaturePlanNode, instance_id: str, axis: AxisSpec, angle_rad: float, session: ExecutionSession) -> FeaturePlanNode:
|
||||
# 环形阵列实例节点:把源特征的全部绝对坐标参数绕 axis 旋转(参数键布局与
|
||||
# _translated_node/_mirrored_node 一致)。workplane/宿主 frame 的轴方向旋转,
|
||||
# 世界坐标点旋转;局部 positions(随宿主 frame)不动。特征自带 axis(圆柱轴/
|
||||
# 旋转轴/嵌套 circular 轴)与几何中心 center_mm 随实例旋转。嵌套 pattern
|
||||
# source(pattern_mirror/pattern_circular)带绝对引用:镜像面 frame / 内层
|
||||
# 源需连同本实例一起旋转,否则重放会退化成与源重合的错误几何。
|
||||
params = deepcopy(node.params)
|
||||
plane = params.get("plane")
|
||||
if isinstance(plane, dict):
|
||||
for key in ("origin_mm", "x_dir", "y_dir", "normal"):
|
||||
if plane.get(key):
|
||||
if key == "origin_mm":
|
||||
plane[key] = _rotated_point(plane[key], axis, angle_rad)
|
||||
else:
|
||||
plane[key] = list(_rotated_vector(tuple(float(v) for v in plane[key]), axis, angle_rad))
|
||||
host = params.get("host_face")
|
||||
host_frame = host.get("frame") if isinstance(host, dict) else None
|
||||
positions_are_local = isinstance(host_frame, dict) and all(
|
||||
host_frame.get(key) is not None for key in ("origin_mm", "x_dir", "normal")
|
||||
)
|
||||
if positions_are_local:
|
||||
if host_frame.get("origin_mm"):
|
||||
host_frame["origin_mm"] = _rotated_point(host_frame["origin_mm"], axis, angle_rad)
|
||||
for key in ("x_dir", "normal"):
|
||||
if host_frame.get(key):
|
||||
host_frame[key] = list(_rotated_vector(tuple(float(v) for v in host_frame[key]), axis, angle_rad))
|
||||
else:
|
||||
for position in params.get("positions") or []:
|
||||
if position.get("mm"):
|
||||
position["mm"] = _rotated_point(position["mm"], axis, angle_rad)
|
||||
feature_axis = params.get("axis")
|
||||
if isinstance(feature_axis, dict):
|
||||
if feature_axis.get("origin_mm"):
|
||||
feature_axis["origin_mm"] = _rotated_point(feature_axis["origin_mm"], axis, angle_rad)
|
||||
if feature_axis.get("direction"):
|
||||
feature_axis["direction"] = list(_rotated_vector(tuple(float(v) for v in feature_axis["direction"]), axis, angle_rad))
|
||||
center = params.get("center_mm")
|
||||
if isinstance(center, list) and len(center) == 3:
|
||||
params["center_mm"] = _rotated_point(center, axis, angle_rad)
|
||||
if node.atomic_id in {"pattern_mirror", "pattern_circular"}:
|
||||
# pattern 引用旋转重解析:镜像面 / 内层源随本实例一起旋转,否则嵌套
|
||||
# pattern 作为 circular source 时重放会退化成错误几何(见 _translated_node)。
|
||||
if node.atomic_id == "pattern_mirror":
|
||||
mirror_plane = params.get("mirror_plane")
|
||||
if not isinstance(mirror_plane, dict):
|
||||
raise ValueError("mirror pattern replayed by circular pattern has no mirror plane reference")
|
||||
frame = _owner_plane_frame(session, mirror_plane)
|
||||
if frame is None:
|
||||
raise ValueError("mirror plane reference cannot be transformed for circular pattern replay")
|
||||
cloned_selector = deepcopy(mirror_plane)
|
||||
cloned_selector["frame"] = {
|
||||
"origin_mm": _rotated_point(frame["origin_mm"], axis, angle_rad),
|
||||
"x_dir": list(_rotated_vector(tuple(frame["x_dir"]), axis, angle_rad)),
|
||||
"normal": list(_rotated_vector(tuple(frame["normal"]), axis, angle_rad)),
|
||||
}
|
||||
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"pattern source feature {source_id} has no replay definition")
|
||||
temp_id = f"{instance_id}.src.{source_id}"
|
||||
shifted = _rotated_node(source_node, temp_id, axis, angle_rad, 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] = _rotated_sketch(source_sketch, axis, angle_rad)
|
||||
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)
|
||||
|
||||
|
||||
def _execute_circular_pattern(node: FeaturePlanNode, session: ExecutionSession, execute: Callable[[FeaturePlanNode, ExecutionSession, dict[str, Any] | None], FeatureResult]) -> FeatureResult:
|
||||
# 环形阵列特征(pattern_circular)执行入口:绕显式轴按数量与包角重放源特征
|
||||
# 形成环形阵列。源特征整体绕轴旋转(绝对坐标变换),非复制当前主体的近似。
|
||||
params = node.params
|
||||
raw_axis = params.get("axis")
|
||||
if not (isinstance(raw_axis, dict) and raw_axis.get("origin_mm") is not None and raw_axis.get("direction") is not None):
|
||||
raise ValueError("circular pattern requires an explicit axis with origin_mm and direction")
|
||||
axis = AxisSpec.from_mapping(raw_axis)
|
||||
count = int(params.get("pattern_count") or 1)
|
||||
if count < 1:
|
||||
raise ValueError("circular pattern pattern_count must be >= 1")
|
||||
sweep_angle_deg = float(params.get("sweep_angle_deg") or 360.0)
|
||||
sources = session.replay_sources(params.get("source_feature_ids") or [])
|
||||
if not sources:
|
||||
raise ValueError("circular pattern source features have no replay definitions")
|
||||
for instance in range(1, count):
|
||||
# 实例 i 位于包角 sweep_angle_deg 的 i/count 处(i=0 即源特征本身)。
|
||||
angle_deg = sweep_angle_deg * instance / count
|
||||
angle_rad = math.radians(angle_deg)
|
||||
for source in sources:
|
||||
dependency = pattern_transform_blocker(source)
|
||||
if dependency:
|
||||
raise ValueError(f"circular pattern source uses an unsupported {dependency}")
|
||||
if source.atomic_id == "box_add" and not _box_circular_is_exact(axis, angle_rad):
|
||||
raise ValueError(
|
||||
"box_add circular pattern is exact only for coordinate-axis rotation "
|
||||
"by multiples of 180 degrees"
|
||||
)
|
||||
cloned = _rotated_node(source, f"{node.feature_id}.c{instance}.{source.feature_id}", axis, angle_rad, session)
|
||||
sketch = session.sketches.get(str(source.sketch_id))
|
||||
execute(cloned, session, _rotated_sketch(sketch, axis, angle_rad) if sketch else None)
|
||||
# 记录本阵列的 replay 定义:后续阵列若选中本阵列,按定义递归重放。
|
||||
session.replay_definitions[node.feature_id] = node
|
||||
return session.result(node)
|
||||
|
||||
|
||||
def _box_circular_is_exact(axis: AxisSpec, angle_rad: float) -> bool:
|
||||
# box_add 是固定世界轴对齐的原生图元:绕轴旋转任意角度会使其棱偏离坐标轴,
|
||||
# 当前参数语义无法表达 → 仅坐标轴旋转且每份转角为 180° 的整数倍时精确
|
||||
# (180° 翻转把轴对齐 box 映射回轴对齐 box)。与 _execute_mirror_pattern 的
|
||||
# box 坐标平面限制同思路:宁可显式拒绝,也不静默产出错误几何。
|
||||
if not _coordinate_axis_direction(axis.direction):
|
||||
return False
|
||||
half_turns = abs(math.degrees(angle_rad)) / 180.0
|
||||
return abs(half_turns - round(half_turns)) < 1e-9
|
||||
|
||||
|
||||
def _circular_pattern_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
|
||||
del sketch
|
||||
return _execute_circular_pattern(node, session, _execute_node)
|
||||
|
||||
|
||||
def _execute_node(node: FeaturePlanNode, session: ExecutionSession, sketch_override: dict[str, Any] | None = None) -> FeatureResult:
|
||||
executor = EXECUTORS.get(node.atomic_id)
|
||||
if executor is None:
|
||||
@@ -1024,6 +1295,16 @@ def _sphere_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: d
|
||||
return _execute_sphere(node, session)
|
||||
|
||||
|
||||
def _box_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
|
||||
del sketch
|
||||
return _execute_box(node, session)
|
||||
|
||||
|
||||
def _cylinder_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
|
||||
del sketch
|
||||
return _execute_cylinder(node, session)
|
||||
|
||||
|
||||
def _hole_executor(node: FeaturePlanNode, session: ExecutionSession, sketch: dict[str, Any] | None) -> FeatureResult:
|
||||
del sketch
|
||||
return _execute_hole(node, session)
|
||||
@@ -1058,6 +1339,8 @@ EXECUTORS: dict[str, ExecutorFunction] = {
|
||||
"reference_plane": _reference_plane_executor,
|
||||
"reference_axis": _reference_axis_executor,
|
||||
"sphere_add": _sphere_executor,
|
||||
"box_add": _box_executor,
|
||||
"cylinder_add": _cylinder_executor,
|
||||
"thread_add": _thread_executor,
|
||||
"thread_cut": _thread_executor,
|
||||
"extrude_add_blind": _primary_executor,
|
||||
@@ -1073,6 +1356,7 @@ EXECUTORS: dict[str, ExecutorFunction] = {
|
||||
"chamfer": _chamfer_executor,
|
||||
"pattern_linear": _linear_pattern_executor,
|
||||
"pattern_mirror": _mirror_pattern_executor,
|
||||
"pattern_circular": _circular_pattern_executor,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user