540 lines
20 KiB
Python
540 lines
20 KiB
Python
|
|
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]))
|