重构 改为标准化输出

This commit is contained in:
2026-09-01 14:10:23 +08:00
parent 7533b298f1
commit bc87bd6d2d
10151 changed files with 1709487 additions and 11776 deletions
@@ -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()