Ganjihong #3
@@ -5,32 +5,39 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from build123d import Axis, Compound, Edge, Face, Plane, Solid, Vector, Wire, export_step
|
from build123d import Axis, Edge, Face, Plane, Solid, Vector, Wire, export_step
|
||||||
|
|
||||||
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
|
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||||
|
|
||||||
|
|
||||||
def _vector(value: list[float] | tuple[float, float, float]) -> Vector:
|
def _vector(value: list[float] | tuple[float, float, float]) -> Vector:
|
||||||
|
# 将三元坐标(list 或 tuple)转换为 build123d 的 Vector 对象。
|
||||||
return Vector(float(value[0]), float(value[1]), float(value[2]))
|
return Vector(float(value[0]), float(value[1]), float(value[2]))
|
||||||
|
|
||||||
|
|
||||||
def _arc_midpoint(edge: dict[str, Any], start: Vector, end: Vector, center: Vector) -> Vector:
|
def _arc_midpoint(edge: dict[str, Any], start: Vector, end: Vector, center: Vector) -> Vector:
|
||||||
|
# 计算圆弧中点(配合 Edge.make_three_point_arc 三点画弧),支持显式法向与顺时针/逆时针方向。
|
||||||
|
# 1. 半径:优先取 edge.radius_mm,缺省时由圆心到起点的距离推算。
|
||||||
radius = float(edge.get("radius_mm") or (start - center).length)
|
radius = float(edge.get("radius_mm") or (start - center).length)
|
||||||
first = start - center
|
first = start - center
|
||||||
second = end - center
|
second = end - center
|
||||||
|
# 2. 起点或终点与圆心重合时,圆弧退化为线段,中点取两端中点。
|
||||||
if first.length <= 1e-9 or second.length <= 1e-9:
|
if first.length <= 1e-9 or second.length <= 1e-9:
|
||||||
return (start + end) / 2
|
return (start + end) / 2
|
||||||
|
# 3. 确定圆弧所在平面法向:优先显式 normal,其次由两半径向量叉积推得,最后回退到 +Z。
|
||||||
normal = _vector(edge.get("normal") or [0, 0, 1])
|
normal = _vector(edge.get("normal") or [0, 0, 1])
|
||||||
if normal.length <= 1e-9:
|
if normal.length <= 1e-9:
|
||||||
normal = first.cross(second)
|
normal = first.cross(second)
|
||||||
if normal.length <= 1e-9:
|
if normal.length <= 1e-9:
|
||||||
normal = Vector(0, 0, 1)
|
normal = Vector(0, 0, 1)
|
||||||
normal = normal.normalized()
|
normal = normal.normalized()
|
||||||
|
# 4. 未指定旋转方向:取两条半径单位向量之和(角平分线)指向圆弧中点。
|
||||||
if "clockwise" not in edge:
|
if "clockwise" not in edge:
|
||||||
bisector = first.normalized() + second.normalized()
|
bisector = first.normalized() + second.normalized()
|
||||||
if bisector.length <= 1e-9:
|
if bisector.length <= 1e-9:
|
||||||
bisector = normal.cross(first)
|
bisector = normal.cross(first)
|
||||||
return center + bisector.normalized() * radius
|
return center + bisector.normalized() * radius
|
||||||
|
# 5. 指定了方向:按有符号扫掠角规整到 (−π, π],再沿首半径旋转半角得到中点。
|
||||||
sweep = math.atan2(normal.dot(first.cross(second)), first.dot(second))
|
sweep = math.atan2(normal.dot(first.cross(second)), first.dot(second))
|
||||||
if bool(edge["clockwise"]):
|
if bool(edge["clockwise"]):
|
||||||
if sweep >= 0:
|
if sweep >= 0:
|
||||||
@@ -47,34 +54,43 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def plane(spec: PlaneSpec) -> Plane:
|
def plane(spec: PlaneSpec) -> Plane:
|
||||||
|
# 将运行时平面定义 PlaneSpec 转换为 build123d 的 Plane。
|
||||||
return Plane(origin=_vector(spec.origin_mm), x_dir=_vector(spec.x_dir), z_dir=_vector(spec.normal))
|
return Plane(origin=_vector(spec.origin_mm), x_dir=_vector(spec.x_dir), z_dir=_vector(spec.normal))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def axis(spec: AxisSpec) -> Axis:
|
def axis(spec: AxisSpec) -> Axis:
|
||||||
|
# 将运行时轴定义 AxisSpec 转换为 build123d 的 Axis。
|
||||||
return Axis(origin=_vector(spec.origin_mm), direction=_vector(spec.direction))
|
return Axis(origin=_vector(spec.origin_mm), direction=_vector(spec.direction))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _wire(edges: list[dict[str, Any]]) -> Wire:
|
def _wire(edges: list[dict[str, Any]]) -> Wire:
|
||||||
|
# 将边字典列表(直线/圆弧)组装成 build123d 的 Wire 线框。
|
||||||
built: list[Edge] = []
|
built: list[Edge] = []
|
||||||
for edge in edges:
|
for edge in edges:
|
||||||
start = _vector(edge["start_mm"])
|
start = _vector(edge["start_mm"])
|
||||||
end = _vector(edge["end_mm"])
|
end = _vector(edge["end_mm"])
|
||||||
if edge.get("type") == "arc" and edge.get("center_mm") is not None:
|
if edge.get("type") == "arc" and edge.get("center_mm") is not None:
|
||||||
|
# 圆弧边:由起点、中点、终点三点构造圆弧。
|
||||||
center = _vector(edge["center_mm"])
|
center = _vector(edge["center_mm"])
|
||||||
built.append(Edge.make_three_point_arc(start, _arc_midpoint(edge, start, end, center), end))
|
built.append(Edge.make_three_point_arc(start, _arc_midpoint(edge, start, end, center), end))
|
||||||
else:
|
else:
|
||||||
|
# 直线边:直接连接首尾。
|
||||||
built.append(Edge.make_line(start, end))
|
built.append(Edge.make_line(start, end))
|
||||||
return Wire(built)
|
return Wire(built)
|
||||||
|
|
||||||
def _circle_wire(self, center: list[float], radius: float, plane_spec: PlaneSpec) -> Wire:
|
def _circle_wire(self, center: list[float], radius: float, plane_spec: PlaneSpec) -> Wire:
|
||||||
|
# 在草图工作平面上,按局部二维圆心与半径生成整圆 Wire(圆心由工作平面原点 + x/y 方向线性组合得到)。
|
||||||
origin = Vector(*plane_spec.origin_mm) + Vector(*plane_spec.x_dir) * float(center[0]) + Vector(*plane_spec.y_dir) * float(center[1])
|
origin = Vector(*plane_spec.origin_mm) + Vector(*plane_spec.x_dir) * float(center[0]) + Vector(*plane_spec.y_dir) * float(center[1])
|
||||||
circle_plane = Plane(origin=origin, x_dir=Vector(*plane_spec.x_dir), z_dir=Vector(*plane_spec.normal))
|
circle_plane = Plane(origin=origin, x_dir=Vector(*plane_spec.x_dir), z_dir=Vector(*plane_spec.normal))
|
||||||
return Wire.make_circle(radius, circle_plane)
|
return Wire.make_circle(radius, circle_plane)
|
||||||
|
|
||||||
def _faces_from_circles(self, entities: list[dict[str, Any]], plane_spec: PlaneSpec) -> list[Face]:
|
def _faces_from_circles(self, entities: list[dict[str, Any]], plane_spec: PlaneSpec) -> list[Face]:
|
||||||
|
# 由草图中的实体圆生成面,按圆间包含关系识别孔洞并跳过落入孔洞区的圆。
|
||||||
|
# 1. 筛选非构造圆;没有实体圆时直接返回空列表。
|
||||||
circles = [item for item in entities if item.get("type") == "circle" and not item.get("construction")]
|
circles = [item for item in entities if item.get("type") == "circle" and not item.get("construction")]
|
||||||
if not circles:
|
if not circles:
|
||||||
return []
|
return []
|
||||||
|
# 2. 逐个生成整圆 Wire,非法半径(≤0)的圆跳过。
|
||||||
entries = []
|
entries = []
|
||||||
for item in circles:
|
for item in circles:
|
||||||
radius = float(item.get("radius_mm") or 0)
|
radius = float(item.get("radius_mm") or 0)
|
||||||
@@ -84,6 +100,7 @@ class Build123dGeometryAdapter:
|
|||||||
entries.append({"center": center, "radius": radius, "wire": self._circle_wire(center, radius, plane_spec)})
|
entries.append({"center": center, "radius": radius, "wire": self._circle_wire(center, radius, plane_spec)})
|
||||||
faces: list[Face] = []
|
faces: list[Face] = []
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
|
# 3. 统计当前圆被多少个更大圆完整包含;被奇数层包含说明其处于孔洞区,跳过不建面。
|
||||||
containing = sum(
|
containing = sum(
|
||||||
math.dist(entry["center"], other["center"]) + entry["radius"] < other["radius"] - 1e-8
|
math.dist(entry["center"], other["center"]) + entry["radius"] < other["radius"] - 1e-8
|
||||||
for other in entries
|
for other in entries
|
||||||
@@ -91,6 +108,7 @@ class Build123dGeometryAdapter:
|
|||||||
)
|
)
|
||||||
if containing % 2:
|
if containing % 2:
|
||||||
continue
|
continue
|
||||||
|
# 4. 收集直接包在自身内部的圆作为孔洞,且它们只能被当前这一层包含。
|
||||||
holes = [
|
holes = [
|
||||||
other["wire"]
|
other["wire"]
|
||||||
for other in entries
|
for other in entries
|
||||||
@@ -101,11 +119,14 @@ class Build123dGeometryAdapter:
|
|||||||
if candidate is not other
|
if candidate is not other
|
||||||
) == containing + 1
|
) == containing + 1
|
||||||
]
|
]
|
||||||
|
# 5. 以当前圆为外轮廓建面,必要时打孔。
|
||||||
face = Face(entry["wire"])
|
face = Face(entry["wire"])
|
||||||
faces.append(face.make_holes(holes) if holes else face)
|
faces.append(face.make_holes(holes) if holes else face)
|
||||||
return faces
|
return faces
|
||||||
|
|
||||||
def faces_for_sketch(self, sketch: dict[str, Any]) -> list[Face]:
|
def faces_for_sketch(self, sketch: dict[str, Any]) -> list[Face]:
|
||||||
|
# 从草图数据解析出可拉伸/旋转的轮廓面,按三种数据来源依次回退。
|
||||||
|
# 1. 优先使用预计算的轮廓区域 contour_regions_mm(外轮廓 + 孔洞列表)。
|
||||||
regions = sketch.get("contour_regions_mm") or []
|
regions = sketch.get("contour_regions_mm") or []
|
||||||
if regions:
|
if regions:
|
||||||
result: list[Face] = []
|
result: list[Face] = []
|
||||||
@@ -117,23 +138,28 @@ class Build123dGeometryAdapter:
|
|||||||
holes = [self._wire(hole) for hole in region.get("holes") or [] if len(hole) >= 2]
|
holes = [self._wire(hole) for hole in region.get("holes") or [] if len(hole) >= 2]
|
||||||
result.append(face.make_holes(holes) if holes else face)
|
result.append(face.make_holes(holes) if holes else face)
|
||||||
return result
|
return result
|
||||||
|
# 2. 退化:仅有单组轮廓边时,直接作为外轮廓建面。
|
||||||
edges = sketch.get("contour_edges_mm") or []
|
edges = sketch.get("contour_edges_mm") or []
|
||||||
if len(edges) >= 2:
|
if len(edges) >= 2:
|
||||||
return [Face(self._wire(edges))]
|
return [Face(self._wire(edges))]
|
||||||
|
# 3. 最终回退:由工作平面与实体圆生成面(圆环/孔洞处理见 _faces_from_circles)。
|
||||||
plane = PlaneSpec.from_mapping(sketch.get("workplane") or {})
|
plane = PlaneSpec.from_mapping(sketch.get("workplane") or {})
|
||||||
return self._faces_from_circles(sketch.get("entities") or [], plane)
|
return self._faces_from_circles(sketch.get("entities") or [], plane)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extrude(face: Face, direction: Vector3) -> Solid:
|
def extrude(face: Face, direction: Vector3) -> Solid:
|
||||||
|
# 沿给定方向向量拉伸一个面,生成实体。
|
||||||
return Solid.extrude(face, _vector(direction))
|
return Solid.extrude(face, _vector(direction))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def body_center(body: Any) -> Vector3:
|
def body_center(body: Any) -> Vector3:
|
||||||
|
# 取主体包围盒的中心坐标,作为体心的近似。
|
||||||
bbox = body.bounding_box()
|
bbox = body.bounding_box()
|
||||||
return ((bbox.min.X + bbox.max.X) / 2, (bbox.min.Y + bbox.max.Y) / 2, (bbox.min.Z + bbox.max.Z) / 2)
|
return ((bbox.min.X + bbox.max.X) / 2, (bbox.min.Y + bbox.max.Y) / 2, (bbox.min.Z + bbox.max.Z) / 2)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def body_span(body: Any, direction: Vector3) -> float:
|
def body_span(body: Any, direction: Vector3) -> float:
|
||||||
|
# 计算主体在指定方向上的最大跨度:8 个包围盒角点沿方向投影后取极差。
|
||||||
unit = _vector(direction).normalized()
|
unit = _vector(direction).normalized()
|
||||||
bbox = body.bounding_box()
|
bbox = body.bounding_box()
|
||||||
values = [
|
values = [
|
||||||
@@ -146,6 +172,7 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def vertex_coordinates(vertex: Any) -> Vector3:
|
def vertex_coordinates(vertex: Any) -> Vector3:
|
||||||
|
# 提取顶点的三维坐标元组。
|
||||||
return (float(vertex.X), float(vertex.Y), float(vertex.Z))
|
return (float(vertex.X), float(vertex.Y), float(vertex.Z))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -157,10 +184,13 @@ class Build123dGeometryAdapter:
|
|||||||
boundary samples let the runtime prove that precondition instead of
|
boundary samples let the runtime prove that precondition instead of
|
||||||
silently constructing a wrong prismatic solid.
|
silently constructing a wrong prismatic solid.
|
||||||
"""
|
"""
|
||||||
|
# 采样轮廓面的代表性点:面心 + 每条边的 0/0.25/0.5/0.75 参数点,
|
||||||
|
# 用于后续校验目标面到轮廓的距离是否处处一致。
|
||||||
points = [face.center()]
|
points = [face.center()]
|
||||||
for edge in face.edges():
|
for edge in face.edges():
|
||||||
for fraction in (0.0, 0.25, 0.5, 0.75):
|
for fraction in (0.0, 0.25, 0.5, 0.75):
|
||||||
points.append(edge.position_at(fraction))
|
points.append(edge.position_at(fraction))
|
||||||
|
# 去重:彼此距离在 1e-6 内的采样点只保留一个,减少重复求交。
|
||||||
unique: list[Vector] = []
|
unique: list[Vector] = []
|
||||||
for point in points:
|
for point in points:
|
||||||
if not any((point - current).length <= 1e-6 for current in unique):
|
if not any((point - current).length <= 1e-6 for current in unique):
|
||||||
@@ -169,10 +199,12 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _forward_intersection_distance(target: Any, point: Vector, direction: Vector) -> float | None:
|
def _forward_intersection_distance(target: Any, point: Vector, direction: Vector) -> float | None:
|
||||||
|
# 从 point 沿 direction 发一条射线,求与目标的第一个正向交点距离。
|
||||||
try:
|
try:
|
||||||
intersections = target.find_intersection_points(Axis(point, direction)) or []
|
intersections = target.find_intersection_points(Axis(point, direction)) or []
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise ValueError("extent target does not support ray intersection") from error
|
raise ValueError("extent target does not support ray intersection") from error
|
||||||
|
# 只保留方向一致(点积 > 0)的交点,返回其中最近距离;无交点则返回 None。
|
||||||
distances = [
|
distances = [
|
||||||
(hit_point - point).dot(direction)
|
(hit_point - point).dot(direction)
|
||||||
for hit_point, _normal in intersections
|
for hit_point, _normal in intersections
|
||||||
@@ -182,6 +214,7 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
def uniform_intersection_distance(self, target: Any, faces: Iterable[Face], direction: Vector3) -> float:
|
def uniform_intersection_distance(self, target: Any, faces: Iterable[Face], direction: Vector3) -> float:
|
||||||
"""Return a proven uniform positive target distance for a profile set."""
|
"""Return a proven uniform positive target distance for a profile set."""
|
||||||
|
# 对所有轮廓采样点求到目标的距离,各点距离必须一致,简单拉伸才能精确表达终止条件。
|
||||||
unit_direction = _vector(direction).normalized()
|
unit_direction = _vector(direction).normalized()
|
||||||
distances: list[float] = []
|
distances: list[float] = []
|
||||||
for face in faces:
|
for face in faces:
|
||||||
@@ -199,44 +232,44 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def revolve(face: Face, angle_deg: float, axis: AxisSpec) -> Solid:
|
def revolve(face: Face, angle_deg: float, axis: AxisSpec) -> Solid:
|
||||||
|
# 绕给定轴将面旋转指定角度,生成回转实体。
|
||||||
return Solid.revolve(face, angle_deg, Build123dGeometryAdapter.axis(axis))
|
return Solid.revolve(face, angle_deg, Build123dGeometryAdapter.axis(axis))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _body_shape(value: Any) -> Any:
|
|
||||||
"""Normalize boolean results, including disconnected ShapeList values."""
|
|
||||||
if hasattr(value, "bounding_box"):
|
|
||||||
return value
|
|
||||||
shapes = list(value)
|
|
||||||
if not shapes:
|
|
||||||
raise ValueError("Boolean operation produced no shapes")
|
|
||||||
return shapes[0] if len(shapes) == 1 else Compound(shapes)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fuse(body: Any | None, solid: Solid) -> Any:
|
def fuse(body: Any | None, solid: Solid) -> Any:
|
||||||
return solid if body is None else Build123dGeometryAdapter._body_shape(body.fuse(solid))
|
# 布尔并:没有既有主体时,直接以该实体作为新主体。
|
||||||
|
return solid if body is None else body.fuse(solid)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cut(body: Any, tool: Any) -> Any:
|
def cut(body: Any, tool: Any) -> Any:
|
||||||
return Build123dGeometryAdapter._body_shape(body.cut(tool))
|
# 从主体上减去工具实体。
|
||||||
|
return body.cut(tool)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sphere(radius_mm: float, center_mm: Vector3) -> Solid:
|
def sphere(radius_mm: float, center_mm: Vector3) -> Solid:
|
||||||
|
# 以给定球心与半径生成球体实体。
|
||||||
return Solid.make_sphere(radius_mm, Plane(origin=_vector(center_mm)))
|
return Solid.make_sphere(radius_mm, Plane(origin=_vector(center_mm)))
|
||||||
|
|
||||||
def hole_tool(self, spec: HoleSpec, starts: Iterable[Vector3], inward: Vector3, through_depth_mm: float) -> Solid:
|
def hole_tool(self, spec: HoleSpec, starts: Iterable[Vector3], inward: Vector3, through_depth_mm: float) -> Solid:
|
||||||
"""Build a neutral ``HoleSpec`` into one OCC cutting tool."""
|
"""Build a neutral ``HoleSpec`` into one OCC cutting tool."""
|
||||||
|
# 将孔规格 HoleSpec 转成一个可直接切除的 OCC 工具体。
|
||||||
|
# 1. 深度:通孔取贯穿深度(保证穿透),盲孔取规格中的深度。
|
||||||
depth = through_depth_mm if spec.end_condition != "blind" else spec.depth_mm
|
depth = through_depth_mm if spec.end_condition != "blind" else spec.depth_mm
|
||||||
result: Solid | None = None
|
result: Solid | None = None
|
||||||
for start in starts:
|
for start in starts:
|
||||||
|
# 2. 每个孔位:以起点为原点、向内方向为轴向,先生成主孔圆柱。
|
||||||
plane = Plane(origin=_vector(start), z_dir=_vector(inward))
|
plane = Plane(origin=_vector(start), z_dir=_vector(inward))
|
||||||
tool = Solid.make_cylinder(spec.diameter_mm / 2, depth, plane)
|
tool = Solid.make_cylinder(spec.diameter_mm / 2, depth, plane)
|
||||||
|
# 3. 沉孔(counterbore):在主孔上并一个更大直径、更浅的短圆柱。
|
||||||
if spec.counterbore:
|
if spec.counterbore:
|
||||||
diameter, bore_depth = spec.counterbore
|
diameter, bore_depth = spec.counterbore
|
||||||
tool = tool.fuse(Solid.make_cylinder(diameter / 2, bore_depth, plane))
|
tool = tool.fuse(Solid.make_cylinder(diameter / 2, bore_depth, plane))
|
||||||
|
# 4. 锪孔(countersink):按锥角与口径差推得锥深,并一个上大下小的圆锥。
|
||||||
if spec.countersink:
|
if spec.countersink:
|
||||||
diameter, angle = spec.countersink
|
diameter, angle = spec.countersink
|
||||||
sink_depth = ((diameter - spec.diameter_mm) / 2) / math.tan(angle / 2)
|
sink_depth = ((diameter - spec.diameter_mm) / 2) / math.tan(angle / 2)
|
||||||
tool = tool.fuse(Solid.make_cone(diameter / 2, spec.diameter_mm / 2, sink_depth, plane))
|
tool = tool.fuse(Solid.make_cone(diameter / 2, spec.diameter_mm / 2, sink_depth, plane))
|
||||||
|
# 5. 汇总所有孔位的工具实体。
|
||||||
result = self.fuse(result, tool)
|
result = self.fuse(result, tool)
|
||||||
if result is None:
|
if result is None:
|
||||||
raise ValueError("hole has no positions")
|
raise ValueError("hole has no positions")
|
||||||
@@ -244,6 +277,7 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fillet(body: Any, radius_mm: float, edges: Iterable[Edge]) -> Any:
|
def fillet(body: Any, radius_mm: float, edges: Iterable[Edge]) -> Any:
|
||||||
|
# 对指定边以给定半径做圆角。
|
||||||
return body.fillet(radius_mm, list(edges))
|
return body.fillet(radius_mm, list(edges))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -254,8 +288,10 @@ class Build123dGeometryAdapter:
|
|||||||
global edge set or source stable IDs, and is consequently safe after a
|
global edge set or source stable IDs, and is consequently safe after a
|
||||||
body mutation invalidates earlier topology objects.
|
body mutation invalidates earlier topology objects.
|
||||||
"""
|
"""
|
||||||
|
# 从种子边出发,沿“共顶点且切线平行”的边链扩展,得到相切连续的一整组边。
|
||||||
edges = list(body.edges())
|
edges = list(body.edges())
|
||||||
selected = [edge for edge in seeds]
|
selected = [edge for edge in seeds]
|
||||||
|
# 1. 用 is_same 把种子边映射到主体边列表的下标集合。
|
||||||
selected_indexes = {
|
selected_indexes = {
|
||||||
index
|
index
|
||||||
for index, edge in enumerate(edges)
|
for index, edge in enumerate(edges)
|
||||||
@@ -265,6 +301,7 @@ class Build123dGeometryAdapter:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def shared_vertex(first: Edge, second: Edge) -> tuple[float, float] | None:
|
def shared_vertex(first: Edge, second: Edge) -> tuple[float, float] | None:
|
||||||
|
# 找两条边共用的端点,返回各自在该端点处的参数位置;无共用端点返回 None。
|
||||||
first_ends = [(0.0, vertex) for vertex in first.vertices()[:1]] + [(1.0, vertex) for vertex in first.vertices()[-1:]]
|
first_ends = [(0.0, vertex) for vertex in first.vertices()[:1]] + [(1.0, vertex) for vertex in first.vertices()[-1:]]
|
||||||
second_ends = [(0.0, vertex) for vertex in second.vertices()[:1]] + [(1.0, vertex) for vertex in second.vertices()[-1:]]
|
second_ends = [(0.0, vertex) for vertex in second.vertices()[:1]] + [(1.0, vertex) for vertex in second.vertices()[-1:]]
|
||||||
for first_parameter, first_vertex in first_ends:
|
for first_parameter, first_vertex in first_ends:
|
||||||
@@ -273,8 +310,10 @@ class Build123dGeometryAdapter:
|
|||||||
return first_parameter, second_parameter
|
return first_parameter, second_parameter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 共顶点且端点处切线平行(方向无关)的边即构成相切连续链。
|
||||||
# Edges sharing a vertex whose tangents are parallel (orientation is
|
# Edges sharing a vertex whose tangents are parallel (orientation is
|
||||||
# irrelevant) are a tangent-continuous chain.
|
# irrelevant) are a tangent-continuous chain.
|
||||||
|
# 2. BFS 扩展:新加入的边作为候选种子,继续寻找与其相切的下一条边。
|
||||||
pending = list(selected_indexes)
|
pending = list(selected_indexes)
|
||||||
while pending:
|
while pending:
|
||||||
current_index = pending.pop()
|
current_index = pending.pop()
|
||||||
@@ -284,27 +323,33 @@ class Build123dGeometryAdapter:
|
|||||||
shared = shared_vertex(edges[current_index], candidate)
|
shared = shared_vertex(edges[current_index], candidate)
|
||||||
if shared is None:
|
if shared is None:
|
||||||
continue
|
continue
|
||||||
|
# 比较两条边在共用端点处的切线方向(取绝对值以忽略方向)。
|
||||||
first_tangent = edges[current_index].tangent_at(shared[0]).normalized()
|
first_tangent = edges[current_index].tangent_at(shared[0]).normalized()
|
||||||
second_tangent = candidate.tangent_at(shared[1]).normalized()
|
second_tangent = candidate.tangent_at(shared[1]).normalized()
|
||||||
if abs(abs(first_tangent.dot(second_tangent)) - 1.0) <= angular_tolerance:
|
if abs(abs(first_tangent.dot(second_tangent)) - 1.0) <= angular_tolerance:
|
||||||
selected_indexes.add(candidate_index)
|
selected_indexes.add(candidate_index)
|
||||||
pending.append(candidate_index)
|
pending.append(candidate_index)
|
||||||
|
# 3. 按下标映射回边对象列表。
|
||||||
return [edge for index, edge in enumerate(edges) if index in selected_indexes]
|
return [edge for index, edge in enumerate(edges) if index in selected_indexes]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def chamfer(body: Any, distance_mm: float, distance_2_mm: float | None, edges: Iterable[Edge], face: Face | None = None) -> Any:
|
def chamfer(body: Any, distance_mm: float, distance_2_mm: float | None, edges: Iterable[Edge], face: Face | None = None) -> Any:
|
||||||
|
# 对指定边做倒角;distance_2_mm 提供时形成非对称倒角。
|
||||||
return body.chamfer(distance_mm, distance_2_mm, list(edges), face=face)
|
return body.chamfer(distance_mm, distance_2_mm, list(edges), face=face)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mirror(body: Any, plane: PlaneSpec) -> Any:
|
def mirror(body: Any, plane: PlaneSpec) -> Any:
|
||||||
|
# 沿给定平面镜像主体。
|
||||||
return body.mirror(Build123dGeometryAdapter.plane(plane))
|
return body.mirror(Build123dGeometryAdapter.plane(plane))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export(body: Any, path: str) -> None:
|
def export(body: Any, path: str) -> None:
|
||||||
|
# 将主体导出为 STEP 文件。
|
||||||
export_step(body, path)
|
export_step(body, path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def body_geometry(body: Any) -> dict[str, Any]:
|
def body_geometry(body: Any) -> dict[str, Any]:
|
||||||
|
# 汇总主体基本几何信息:包围盒与体积。
|
||||||
bbox = body.bounding_box()
|
bbox = body.bounding_box()
|
||||||
return {
|
return {
|
||||||
"bbox_mm": [bbox.min.X, bbox.min.Y, bbox.min.Z, bbox.max.X, bbox.max.Y, bbox.max.Z],
|
"bbox_mm": [bbox.min.X, bbox.min.Y, bbox.min.Z, bbox.max.X, bbox.max.Y, bbox.max.Z],
|
||||||
@@ -313,6 +358,7 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def topology_records(body: Any, feature_id: str, body_id: str) -> list[TopologyRecord]:
|
def topology_records(body: Any, feature_id: str, body_id: str) -> list[TopologyRecord]:
|
||||||
|
# 从主体导出全部面/边/顶点拓扑记录,供后续特征选择与引用。
|
||||||
records: list[TopologyRecord] = []
|
records: list[TopologyRecord] = []
|
||||||
faces = list(body.faces())
|
faces = list(body.faces())
|
||||||
edges = list(body.edges())
|
edges = list(body.edges())
|
||||||
@@ -320,17 +366,20 @@ class Build123dGeometryAdapter:
|
|||||||
|
|
||||||
def index_for(shape: Any, candidates: list[Any]) -> int | None:
|
def index_for(shape: Any, candidates: list[Any]) -> int | None:
|
||||||
"""Map a subshape returned by a face/edge back to body topology."""
|
"""Map a subshape returned by a face/edge back to body topology."""
|
||||||
|
# 用 is_same 把面/边的子形状映射回主体拓扑列表的下标。
|
||||||
for index, candidate in enumerate(candidates):
|
for index, candidate in enumerate(candidates):
|
||||||
if shape.is_same(candidate):
|
if shape.is_same(candidate):
|
||||||
return index
|
return index
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 1. 建立邻接索引:每条边关联的面集合(edge_faces)。
|
||||||
edge_faces: list[set[int]] = [set() for _edge in edges]
|
edge_faces: list[set[int]] = [set() for _edge in edges]
|
||||||
for face_index, face in enumerate(faces):
|
for face_index, face in enumerate(faces):
|
||||||
for edge in face.edges():
|
for edge in face.edges():
|
||||||
edge_index = index_for(edge, edges)
|
edge_index = index_for(edge, edges)
|
||||||
if edge_index is not None:
|
if edge_index is not None:
|
||||||
edge_faces[edge_index].add(face_index)
|
edge_faces[edge_index].add(face_index)
|
||||||
|
# 2. 建立邻接索引:每个顶点关联的边集合(vertex_edges)。
|
||||||
vertex_edges: list[set[int]] = [set() for _vertex in vertices]
|
vertex_edges: list[set[int]] = [set() for _vertex in vertices]
|
||||||
for edge_index, edge in enumerate(edges):
|
for edge_index, edge in enumerate(edges):
|
||||||
for vertex in edge.vertices():
|
for vertex in edge.vertices():
|
||||||
@@ -339,6 +388,7 @@ class Build123dGeometryAdapter:
|
|||||||
vertex_edges[vertex_index].add(edge_index)
|
vertex_edges[vertex_index].add(edge_index)
|
||||||
|
|
||||||
def edge_signature(edge_index: int) -> str:
|
def edge_signature(edge_index: int) -> str:
|
||||||
|
# 边的特征签名:几何类型 + 长度 + 相邻面数,用作面邻接指纹。
|
||||||
edge = edges[edge_index]
|
edge = edges[edge_index]
|
||||||
return ":".join((
|
return ":".join((
|
||||||
str(edge.geom_type).split(".")[-1].lower(),
|
str(edge.geom_type).split(".")[-1].lower(),
|
||||||
@@ -346,6 +396,8 @@ class Build123dGeometryAdapter:
|
|||||||
str(len(edge_faces[edge_index])),
|
str(len(edge_faces[edge_index])),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# 3. 导出面记录:含包围盒、中心、法向、面积、曲面类型与邻接签名;
|
||||||
|
# 平面面额外写入规范化法向与平面偏移,便于后续按平面匹配。
|
||||||
for index, face in enumerate(faces):
|
for index, face in enumerate(faces):
|
||||||
bbox = face.bounding_box()
|
bbox = face.bounding_box()
|
||||||
center = face.center()
|
center = face.center()
|
||||||
@@ -371,6 +423,7 @@ class Build123dGeometryAdapter:
|
|||||||
record_id=f"{body_id}:face:{index}", kind="face", feature_id=feature_id, body_id=body_id, value=face,
|
record_id=f"{body_id}:face:{index}", kind="face", feature_id=feature_id, body_id=body_id, value=face,
|
||||||
geometry=geometry,
|
geometry=geometry,
|
||||||
))
|
))
|
||||||
|
# 4. 导出边记录:含包围盒、中心、长度、曲线类型与相邻面数;端点坐标可用时附加。
|
||||||
for index, edge in enumerate(edges):
|
for index, edge in enumerate(edges):
|
||||||
bbox = edge.bounding_box()
|
bbox = edge.bounding_box()
|
||||||
center = edge.center()
|
center = edge.center()
|
||||||
@@ -388,6 +441,7 @@ class Build123dGeometryAdapter:
|
|||||||
record_id=f"{body_id}:edge:{index}", kind="edge", feature_id=feature_id, body_id=body_id, value=edge,
|
record_id=f"{body_id}:edge:{index}", kind="edge", feature_id=feature_id, body_id=body_id, value=edge,
|
||||||
geometry=geometry,
|
geometry=geometry,
|
||||||
))
|
))
|
||||||
|
# 5. 导出顶点记录:含坐标与关联边数。
|
||||||
for index, vertex in enumerate(vertices):
|
for index, vertex in enumerate(vertices):
|
||||||
point = [vertex.X, vertex.Y, vertex.Z]
|
point = [vertex.X, vertex.Y, vertex.Z]
|
||||||
records.append(TopologyRecord(
|
records.append(TopologyRecord(
|
||||||
|
|||||||
@@ -184,9 +184,22 @@ def _workplane(sketch: dict[str, Any]) -> dict[str, Any]:
|
|||||||
A conservative XY fallback keeps the record valid when the exporter did
|
A conservative XY fallback keeps the record valid when the exporter did
|
||||||
not provide a usable matrix.
|
not provide a usable matrix.
|
||||||
"""
|
"""
|
||||||
|
workplane = _workplane_from_matrix(sketch)
|
||||||
|
if workplane is not None:
|
||||||
|
return workplane
|
||||||
|
return {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "y_dir": [0.0, 1.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
||||||
|
|
||||||
|
|
||||||
|
def _workplane_from_matrix(sketch: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
"""Parse the exporter's model-to-sketch matrix into a workplane.
|
||||||
|
|
||||||
|
Returns None when no usable rigid rotation can be extracted (missing
|
||||||
|
matrix or a non-orthonormal frame). The caller then decides between a
|
||||||
|
rebuilt workplane and the conservative XY fallback.
|
||||||
|
"""
|
||||||
matrix = sketch.get("model_to_sketch_transform")
|
matrix = sketch.get("model_to_sketch_transform")
|
||||||
if not isinstance(matrix, list) or len(matrix) != 16 or not all(isinstance(item, (int, float)) for item in matrix):
|
if not isinstance(matrix, list) or len(matrix) != 16 or not all(isinstance(item, (int, float)) for item in matrix):
|
||||||
return {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "y_dir": [0.0, 1.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
return None
|
||||||
# The exporter serializes MathTransform in row-major form.
|
# The exporter serializes MathTransform in row-major form.
|
||||||
rotation = [matrix[0:3], matrix[4:7], matrix[8:11]]
|
rotation = [matrix[0:3], matrix[4:7], matrix[8:11]]
|
||||||
translation = [matrix[3], matrix[7], matrix[11]]
|
translation = [matrix[3], matrix[7], matrix[11]]
|
||||||
@@ -195,9 +208,69 @@ def _workplane(sketch: dict[str, Any]) -> dict[str, Any]:
|
|||||||
x_dir = _normalize([inverse_rotation[row][0] for row in range(3)])
|
x_dir = _normalize([inverse_rotation[row][0] for row in range(3)])
|
||||||
y_dir = _normalize([inverse_rotation[row][1] for row in range(3)])
|
y_dir = _normalize([inverse_rotation[row][1] for row in range(3)])
|
||||||
normal = _normalize(_cross(x_dir, y_dir))
|
normal = _normalize(_cross(x_dir, y_dir))
|
||||||
|
# A rigid model-to-sketch rotation keeps the sketch axes orthogonal. The
|
||||||
|
# exporter sometimes emits a non-orthogonal matrix for profiles placed on
|
||||||
|
# a copied reference plane; only trust a frame that is truly orthonormal.
|
||||||
|
if abs(x_dir[0] * y_dir[0] + x_dir[1] * y_dir[1] + x_dir[2] * y_dir[2]) > 1e-9:
|
||||||
|
return None
|
||||||
return {"origin_mm": [_mm(value) for value in origin_m], "x_dir": x_dir, "y_dir": y_dir, "normal": normal}
|
return {"origin_mm": [_mm(value) for value in origin_m], "x_dir": x_dir, "y_dir": y_dir, "normal": normal}
|
||||||
|
|
||||||
|
|
||||||
|
def _workplane_is_orthonormal(workplane: dict[str, Any]) -> bool:
|
||||||
|
"""True when a workplane's sketch axes describe an orthonormal frame."""
|
||||||
|
x_dir = workplane.get("x_dir")
|
||||||
|
y_dir = workplane.get("y_dir")
|
||||||
|
if not x_dir or not y_dir:
|
||||||
|
return False
|
||||||
|
return abs(x_dir[0] * y_dir[0] + x_dir[1] * y_dir[1] + x_dir[2] * y_dir[2]) <= 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def _workplane_rebuilt(parent_plane: dict[str, Any] | None, axis_direction: list[float] | None) -> dict[str, Any] | None:
|
||||||
|
"""Rebuild a workplane from the parent reference plane and the revolve axis.
|
||||||
|
|
||||||
|
When the exporter's matrix is unusable, the profile still lies on its
|
||||||
|
parent reference plane and the revolve axis runs along the sketch V axis.
|
||||||
|
The captured axis direction is world space, so V maps directly onto it and
|
||||||
|
U is completed by the cross product to keep a right-handed frame.
|
||||||
|
"""
|
||||||
|
if not parent_plane or not axis_direction:
|
||||||
|
return None
|
||||||
|
normal = _normalize(list(parent_plane.get("normal") or []))
|
||||||
|
y_dir = _normalize(list(axis_direction))
|
||||||
|
if abs(normal[0] * y_dir[0] + normal[1] * y_dir[1] + normal[2] * y_dir[2]) > 0.9:
|
||||||
|
# The axis would lie parallel to the plane normal and the revolve
|
||||||
|
# would degenerate to zero volume; do not trust this rebuild.
|
||||||
|
return None
|
||||||
|
x_dir = _normalize(_cross(y_dir, normal))
|
||||||
|
origin_mm = list(parent_plane.get("origin_mm") or [0.0, 0.0, 0.0])
|
||||||
|
return {"origin_mm": origin_mm, "x_dir": x_dir, "y_dir": y_dir, "normal": normal}
|
||||||
|
|
||||||
|
|
||||||
|
def _axis_in_world(axis: dict[str, Any], workplane: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Transform a revolve axis captured in sketch-local coordinates to world space."""
|
||||||
|
origin = axis.get("origin_mm")
|
||||||
|
direction = axis.get("direction")
|
||||||
|
if not origin or not direction or not workplane.get("x_dir"):
|
||||||
|
return axis
|
||||||
|
origin_mm = workplane.get("origin_mm") or [0.0, 0.0, 0.0]
|
||||||
|
x_dir = workplane["x_dir"]
|
||||||
|
y_dir = workplane.get("y_dir") or [0.0, 1.0, 0.0]
|
||||||
|
normal = workplane.get("normal") or [0.0, 0.0, 1.0]
|
||||||
|
world_origin = [
|
||||||
|
origin_mm[index] + origin[0] * x_dir[index] + origin[1] * y_dir[index] + origin[2] * normal[index]
|
||||||
|
for index in range(3)
|
||||||
|
]
|
||||||
|
world_direction = _normalize([
|
||||||
|
direction[0] * x_dir[index] + direction[1] * y_dir[index] + direction[2] * normal[index]
|
||||||
|
for index in range(3)
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
**axis,
|
||||||
|
"origin_mm": [round(value, 9) for value in world_origin],
|
||||||
|
"direction": world_direction if math.sqrt(sum(c * c for c in world_direction)) > 1e-12 else list(direction),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _workplane_from_plane_reference(reference: Any) -> dict[str, Any] | None:
|
def _workplane_from_plane_reference(reference: Any) -> dict[str, Any] | None:
|
||||||
"""Build a workplane from an exporter-captured planar face signature."""
|
"""Build a workplane from an exporter-captured planar face signature."""
|
||||||
if not isinstance(reference, dict):
|
if not isinstance(reference, dict):
|
||||||
@@ -892,6 +965,25 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
sketch_id_by_name = {str(feature.get("name")): sketch_id_by_source[source_id] for feature in sketch_features if (source_id := _identity_key(feature)) and feature.get("name") is not None}
|
sketch_id_by_name = {str(feature.get("name")): sketch_id_by_source[source_id] for feature in sketch_features if (source_id := _identity_key(feature)) and feature.get("name") is not None}
|
||||||
sketch_workplanes_by_parent_source: dict[str, tuple[str, dict[str, Any]]] = {}
|
sketch_workplanes_by_parent_source: dict[str, tuple[str, dict[str, Any]]] = {}
|
||||||
sketch_workplanes_by_parent_name: dict[str, tuple[str, dict[str, Any]]] = {}
|
sketch_workplanes_by_parent_name: dict[str, tuple[str, dict[str, Any]]] = {}
|
||||||
|
# sketch source id -> the parent reference-plane source id it is drawn on
|
||||||
|
sketch_parent_plane_by_source: dict[str, str] = {}
|
||||||
|
# sketch source id -> the world-space direction of its revolve axis
|
||||||
|
sketch_axis_direction_by_source: dict[str, list[float]] = {}
|
||||||
|
# sketch source ids whose exporter matrix did not form an orthonormal frame
|
||||||
|
sketch_matrix_unreliable: set[str] = set()
|
||||||
|
sketch_record_by_id: dict[str, dict[str, Any]] = {}
|
||||||
|
# Prescan the revolve features so each profile's workplane can be rebuilt
|
||||||
|
# from the axis direction captured on its own revolve feature.
|
||||||
|
for feature in features:
|
||||||
|
if _feature_family(feature) not in {"Revolution", "RevCut"}:
|
||||||
|
continue
|
||||||
|
props, _ = _record_values(feature)
|
||||||
|
axis, _ = _axis_from_reference(props.get("Axis"))
|
||||||
|
if not axis or not axis.get("direction"):
|
||||||
|
continue
|
||||||
|
for parent in _record_parents(feature):
|
||||||
|
if str(parent.get("effective_type") or "") in SKETCH_TYPES and (sketch_source_id := _identity_key(parent)):
|
||||||
|
sketch_axis_direction_by_source.setdefault(sketch_source_id, list(axis["direction"]))
|
||||||
sketches = []
|
sketches = []
|
||||||
for feature in sketch_features:
|
for feature in sketch_features:
|
||||||
source_id = _identity_key(feature)
|
source_id = _identity_key(feature)
|
||||||
@@ -901,10 +993,15 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
workplane = _workplane(sketch)
|
workplane = _workplane(sketch)
|
||||||
record = {"id": sketch_id_by_source[source_id], "name": str(feature.get("name") or sketch_id_by_source[source_id]), "role": "profile", "workplane": workplane, "profile": _analytic_profile(sketch)}
|
record = {"id": sketch_id_by_source[source_id], "name": str(feature.get("name") or sketch_id_by_source[source_id]), "role": "profile", "workplane": workplane, "profile": _analytic_profile(sketch)}
|
||||||
sketches.append(record)
|
sketches.append(record)
|
||||||
|
sketch_record_by_id[record["id"]] = record
|
||||||
|
if not _workplane_is_orthonormal(workplane):
|
||||||
|
sketch_matrix_unreliable.add(source_id)
|
||||||
for parent in _record_parents(feature):
|
for parent in _record_parents(feature):
|
||||||
parent_source_id = _identity_key(parent)
|
parent_source_id = _identity_key(parent)
|
||||||
if parent_source_id:
|
if parent_source_id:
|
||||||
sketch_workplanes_by_parent_source.setdefault(parent_source_id, (record["id"], workplane))
|
sketch_workplanes_by_parent_source.setdefault(parent_source_id, (record["id"], workplane))
|
||||||
|
if _feature_family(parent) == "RefPlane":
|
||||||
|
sketch_parent_plane_by_source[source_id] = parent_source_id
|
||||||
if parent.get("name") is not None:
|
if parent.get("name") is not None:
|
||||||
sketch_workplanes_by_parent_name.setdefault(str(parent["name"]), (record["id"], workplane))
|
sketch_workplanes_by_parent_name.setdefault(str(parent["name"]), (record["id"], workplane))
|
||||||
|
|
||||||
@@ -944,6 +1041,11 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
cdsl_features: list[dict[str, Any]] = []
|
cdsl_features: list[dict[str, Any]] = []
|
||||||
previous_id: str | None = None
|
previous_id: str | None = None
|
||||||
diagnostics: list[dict[str, Any]] = []
|
diagnostics: list[dict[str, Any]] = []
|
||||||
|
# Sketch source ids whose workplane was successfully rebuilt once their
|
||||||
|
# parent reference plane was resolved. Their revolve axis is expressed in
|
||||||
|
# sketch-local coordinates and must be transformed to world space too.
|
||||||
|
sketch_fix_applied: set[str] = set()
|
||||||
|
sketch_source_by_id = {sketch_id: source_id for source_id, sketch_id in sketch_id_by_source.items()}
|
||||||
|
|
||||||
for feature in model_features:
|
for feature in model_features:
|
||||||
source_id = _identity_key(feature)
|
source_id = _identity_key(feature)
|
||||||
@@ -985,6 +1087,14 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
selectors.append(axis_selector)
|
selectors.append(axis_selector)
|
||||||
if props.get("Axis") and any("no captured semantic selections" in item for item in unresolved):
|
if props.get("Axis") and any("no captured semantic selections" in item for item in unresolved):
|
||||||
unresolved = [item for item in unresolved if "no captured semantic selections" not in item]
|
unresolved = [item for item in unresolved if "no captured semantic selections" not in item]
|
||||||
|
# When the profile workplane was rebuilt, the captured revolve axis
|
||||||
|
# is still in sketch-local coordinates; map it to world space so
|
||||||
|
# the cut lands on the resolved parent reference plane.
|
||||||
|
if sketch_id and (axis_source_id := sketch_source_by_id.get(sketch_id)) in sketch_fix_applied:
|
||||||
|
sketch_workplane = sketch_record_by_id.get(sketch_id)
|
||||||
|
axis = params.get("axis")
|
||||||
|
if isinstance(axis, dict) and sketch_workplane is not None:
|
||||||
|
params["axis"] = _axis_in_world(axis, sketch_workplane["workplane"])
|
||||||
elif family == "HoleWzd":
|
elif family == "HoleWzd":
|
||||||
atomic_id = "hole_wizard"
|
atomic_id = "hole_wizard"
|
||||||
params, positions = _hole_params(props, methods)
|
params, positions = _hole_params(props, methods)
|
||||||
@@ -1065,6 +1175,21 @@ def convert_evidence(evidence: dict[str, Any], *, source_name: str, step_path: P
|
|||||||
if "unresolved" not in params["plane"]:
|
if "unresolved" not in params["plane"]:
|
||||||
more_unresolved = [item for item in more_unresolved if item != "reference plane orientation was not captured"]
|
more_unresolved = [item for item in more_unresolved if item != "reference plane orientation was not captured"]
|
||||||
unresolved = [item for item in unresolved if item != "reference plane orientation was not captured"]
|
unresolved = [item for item in unresolved if item != "reference plane orientation was not captured"]
|
||||||
|
plane = params.get("plane")
|
||||||
|
if isinstance(plane, dict) and "origin_mm" in plane and "unresolved" not in plane:
|
||||||
|
# Profiles whose exporter matrix was unusable are rebuilt from
|
||||||
|
# this resolved reference plane (normal + origin) and the axis
|
||||||
|
# direction of the profile's own revolve feature.
|
||||||
|
for sketch_source_id, ref_source_id in sketch_parent_plane_by_source.items():
|
||||||
|
if ref_source_id != source_id or sketch_source_id not in sketch_matrix_unreliable:
|
||||||
|
continue
|
||||||
|
rebuilt = _workplane_rebuilt(plane, sketch_axis_direction_by_source.get(sketch_source_id))
|
||||||
|
if rebuilt is None:
|
||||||
|
continue
|
||||||
|
sketch_record = sketch_record_by_id.get(sketch_id_by_source.get(sketch_source_id) or "")
|
||||||
|
if sketch_record is not None:
|
||||||
|
sketch_record["workplane"] = rebuilt
|
||||||
|
sketch_fix_applied.add(sketch_source_id)
|
||||||
for reference in params.get("references", []):
|
for reference in params.get("references", []):
|
||||||
if reference not in selectors:
|
if reference not in selectors:
|
||||||
selectors.append(reference)
|
selectors.append(reference)
|
||||||
|
|||||||
Reference in New Issue
Block a user