feat: 完善 STEP 一级参数化编辑识别与关系式建模
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.GeomAbs import GeomAbs_Cylinder, GeomAbs_Plane
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.TopAbs import TopAbs_EDGE
|
||||
from OCC.Core.TopExp import topexp
|
||||
from OCC.Core.TopTools import TopTools_IndexedMapOfShape
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
|
||||
from .geometry_utils import (
|
||||
_axis_parameter,
|
||||
_direction_dot,
|
||||
_point_axis_distance,
|
||||
_shape_axis_interval,
|
||||
_shape_diagonal,
|
||||
_surface_center,
|
||||
)
|
||||
|
||||
|
||||
ANGULAR_TOLERANCE = 1.0e-7
|
||||
COVERAGE_TOLERANCE = 0.82
|
||||
EXTERNAL_COAXIAL_CONFIDENCE_BOOST = 0.08
|
||||
EXTERNAL_TANGENT_CONFIDENCE_BOOST = 0.03
|
||||
EXTERNAL_OPENING_PLANE_CONFIDENCE_BOOST = 0.04
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecognitionFace:
|
||||
face_id: int
|
||||
solid_id: int
|
||||
surface_type: str
|
||||
area: float
|
||||
centroid: tuple[float, float, float]
|
||||
boundary_edge_ids: tuple[int, ...]
|
||||
adjacent_face_ids: tuple[int, ...]
|
||||
axis_point: object | None = None
|
||||
axis_direction: object | None = None
|
||||
radius: float | None = None
|
||||
axis_interval: tuple[float, float] | None = None
|
||||
angular_span: float | None = None
|
||||
plane_parameter: float | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecognitionRelation:
|
||||
relation_type: str
|
||||
face_ids: tuple[int, ...]
|
||||
residual: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecognitionGraph:
|
||||
solid_id: int
|
||||
face_ids: tuple[int, ...]
|
||||
faces: tuple[RecognitionFace, ...]
|
||||
relation_counts: dict[str, int]
|
||||
relations: tuple[RecognitionRelation, ...]
|
||||
|
||||
def face(self, face_id: int) -> RecognitionFace | None:
|
||||
for item in self.faces:
|
||||
if item.face_id == int(face_id):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThroughHoleRegion:
|
||||
face_ids: tuple[int, ...]
|
||||
solid_id: int
|
||||
diameter: float
|
||||
axis_interval: tuple[float, float]
|
||||
angular_coverage: float
|
||||
opening_face_ids: tuple[int, ...]
|
||||
confidence: float
|
||||
|
||||
|
||||
def build_recognition_graph(model: object, solid_id: int) -> RecognitionGraph:
|
||||
face_ids = tuple(
|
||||
face_id
|
||||
for face_id, item in enumerate(getattr(model, "face_solid_ids", ()))
|
||||
if int(item) == int(solid_id)
|
||||
)
|
||||
faces: list[RecognitionFace] = []
|
||||
for face_id in face_ids:
|
||||
face = getattr(model, "faces")[face_id]
|
||||
boundary_edge_ids = tuple(_face_boundary_edge_ids(model, face_id))
|
||||
adjacent_face_ids = tuple(sorted(_adjacent_face_ids(model, boundary_edge_ids, face_id)))
|
||||
surf = BRepAdaptor_Surface(face)
|
||||
surface_type = "other"
|
||||
axis_point = None
|
||||
axis_direction = None
|
||||
radius: float | None = None
|
||||
axis_interval: tuple[float, float] | None = None
|
||||
angular_span: float | None = None
|
||||
plane_parameter: float | None = None
|
||||
if surf.GetType() == GeomAbs_Cylinder:
|
||||
surface_type = "cylinder"
|
||||
cylinder = surf.Cylinder()
|
||||
axis = cylinder.Axis()
|
||||
axis_point = axis.Location()
|
||||
axis_direction = axis.Direction()
|
||||
radius = float(cylinder.Radius())
|
||||
axis_interval = _shape_axis_interval(face, axis_point, axis_direction)
|
||||
angular_span = abs(float(surf.LastUParameter()) - float(surf.FirstUParameter()))
|
||||
elif surf.GetType() == GeomAbs_Plane:
|
||||
surface_type = "plane"
|
||||
plane = surf.Plane()
|
||||
axis_point = plane.Location()
|
||||
axis_direction = plane.Axis().Direction()
|
||||
plane_parameter = _axis_parameter(axis_point, axis_direction, plane.Location())
|
||||
area, centroid = _surface_metrics(face)
|
||||
faces.append(
|
||||
RecognitionFace(
|
||||
face_id=face_id,
|
||||
solid_id=int(solid_id),
|
||||
surface_type=surface_type,
|
||||
area=area,
|
||||
centroid=centroid,
|
||||
boundary_edge_ids=boundary_edge_ids,
|
||||
adjacent_face_ids=adjacent_face_ids,
|
||||
axis_point=axis_point,
|
||||
axis_direction=axis_direction,
|
||||
radius=radius,
|
||||
axis_interval=axis_interval,
|
||||
angular_span=angular_span,
|
||||
plane_parameter=plane_parameter,
|
||||
)
|
||||
)
|
||||
relations = infer_recognition_relations(faces, _recognition_tolerance(model))
|
||||
relations.extend(_external_recognition_relations(model, face_ids))
|
||||
relation_counts = dict(Counter(item.relation_type for item in relations))
|
||||
return RecognitionGraph(
|
||||
solid_id=int(solid_id),
|
||||
face_ids=face_ids,
|
||||
faces=tuple(faces),
|
||||
relation_counts=relation_counts,
|
||||
relations=tuple(relations),
|
||||
)
|
||||
|
||||
|
||||
def infer_recognition_relations(
|
||||
faces: Iterable[RecognitionFace],
|
||||
tolerance: float,
|
||||
) -> list[RecognitionRelation]:
|
||||
items = list(faces)
|
||||
relations: list[RecognitionRelation] = []
|
||||
for face in items:
|
||||
for adjacent_id in face.adjacent_face_ids:
|
||||
if face.face_id < adjacent_id:
|
||||
relations.append(RecognitionRelation("adjacent", (face.face_id, adjacent_id), 0.0))
|
||||
|
||||
for index, left in enumerate(items):
|
||||
for right in items[index + 1 :]:
|
||||
if left.surface_type == "plane" and right.surface_type == "plane":
|
||||
relation = _plane_relation(left, right, tolerance)
|
||||
if relation is not None:
|
||||
relations.append(relation)
|
||||
if left.surface_type == "cylinder" and right.surface_type == "cylinder":
|
||||
relation = _cylinder_relation(left, right, tolerance)
|
||||
if relation is not None:
|
||||
relations.append(relation)
|
||||
return relations
|
||||
|
||||
|
||||
def recognize_through_hole_regions(model: object, solid_id: int | None = None) -> list[ThroughHoleRegion]:
|
||||
solid_ids = _solid_ids(model, solid_id)
|
||||
cache_key = ("all", solid_ids) if solid_id is None else ("solid", int(solid_id))
|
||||
cache = getattr(model, "_through_hole_regions_cache", None)
|
||||
if isinstance(cache, dict) and cache_key in cache:
|
||||
return list(cache[cache_key])
|
||||
|
||||
regions: list[ThroughHoleRegion] = []
|
||||
for current_solid_id in solid_ids:
|
||||
graph = _cached_recognition_graph(model, current_solid_id)
|
||||
regions.extend(_recognize_graph_through_hole_regions(model, graph))
|
||||
result = _dedupe_regions(regions)
|
||||
if isinstance(cache, dict):
|
||||
cache[cache_key] = list(result)
|
||||
return result
|
||||
|
||||
|
||||
def recognition_summary(model: object) -> dict[str, object]:
|
||||
solid_ids = _solid_ids(model, None)
|
||||
relation_counts: Counter[str] = Counter()
|
||||
hole_count = 0
|
||||
face_count = 0
|
||||
for solid_id in solid_ids:
|
||||
graph = _cached_recognition_graph(model, solid_id)
|
||||
relation_counts.update(graph.relation_counts)
|
||||
face_count += len(graph.face_ids)
|
||||
hole_count += len(recognize_through_hole_regions(model, solid_id))
|
||||
return {
|
||||
"source": "internal-recognition-graph",
|
||||
"solid_count": len(solid_ids),
|
||||
"face_count": face_count,
|
||||
"relation_counts": dict(relation_counts),
|
||||
"through_hole_region_count": hole_count,
|
||||
}
|
||||
|
||||
|
||||
def _cached_recognition_graph(model: object, solid_id: int) -> RecognitionGraph:
|
||||
cache = getattr(model, "_recognition_graph_cache", None)
|
||||
if isinstance(cache, dict) and int(solid_id) in cache:
|
||||
return cache[int(solid_id)]
|
||||
graph = build_recognition_graph(model, int(solid_id))
|
||||
if isinstance(cache, dict):
|
||||
cache[int(solid_id)] = graph
|
||||
return graph
|
||||
|
||||
|
||||
def _recognize_graph_through_hole_regions(model: object, graph: RecognitionGraph) -> list[ThroughHoleRegion]:
|
||||
cylinders = [face for face in graph.faces if face.surface_type == "cylinder" and face.radius and face.radius > 0]
|
||||
if not cylinders:
|
||||
return []
|
||||
tolerance = _recognition_tolerance(model)
|
||||
visited: set[int] = set()
|
||||
regions: list[ThroughHoleRegion] = []
|
||||
for source in cylinders:
|
||||
if source.face_id in visited:
|
||||
continue
|
||||
group = _cocylindrical_interval_group(model, cylinders, source, tolerance)
|
||||
visited.update(face.face_id for face in group)
|
||||
if not group:
|
||||
continue
|
||||
coverage = sum(min(abs(float(face.angular_span or 0.0)), math.tau) for face in group)
|
||||
if coverage < math.tau * COVERAGE_TOLERANCE:
|
||||
continue
|
||||
intervals = [face.axis_interval for face in group if face.axis_interval is not None]
|
||||
if not intervals:
|
||||
continue
|
||||
v_min = min(float(item[0]) for item in intervals)
|
||||
v_max = max(float(item[1]) for item in intervals)
|
||||
opening_face_ids = _opening_plane_face_ids(graph, group, tolerance)
|
||||
confidence = 0.72
|
||||
if coverage >= math.tau * 0.98:
|
||||
confidence += 0.12
|
||||
if len(opening_face_ids) >= 2:
|
||||
confidence += 0.12
|
||||
if len(group) > 1:
|
||||
confidence += 0.04
|
||||
if _has_external_relation(model, (face.face_id for face in group), {"coaxial"}):
|
||||
confidence += EXTERNAL_COAXIAL_CONFIDENCE_BOOST
|
||||
if _has_external_relation(model, (face.face_id for face in group), {"tangent"}):
|
||||
confidence += EXTERNAL_TANGENT_CONFIDENCE_BOOST
|
||||
if len(opening_face_ids) >= 2 and _has_external_relation(
|
||||
model,
|
||||
opening_face_ids,
|
||||
{"coplanar", "parallel"},
|
||||
):
|
||||
confidence += EXTERNAL_OPENING_PLANE_CONFIDENCE_BOOST
|
||||
regions.append(
|
||||
ThroughHoleRegion(
|
||||
face_ids=tuple(sorted(face.face_id for face in group)),
|
||||
solid_id=graph.solid_id,
|
||||
diameter=float(group[0].radius or 0.0) * 2.0,
|
||||
axis_interval=(v_min, v_max),
|
||||
angular_coverage=coverage,
|
||||
opening_face_ids=tuple(sorted(opening_face_ids)),
|
||||
confidence=min(confidence, 0.99),
|
||||
)
|
||||
)
|
||||
return regions
|
||||
|
||||
|
||||
def _external_recognition_relations(model: object, face_ids: Iterable[int]) -> list[RecognitionRelation]:
|
||||
cache = getattr(model, "_asitus_geometric_relation_cache", None)
|
||||
if not isinstance(cache, dict):
|
||||
return []
|
||||
valid_face_ids = {int(item) for item in face_ids}
|
||||
relations: list[RecognitionRelation] = []
|
||||
for pair, items in cache.items():
|
||||
try:
|
||||
face_pair = tuple(sorted(int(item) for item in pair))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if len(face_pair) != 2 or face_pair[0] not in valid_face_ids or face_pair[1] not in valid_face_ids:
|
||||
continue
|
||||
if not isinstance(items, (tuple, list)):
|
||||
continue
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
relation_type = str(item.get("relation_type") or "").strip()
|
||||
if not relation_type:
|
||||
continue
|
||||
relations.append(
|
||||
RecognitionRelation(
|
||||
f"external_{relation_type}",
|
||||
face_pair,
|
||||
_float_or_zero(item.get("residual")),
|
||||
)
|
||||
)
|
||||
return relations
|
||||
|
||||
|
||||
def _has_external_relation(model: object, face_ids: Iterable[int], relation_types: set[str]) -> bool:
|
||||
return _external_relation(model, face_ids, relation_types) is not None
|
||||
|
||||
|
||||
def _external_relation(
|
||||
model: object,
|
||||
face_ids: Iterable[int],
|
||||
relation_types: set[str],
|
||||
) -> dict[str, object] | None:
|
||||
cache = getattr(model, "_asitus_geometric_relation_cache", None)
|
||||
if not isinstance(cache, dict):
|
||||
return None
|
||||
face_id_set = {int(item) for item in face_ids}
|
||||
if len(face_id_set) < 2:
|
||||
return None
|
||||
for pair, items in cache.items():
|
||||
try:
|
||||
face_pair = tuple(sorted(int(item) for item in pair))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if len(face_pair) != 2 or face_pair[0] not in face_id_set or face_pair[1] not in face_id_set:
|
||||
continue
|
||||
if not isinstance(items, (tuple, list)):
|
||||
continue
|
||||
for item in items:
|
||||
if isinstance(item, dict) and str(item.get("relation_type") or "").strip() in relation_types:
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def _float_or_zero(value: object) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _cocylindrical_interval_group(
|
||||
model: object,
|
||||
cylinders: list[RecognitionFace],
|
||||
source: RecognitionFace,
|
||||
tolerance: float,
|
||||
) -> list[RecognitionFace]:
|
||||
pending = [source]
|
||||
visited = {source.face_id}
|
||||
result: list[RecognitionFace] = []
|
||||
while pending:
|
||||
current = pending.pop(0)
|
||||
result.append(current)
|
||||
for candidate in cylinders:
|
||||
if candidate.face_id in visited:
|
||||
continue
|
||||
if candidate.solid_id != source.solid_id:
|
||||
continue
|
||||
if not _recognition_faces_are_cocylindrical(source, candidate, tolerance) and not (
|
||||
_external_cocylindrical_hint(model, source, candidate, tolerance)
|
||||
):
|
||||
continue
|
||||
if not _intervals_overlap_or_touch(current.axis_interval, candidate.axis_interval, tolerance * 50.0):
|
||||
continue
|
||||
visited.add(candidate.face_id)
|
||||
pending.append(candidate)
|
||||
return result
|
||||
|
||||
|
||||
def _external_cocylindrical_hint(
|
||||
model: object,
|
||||
left: RecognitionFace,
|
||||
right: RecognitionFace,
|
||||
tolerance: float,
|
||||
) -> bool:
|
||||
relation = _external_relation(model, (left.face_id, right.face_id), {"coaxial"})
|
||||
if relation is None:
|
||||
return False
|
||||
if left.radius is None or right.radius is None:
|
||||
return False
|
||||
radius_tolerance = max(tolerance, max(left.radius, right.radius) * 1e-6)
|
||||
radius_delta = abs(float(left.radius) - float(right.radius))
|
||||
residual = _float_or_zero(relation.get("residual"))
|
||||
return radius_delta <= radius_tolerance or residual <= radius_tolerance
|
||||
|
||||
|
||||
def _recognition_faces_are_cocylindrical(left: RecognitionFace, right: RecognitionFace, tolerance: float) -> bool:
|
||||
if left.axis_point is None or left.axis_direction is None or right.axis_point is None or right.axis_direction is None:
|
||||
return False
|
||||
if left.radius is None or right.radius is None:
|
||||
return False
|
||||
radius_tolerance = max(tolerance, max(left.radius, right.radius) * 1e-6)
|
||||
if abs(left.radius - right.radius) > radius_tolerance:
|
||||
return False
|
||||
if abs(_direction_dot(left.axis_direction, right.axis_direction)) < 1.0 - 1e-6:
|
||||
return False
|
||||
return _point_axis_distance(left.axis_point, left.axis_direction, right.axis_point) <= max(tolerance, radius_tolerance)
|
||||
|
||||
|
||||
def _opening_plane_face_ids(
|
||||
graph: RecognitionGraph,
|
||||
group: list[RecognitionFace],
|
||||
tolerance: float,
|
||||
) -> set[int]:
|
||||
if not group or group[0].axis_point is None or group[0].axis_direction is None:
|
||||
return set()
|
||||
axis_point = group[0].axis_point
|
||||
axis_direction = group[0].axis_direction
|
||||
intervals = [face.axis_interval for face in group if face.axis_interval is not None]
|
||||
if not intervals:
|
||||
return set()
|
||||
v_min = min(float(item[0]) for item in intervals)
|
||||
v_max = max(float(item[1]) for item in intervals)
|
||||
end_tolerance = max(tolerance * 80.0, abs(v_max - v_min) * 1e-4, 1e-4)
|
||||
side_ids = {face.face_id for face in group}
|
||||
adjacent_ids: set[int] = set()
|
||||
for face in group:
|
||||
adjacent_ids.update(face.adjacent_face_ids)
|
||||
openings: set[int] = set()
|
||||
by_id = {face.face_id: face for face in graph.faces}
|
||||
for adjacent_id in adjacent_ids - side_ids:
|
||||
adjacent = by_id.get(adjacent_id)
|
||||
if adjacent is None or adjacent.surface_type != "plane" or adjacent.axis_direction is None:
|
||||
continue
|
||||
if abs(_direction_dot(adjacent.axis_direction, axis_direction)) < 1.0 - ANGULAR_TOLERANCE:
|
||||
continue
|
||||
try:
|
||||
parameter = _axis_parameter(axis_point, axis_direction, _gp_point(adjacent.centroid))
|
||||
except Exception:
|
||||
continue
|
||||
if abs(parameter - v_min) <= end_tolerance or abs(parameter - v_max) <= end_tolerance:
|
||||
openings.add(adjacent_id)
|
||||
return openings
|
||||
|
||||
|
||||
def _plane_relation(left: RecognitionFace, right: RecognitionFace, tolerance: float) -> RecognitionRelation | None:
|
||||
if left.axis_direction is None or right.axis_direction is None:
|
||||
return None
|
||||
dot = abs(_direction_dot(left.axis_direction, right.axis_direction))
|
||||
if dot >= 1.0 - ANGULAR_TOLERANCE:
|
||||
residual = abs(_plane_offset(left, right))
|
||||
if residual <= tolerance:
|
||||
return RecognitionRelation("coplanar", (left.face_id, right.face_id), residual)
|
||||
return RecognitionRelation("parallel", (left.face_id, right.face_id), residual)
|
||||
if dot <= ANGULAR_TOLERANCE:
|
||||
return RecognitionRelation("perpendicular", (left.face_id, right.face_id), dot)
|
||||
return None
|
||||
|
||||
|
||||
def _cylinder_relation(left: RecognitionFace, right: RecognitionFace, tolerance: float) -> RecognitionRelation | None:
|
||||
if not _recognition_faces_are_cocylindrical(left, right, tolerance):
|
||||
if left.axis_point is not None and left.axis_direction is not None and right.axis_direction is not None:
|
||||
if abs(_direction_dot(left.axis_direction, right.axis_direction)) >= 1.0 - ANGULAR_TOLERANCE:
|
||||
return RecognitionRelation("parallel_axis", (left.face_id, right.face_id), 0.0)
|
||||
return None
|
||||
residual = 0.0
|
||||
if left.axis_point is not None and left.axis_direction is not None and right.axis_point is not None:
|
||||
residual = _point_axis_distance(left.axis_point, left.axis_direction, right.axis_point)
|
||||
return RecognitionRelation("coaxial", (left.face_id, right.face_id), residual)
|
||||
|
||||
|
||||
def _plane_offset(left: RecognitionFace, right: RecognitionFace) -> float:
|
||||
if left.axis_point is None or left.axis_direction is None or right.axis_point is None:
|
||||
return math.inf
|
||||
return float(_axis_parameter(left.axis_point, left.axis_direction, right.axis_point))
|
||||
|
||||
|
||||
def _surface_metrics(shape: TopoDS_Shape) -> tuple[float, tuple[float, float, float]]:
|
||||
props = GProp_GProps()
|
||||
try:
|
||||
brepgprop.SurfaceProperties(shape, props)
|
||||
center = props.CentreOfMass()
|
||||
return float(props.Mass()), (float(center.X()), float(center.Y()), float(center.Z()))
|
||||
except Exception:
|
||||
center = _surface_center(shape)
|
||||
return 0.0, (float(center.X()), float(center.Y()), float(center.Z()))
|
||||
|
||||
|
||||
def _face_boundary_edge_ids(model: object, face_id: int) -> list[int]:
|
||||
if hasattr(model, "_face_boundary_edge_ids"):
|
||||
return list(model._face_boundary_edge_ids(face_id)) # noqa: SLF001
|
||||
edges = TopTools_IndexedMapOfShape()
|
||||
topexp.MapShapes(getattr(model, "faces")[face_id], TopAbs_EDGE, edges)
|
||||
return list(range(edges.Size()))
|
||||
|
||||
|
||||
def _adjacent_face_ids(model: object, edge_ids: Iterable[int], face_id: int) -> set[int]:
|
||||
adjacent: set[int] = set()
|
||||
if hasattr(model, "_adjacent_face_ids_for_edges"):
|
||||
adjacent.update(model._adjacent_face_ids_for_edges(edge_ids, face_id)) # noqa: SLF001
|
||||
else:
|
||||
edge_face_ids = getattr(model, "_edge_face_ids_cache", {})
|
||||
for edge_id in edge_ids:
|
||||
adjacent.update(int(item) for item in edge_face_ids.get(int(edge_id), ()) if int(item) != int(face_id))
|
||||
return adjacent
|
||||
|
||||
|
||||
def _recognition_tolerance(model: object) -> float:
|
||||
try:
|
||||
diagonal = _shape_diagonal(getattr(model, "shape"))
|
||||
except Exception:
|
||||
diagonal = 1.0
|
||||
return min(max(float(diagonal) * 1e-7, 1e-6), 1e-3)
|
||||
|
||||
|
||||
def _solid_ids(model: object, solid_id: int | None) -> tuple[int, ...]:
|
||||
if solid_id is not None:
|
||||
return (int(solid_id),)
|
||||
face_solid_ids = sorted({int(item) for item in getattr(model, "face_solid_ids", ()) if int(item) >= 0})
|
||||
if face_solid_ids:
|
||||
return tuple(face_solid_ids)
|
||||
return tuple(range(len(getattr(model, "solids", ()) or ())))
|
||||
|
||||
|
||||
def _intervals_overlap_or_touch(
|
||||
left: tuple[float, float] | None,
|
||||
right: tuple[float, float] | None,
|
||||
tolerance: float,
|
||||
) -> bool:
|
||||
if left is None or right is None:
|
||||
return True
|
||||
left_min, left_max = min(left), max(left)
|
||||
right_min, right_max = min(right), max(right)
|
||||
return max(left_min, right_min) <= min(left_max, right_max) + max(tolerance, 0.0)
|
||||
|
||||
|
||||
def _dedupe_regions(regions: Iterable[ThroughHoleRegion]) -> list[ThroughHoleRegion]:
|
||||
result: list[ThroughHoleRegion] = []
|
||||
seen: set[tuple[int, ...]] = set()
|
||||
for region in sorted(regions, key=lambda item: (item.solid_id, item.face_ids)):
|
||||
if region.face_ids in seen:
|
||||
continue
|
||||
seen.add(region.face_ids)
|
||||
result.append(region)
|
||||
return result
|
||||
|
||||
|
||||
def _gp_point(values: tuple[float, float, float]):
|
||||
from OCC.Core.gp import gp_Pnt
|
||||
|
||||
return gp_Pnt(float(values[0]), float(values[1]), float(values[2]))
|
||||
Reference in New Issue
Block a user