Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Scripts/repro/539-nearest-point-on-curve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ was left out of #539 because fixing it needs a decision about its `solutionCount
`580-repair-options.mm` measures that decision rather than leaving it open. Over 189 edge/point
combinations:

> **Shipped.** #580 took the recommendation below: `Shape.pointEdgeExtrema` now computes its
> distance, parameter and point with `occtNearestPointOnCurveRange` and keeps `BRepExtrema_ExtPC`
> solely for `solutionCount`, which stops doubling as a success flag. Fixing it also turned up a
> second, unrelated defect the probes here do not cover: `edgeIndex` walked a bare `TopExp_Explorer`
> (one entry per *occurrence* — a box's 12 edges are 24), so from index 9 it named a different edge
> than `Shape.edges()`. It now uses `occtEdgeAt`, #541's enumeration.

| candidate set | correct distances |
|---|---|
| all extrema, `nil` when `IsDone()` is false (today) | 101 correct, 34 wrong, 54 `nil` |
Expand Down
43 changes: 31 additions & 12 deletions Sources/OCCTBridge/include/OCCTBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@
// BRepExtrema_ExtCC → OCCTBRepExtremaExtCC
// BRepExtrema_ExtCF → OCCTBRepExtremaExtCF
// BRepExtrema_ExtFF → OCCTBRepExtremaExtFF
// BRepExtrema_ExtPC → OCCTBRepExtremaExtPC
// BRepExtrema_ExtPC → OCCTBRepExtremaExtPC (for its extrema COUNT only since
// #580; the distance/parameter/point come from
// occtNearestPointOnCurveRange, because extrema exclude the
// edge's ends and the one in range can be a maximum)
// BRepExtrema_ExtPF → OCCTBRepExtremaExtPF
// BRepExtrema_Poly → OCCTShapePolyhedralDistance
//
Expand Down Expand Up @@ -352,8 +355,9 @@
// (NOT OCCTSurfacePlateThrough; see GeomPlate)
// GeomAPI_ProjectPointOnCurve → OCCTCurve3DNearestParameter, OCCTExtremaLocateOnCurve,
// OCCTExtremaPointCurve, OCCTProjOnCurve*,
// OCCTEdgeProjectPoint, OCCTCurve3DProjectPoint
// (the last two only as one of the three candidate sources
// OCCTEdgeProjectPoint, OCCTCurve3DProjectPoint,
// OCCTBRepExtremaExtPC
// (the last three only as one of the three candidate sources
// occtNearestPointOnCurveRange takes a minimum over; see
// ShapeAnalysis_Curve for the other, and #539 for why
// neither class answers correctly on its own)
Expand Down Expand Up @@ -482,8 +486,9 @@
// --- ShapeAnalysis ---
// ShapeAnalysis_Curve → OCCTCurve3DProjectPoint, OCCTCurve3DValidateRange,
// OCCTCurve3DGetSamplePoints3D, OCCTCurve3DIsClosedWithPreci,
// OCCTCurve3DIsPeriodicSA, OCCTEdgeProjectPoint
// (the two projection entry points reach ::Project through
// OCCTCurve3DIsPeriodicSA, OCCTEdgeProjectPoint,
// OCCTBRepExtremaExtPC
// (the three projection entry points reach ::Project through
// occtNearestPointOnCurveRange, which does not trust its
// answer alone; see GeomAPI_ProjectPointOnCurve)
// ShapeAnalysis_FreeBounds → OCCTShapeFreeBounds, OCCTShapeFreeBoundsClosedCount,
Expand Down Expand Up @@ -6508,17 +6513,31 @@ OCCTShapeRef _Nullable OCCTShapeUpgradeDivideContinuity(OCCTShapeRef shape, int3

/// Point-edge extrema result
typedef struct {
double distance; // Minimum distance
double parameter; // Parameter on edge at closest point
double ptx, pty, ptz; // Closest point on edge
int32_t solutionCount; // Number of extrema found
double distance; // Minimum distance over the edge, ends included (#580)
double parameter; // Parameter on edge at the nearest point
double ptx, pty, ptz; // Nearest point on edge
int32_t solutionCount; // Number of perpendicular feet BRepExtrema_ExtPC found; 0 is a
// reportable state, not a failure -- see below
bool isValid; // false only when there is no such edge, or it has no 3D curve
} OCCTPointEdgeExtremaResult;

/// Compute distance from a point to an edge.
/// Compute the minimum distance from a point to an edge of a shape, over the whole edge.
///
/// The distance/parameter/point are the minimum over the edge's own range including its two ends,
/// via occtNearestPointOnCurveRange -- the same helper behind OCCTEdgeProjectPoint, so the two
/// cannot disagree about the same point and the same edge. This used to report the minimum over
/// BRepExtrema_ExtPC's extrema instead, which excludes the ends and can consist of a single
/// MAXIMUM: a point below a half circle of radius 5 read as 11 away when it is 7.81 away, and a
/// point past the end of a trimmed segment had no answer at all (#580).
///
/// `solutionCount` keeps both its meaning and its source: how many extrema BRepExtrema_ExtPC found,
/// which is how many perpendicular feet the point has on the edge. It is no longer a success flag.
/// Zero means the nearest point is one of the ends, which is worth reporting rather than refusing.
///
/// @param px,py,pz Point coordinates
/// @param shape Shape containing the edge
/// @param edgeIndex Edge index (0-based)
/// @return Extrema result (minimum distance solution)
/// @param edgeIndex Edge index (0-based, in the enumeration Shape.edges() reads)
/// @return Nearest-point result; check isValid, not solutionCount
OCCTPointEdgeExtremaResult OCCTBRepExtremaExtPC(double px, double py, double pz,
OCCTShapeRef shape, int32_t edgeIndex);

Expand Down
73 changes: 49 additions & 24 deletions Sources/OCCTBridge/src/OCCTBridge_Topology.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1207,39 +1207,64 @@ OCCTFaceFaceExtremaResult OCCTBRepExtremaExtFF(OCCTShapeRef shape1, int32_t face
}

// MARK: - BRepExtrema Ext PC/CF (v0.49)

// #580: the nearest point on the edge, not the nearest of BRepExtrema_ExtPC's extrema.
//
// BRepExtrema_ExtPC searches for perpendicular feet, so it excludes the edge's own two ends and
// makes no distinction between a minimum and a maximum. Reporting the smallest of what it found is
// therefore not the smallest distance to the edge, and measurably so: over 189 edge/point
// combinations (Scripts/repro/539-nearest-point-on-curve/580-repair-options.mm) it answered 101
// correctly, 34 with a distance that was too large -- a point below a half circle of radius 5 read
// as 11 rather than 7.81, because the sole extremum in range is the far side of the arc -- and
// refused 54 outright, IsDone() being false whenever no foot exists at all.
//
// The measured trap: filtering the extrema to the IsMin ones scores 101, exactly what it scored
// before. The cases that filter drops are precisely the ones it leaves with no candidate.
//
// occtNearestPointOnCurveRange (#539) is what answers this, so both this entry point and
// OCCTEdgeProjectPoint reach one implementation and cannot disagree about the same edge and the
// same point. Adding BRepExtrema_ExtPC's own ends via TrimmedSquareDistances would be the smaller
// diff but tops out at 188/189: on a BSpline queried from (2, 0, 0) Extrema_ExtPC does not
// converge, leaving the nearer end to answer 2 against a truth of 1.996434, where the helper's
// GeomAPI_ProjectPointOnCurve finds the interior minimum.
OCCTPointEdgeExtremaResult OCCTBRepExtremaExtPC(double px, double py, double pz,
OCCTShapeRef shape, int32_t edgeIndex) {
OCCTPointEdgeExtremaResult result = {};
if (!shape) return result;
try {
TopoDS_Edge edge;
int idx = 0;
for (TopExp_Explorer exp(shape->shape, TopAbs_EDGE); exp.More(); exp.Next()) {
if (idx == edgeIndex) { edge = TopoDS::Edge(exp.Current()); break; }
idx++;
}
// #541's enumeration, which is the one Shape.edges() and Shape.edge(at:) read. This used to
// walk its own bare explorer, which counts one entry per *occurrence*: a box's 12 edges are
// 24 occurrences, since each belongs to two faces, and measured on the pinned kernel the two
// orders diverge from index 9 onwards -- edgeIndex 9 was the edge through (10, 0, 5) here
// and the edge through (5, 0, 10) to every other entry point. A caller holding an index from
// edges() measured to an edge it had not selected, on the most ordinary shape there is.
TopoDS_Edge edge = occtEdgeAt(shape->shape, edgeIndex);
if (edge.IsNull()) return result;

TopoDS_Vertex vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(px, py, pz));
BRepExtrema_ExtPC ext(vertex, edge);
if (!ext.IsDone()) return result;
Standard_Real first, last;
Handle(Geom_Curve) curve = BRep_Tool::Curve(edge, first, last);
if (curve.IsNull()) return result;

result.solutionCount = ext.NbExt();
if (result.solutionCount >= 1) {
// Find minimum distance
double minDist2 = ext.SquareDistance(1);
int minIdx = 1;
for (int i = 2; i <= ext.NbExt(); i++) {
if (ext.SquareDistance(i) < minDist2) {
minDist2 = ext.SquareDistance(i);
minIdx = i;
}
}
result.distance = sqrt(minDist2);
result.parameter = ext.Parameter(minIdx);
gp_Pnt pt = ext.Point(minIdx);
result.ptx = pt.X(); result.pty = pt.Y(); result.ptz = pt.Z();
gp_Pnt nearest;
if (!occtNearestPointOnCurveRange(curve, gp_Pnt(px, py, pz), first, last,
Precision::Confusion(),
&nearest, &result.parameter, &result.distance)) {
return result;
}
result.ptx = nearest.X(); result.pty = nearest.Y(); result.ptz = nearest.Z();

// Still BRepExtrema_ExtPC's own count, reported for its own sake: how many perpendicular
// feet the point has on this edge. Zero now travels to the caller instead of erasing the
// answer, and a search that cannot finish leaves it zero rather than failing the call.
try {
TopoDS_Vertex vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(px, py, pz));
BRepExtrema_ExtPC ext(vertex, edge);
if (ext.IsDone()) result.solutionCount = ext.NbExt();
} catch (...) {
// A count we could not take is zero feet reported, not a failed distance.
}

result.isValid = true;
return result;
} catch (...) {
return result;
Expand Down
47 changes: 38 additions & 9 deletions Sources/OCCTSwift/Shape.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8011,27 +8011,56 @@ extension Shape {

/// Result of point-edge distance extrema computation
public struct PointEdgeExtrema: Sendable {
/// Minimum distance from the point to the edge
/// Minimum distance from the point to the edge, over the whole edge including its ends
public let distance: Double
/// Parameter on edge at closest point
/// Parameter on the edge at the nearest point
public let parameter: Double
/// Closest point on edge
/// The nearest point on the edge
public let pointOnEdge: SIMD3<Double>
/// Number of extrema solutions found
/// How many perpendicular feet the point has on this edge (`BRepExtrema_ExtPC`'s extrema).
///
/// Zero is an ordinary, informative answer rather than a failure: it means the nearest
/// point is one of the edge's two ends. A non-zero count does *not* mean the nearest point
/// is one of those feet — an extremum can be a maximum — so read `distance`, `parameter`
/// and `pointOnEdge` for the answer and this only for the count.
public let solutionCount: Int
}

/// Compute minimum distance from a point to an edge of this shape.
/// Compute the minimum distance from a point to an edge of this shape.
///
/// The answer is the nearest point over the whole edge, its two ends included, and matches
/// ``Edge/project(point:)`` on the same edge and the same point — both take a minimum over
/// `ShapeAnalysis_Curve`, `GeomAPI_ProjectPointOnCurve` and the edge's ends.
///
/// ```swift
/// let arc = Shape.fromWire(Wire.arc(
/// center: SIMD3(0, 0, 0), radius: 5, startAngle: 0, endAngle: .pi)!)!
///
/// Uses BRepExtrema_ExtPC to find the closest point on the specified edge.
/// // Below the arc, the nearest point is an end — the only extremum is the far side of it.
/// if let hit = arc.pointEdgeExtrema(point: SIMD3(0, -6, 0), edgeIndex: 0) {
/// print(hit.distance) // 7.81, to the end at (5, 0, 0). Was 11, the far side.
/// print(hit.solutionCount) // 1 — and that one extremum is a maximum
/// }
///
/// let segment = Shape.fromWire(Wire.line(from: SIMD3(3, 0, 0), to: SIMD3(8, 0, 0))!)!
/// if let hit = segment.pointEdgeExtrema(point: SIMD3(100, 0, 0), edgeIndex: 0) {
/// print(hit.distance) // 92. Was nil: no extremum exists past the end.
/// print(hit.solutionCount) // 0 — no perpendicular foot, not a failure
/// }
/// ```
///
/// Before #580 this reported the smallest of `BRepExtrema_ExtPC`'s extrema, which excludes the
/// edge's ends and can be a single *maximum*: the arc above answered 11 (the far side), and a
/// point beyond the end of a trimmed segment answered `nil`.
///
/// - Parameters:
/// - point: 3D point
/// - edgeIndex: 0-based edge index
/// - Returns: Extrema result, or nil if computation fails
/// - edgeIndex: 0-based edge index, in the enumeration ``edges()`` reads
/// - Returns: The nearest-point result, or nil if there is no such edge index or that edge has
/// no 3D curve.
public func pointEdgeExtrema(point: SIMD3<Double>, edgeIndex: Int) -> PointEdgeExtrema? {
let result = OCCTBRepExtremaExtPC(point.x, point.y, point.z, handle, Int32(edgeIndex))
guard result.solutionCount > 0 else { return nil }
guard result.isValid else { return nil }
return PointEdgeExtrema(
distance: result.distance,
parameter: result.parameter,
Expand Down
Loading
Loading