重构 改为标准化输出
This commit is contained in:
@@ -451,6 +451,11 @@ class Build123dGeometryAdapter:
|
||||
|
||||
# 3. 导出面记录:含包围盒、中心、法向、面积、曲面类型与邻接签名;
|
||||
# 平面面额外写入规范化法向与平面偏移,便于后续按平面匹配。
|
||||
# 圆柱面还保存轴、半径和共享边关联的平面面。这使 verifier 能从
|
||||
# 实际 B-rep 证明孔是否连接两个方向相反的外部平面,而不是根据
|
||||
# author 传入的 blind-depth 文字猜测“贯穿”。
|
||||
face_edge_indexes: list[set[int]] = []
|
||||
face_geometries: list[dict[str, Any]] = []
|
||||
for index, face in enumerate(faces):
|
||||
bbox = face.bounding_box()
|
||||
center = face.center()
|
||||
@@ -472,10 +477,162 @@ class Build123dGeometryAdapter:
|
||||
)
|
||||
geometry["plane_normal"] = list(plane_normal)
|
||||
geometry["plane_offset_mm"] = plane_offset
|
||||
boundary_loops: list[list[list[float]]] = []
|
||||
for wire in face.wires():
|
||||
samples: list[list[float]] = []
|
||||
for edge in wire.edges():
|
||||
curve_type = str(edge.geom_type).split(".")[-1].lower()
|
||||
fractions = [step / 16 for step in range(16)] if curve_type in {"circle", "ellipse"} else [0.0]
|
||||
for fraction in fractions:
|
||||
point = edge.position_at(fraction)
|
||||
value = [float(point.X), float(point.Y), float(point.Z)]
|
||||
if not samples or sum((value[axis] - samples[-1][axis]) ** 2 for axis in range(3)) > 1e-12:
|
||||
samples.append(value)
|
||||
if len(samples) >= 3:
|
||||
boundary_loops.append(samples)
|
||||
if boundary_loops:
|
||||
geometry["boundary_loops_mm"] = boundary_loops
|
||||
elif geometry["surface_type"] in {"cylinder", "cone"}:
|
||||
axis = face.axis_of_rotation
|
||||
if axis is None:
|
||||
raise ValueError("rotational face is missing an axis")
|
||||
direction = axis.direction
|
||||
origin = axis.position
|
||||
geometry["axis_origin_mm"] = [origin.X, origin.Y, origin.Z]
|
||||
geometry["axis_direction"] = [direction.X, direction.Y, direction.Z]
|
||||
raw_cylinder_radius = face.radius if geometry["surface_type"] == "cylinder" else None
|
||||
if geometry["surface_type"] == "cone":
|
||||
boundary_radii: list[float] = []
|
||||
for edge in face.edges():
|
||||
if str(edge.geom_type).split(".")[-1].lower() != "circle":
|
||||
continue
|
||||
try:
|
||||
boundary_radii.append(float(edge.radius))
|
||||
except ValueError:
|
||||
continue
|
||||
geometry["boundary_radii_mm"] = sorted(boundary_radii)
|
||||
geometry["semi_angle_deg"] = float(face.semi_angle) if face.semi_angle is not None else None
|
||||
# ``through`` alone describes a cylinder spanning two opposed
|
||||
# planar faces. That applies to both a through bore and the
|
||||
# outside wall of a cylindrical extrusion. Classify the B-rep
|
||||
# face by its oriented normal so downstream acceptance claims
|
||||
# can prove holes without mistaking an exterior wall for one.
|
||||
unit_axis = (direction.X, direction.Y, direction.Z)
|
||||
radial = (center.X - origin.X, center.Y - origin.Y, center.Z - origin.Z)
|
||||
axial_projection = sum(radial[component] * unit_axis[component] for component in range(3))
|
||||
radial = tuple(radial[component] - axial_projection * unit_axis[component] for component in range(3))
|
||||
radial_length = sum(component * component for component in radial) ** 0.5
|
||||
if geometry["surface_type"] == "cylinder":
|
||||
# OCC can report a cylinder surface with ``radius=None``
|
||||
# after a non-planar-side Boolean cut. The face centre is
|
||||
# still on that cylinder, so its perpendicular distance to
|
||||
# the rotation axis is an equivalent measured radius. Do
|
||||
# not fail an otherwise valid build merely because that
|
||||
# optional OCC convenience property is absent.
|
||||
if isinstance(raw_cylinder_radius, (int, float)) and math.isfinite(float(raw_cylinder_radius)):
|
||||
geometry["radius_mm"] = float(raw_cylinder_radius)
|
||||
elif radial_length > 1e-9:
|
||||
geometry["radius_mm"] = radial_length
|
||||
if radial_length > 1e-9:
|
||||
normal_components = (normal.X, normal.Y, normal.Z)
|
||||
alignment = sum(float(normal_components[component]) * radial[component] for component in range(3)) / radial_length
|
||||
geometry["radial_normal_alignment"] = alignment
|
||||
geometry["cylinder_role"] = "outer" if alignment > 0.5 else "inner" if alignment < -0.5 else "unknown"
|
||||
else:
|
||||
geometry["cylinder_role"] = "unknown"
|
||||
face_edge_indexes.append(set(boundary_edge_indexes))
|
||||
face_geometries.append(geometry)
|
||||
records.append(TopologyRecord(
|
||||
record_id=f"{body_id}:face:{index}", kind="face", feature_id=feature_id, body_id=body_id, value=face,
|
||||
geometry=geometry,
|
||||
))
|
||||
plane_indexes = [index for index, geometry in enumerate(face_geometries) if geometry["surface_type"] == "plane"]
|
||||
|
||||
def directly_linked_planes(face_index: int) -> list[int]:
|
||||
return [
|
||||
plane_index
|
||||
for plane_index in plane_indexes
|
||||
if face_edge_indexes[face_index].intersection(face_edge_indexes[plane_index])
|
||||
]
|
||||
|
||||
def same_inner_rotational_channel(first: int, second: int) -> bool:
|
||||
"""Whether two inner rotational faces share one B-rep bore channel."""
|
||||
if not face_edge_indexes[first].intersection(face_edge_indexes[second]):
|
||||
return False
|
||||
left, right = face_geometries[first], face_geometries[second]
|
||||
if left.get("cylinder_role") != "inner" or right.get("cylinder_role") != "inner":
|
||||
return False
|
||||
left_axis, right_axis = left.get("axis_direction"), right.get("axis_direction")
|
||||
left_origin, right_origin = left.get("axis_origin_mm"), right.get("axis_origin_mm")
|
||||
if not all(isinstance(value, list) and len(value) == 3 for value in (left_axis, right_axis, left_origin, right_origin)):
|
||||
return False
|
||||
try:
|
||||
left_direction = tuple(float(value) for value in left_axis)
|
||||
right_direction = tuple(float(value) for value in right_axis)
|
||||
offset = tuple(float(left_origin[index]) - float(right_origin[index]) for index in range(3))
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
alignment = sum(left_direction[index] * right_direction[index] for index in range(3))
|
||||
if abs(alignment) < 1.0 - 1e-6:
|
||||
return False
|
||||
axial_offset = sum(offset[index] * left_direction[index] for index in range(3))
|
||||
radial_offset = tuple(offset[index] - axial_offset * left_direction[index] for index in range(3))
|
||||
return sum(value * value for value in radial_offset) ** 0.5 <= 1e-5
|
||||
|
||||
inner_rotational_indexes = [
|
||||
index
|
||||
for index, geometry in enumerate(face_geometries)
|
||||
if geometry["surface_type"] in {"cylinder", "cone"} and geometry.get("cylinder_role") == "inner"
|
||||
]
|
||||
|
||||
def channel_plane_indexes(start: int) -> list[int]:
|
||||
"""Collect endpoint planes through joined, co-axial inner faces.
|
||||
|
||||
A countersink or counterbore splits a physical bore into a cone and
|
||||
a cylinder. The cylinder has only one direct planar neighbour, so
|
||||
direct adjacency alone cannot prove that the complete channel exits
|
||||
the part. Traverse shared B-rep edges only across co-axial inner
|
||||
rotational faces, then inspect the channel's actual plane ends.
|
||||
"""
|
||||
pending = [start]
|
||||
visited: set[int] = set()
|
||||
endpoints: set[int] = set()
|
||||
while pending:
|
||||
index = pending.pop()
|
||||
if index in visited:
|
||||
continue
|
||||
visited.add(index)
|
||||
endpoints.update(directly_linked_planes(index))
|
||||
pending.extend(
|
||||
candidate
|
||||
for candidate in inner_rotational_indexes
|
||||
if candidate not in visited and same_inner_rotational_channel(index, candidate)
|
||||
)
|
||||
return sorted(endpoints)
|
||||
|
||||
def spans_opposed_planes(linked: list[int], axis: Any) -> bool:
|
||||
if not isinstance(axis, list) or len(axis) != 3:
|
||||
return False
|
||||
try:
|
||||
direction = tuple(float(value) for value in axis)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return any(
|
||||
sum(float(face_geometries[first]["normal"][component]) * float(face_geometries[second]["normal"][component]) for component in range(3)) <= -0.99
|
||||
and all(abs(sum(float(face_geometries[position]["normal"][component]) * direction[component] for component in range(3))) >= 0.99 for position in (first, second))
|
||||
for first in linked
|
||||
for second in linked
|
||||
if first < second
|
||||
)
|
||||
|
||||
for index, geometry in enumerate(face_geometries):
|
||||
if geometry["surface_type"] != "cylinder":
|
||||
continue
|
||||
linked = directly_linked_planes(index)
|
||||
geometry["connected_plane_ids"] = [records[plane_index].record_id for plane_index in linked]
|
||||
channel_linked = channel_plane_indexes(index) if geometry.get("cylinder_role") == "inner" else linked
|
||||
geometry["channel_connected_plane_ids"] = [records[plane_index].record_id for plane_index in channel_linked]
|
||||
geometry["through"] = spans_opposed_planes(channel_linked, geometry.get("axis_direction"))
|
||||
# 4. 导出边记录:含包围盒、中心、长度、曲线类型与相邻面数;端点坐标可用时附加。
|
||||
for index, edge in enumerate(edges):
|
||||
bbox = edge.bounding_box()
|
||||
|
||||
Reference in New Issue
Block a user