diff --git a/crates/solidcore-ffi/src/units/topology_vertex.rs b/crates/solidcore-ffi/src/units/topology_vertex.rs index 14d0465ef..39f282659 100644 --- a/crates/solidcore-ffi/src/units/topology_vertex.rs +++ b/crates/solidcore-ffi/src/units/topology_vertex.rs @@ -24,10 +24,10 @@ //! (`SmBSplineCurve`). Indices refer to the export's traversal order (`-1` selects null //! where upstream takes a pointer). Every op's output starts with `0`, or with //! `-1 status` when the B-rep could not be built (nothing else follows). `-2` in an -//! output is "upstream would dereference null or read an uninitialised value" (the shim -//! did not call it). A list is `count ids...`; "gap states" are the vertex/edge and -//! vertex/face cache states of every vertex use, in order; a gap is `kind vu length` -//! (`kind` 0 vertex/edge, 1 vertex/face; `-1` alone for none). +//! output is "upstream would dereference null" (the shim did not call it). A list is +//! `count ids...`; "gap states" are the vertex/edge and vertex/face cache states of +//! every vertex use, in order; a gap is `kind vu length` (`kind` 0 vertex/edge, 1 +//! vertex/face; `-1` alone for none). //! //! | [`Op`] | parameters after the block | outputs | //! |---------------------|----------------------------|---------| diff --git a/crates/solidcore/src/query/loop.rs b/crates/solidcore/src/query/loop.rs index 715394a6e..b68b492b1 100644 --- a/crates/solidcore/src/query/loop.rs +++ b/crates/solidcore/src/query/loop.rs @@ -33,8 +33,9 @@ //! reached when the point lies on a side of the surface's natural domain and an edge //! use end of the loop does too. //! - **Ignored statuses.** Upstream ignores the status of `EvaluatePoint` / -//! `Evaluate` of the surface and the trim curves and then reads the (uninitialised) -//! outputs; the port returns their error instead (no valid input reaches it). +//! `Evaluate` of the surface and the trim curves and then reads the outputs they did +//! not write (`SM_UNDEF_DOUBLE` defaults, or the previous step's derivatives); the +//! port returns their error instead (no valid input reaches it). //! `DropPoint` and `LocalPointSolve` failures are "no answer" (both reset their //! success flag first upstream). [`SolidError::Unported`] from any callee is //! returned. diff --git a/crates/solidcore/src/topology/vertex.rs b/crates/solidcore/src/topology/vertex.rs index 9a165f171..ff80987df 100644 --- a/crates/solidcore/src/topology/vertex.rs +++ b/crates/solidcore/src/topology/vertex.rs @@ -46,11 +46,14 @@ //! use without edge, a face use without face, a shell without region: a malformed //! B-rep) the port returns [`SolidError::InvalidInput`]; [`faceuses`] keeps its null //! entries (the edge vertex uses of a wire edge have no face use). -//! - **Uninitialised tangents.** `AreEdgesTangent` and `IsTopologicalVertex` ignore a -//! failed `GetEndTangent` (`SE`) and then read the tangent left uninitialised (an -//! `SmVector3d` has no constructor); the port returns [`SolidError::InvalidInput`] -//! there. `IsTopologicalVertex` returns the failed status of `GetEndTangent` at an -//! edge's start (`SER`) as its `SmBoolean`, which is true; ported as such. +//! - **Unset tangents.** `AreEdgesTangent` and `IsTopologicalVertex` ignore a failed +//! `GetEndTangent` (`SE`) and then read the tangent it did not write. That is the +//! `SmVector3d` default, `SM_UNDEF_DOUBLE` in every component ([`Vec3::UNDEF`]), or in +//! `IsTopologicalVertex` the line vector `IsLine` wrote for that edge. `AngleBetween` +//! then sees a zero-length vector and reports `0`, so both answer true; ported as +//! such (WR-256; UB-106 assumed the tangent uninitialised). `IsTopologicalVertex` +//! returns the failed status of `GetEndTangent` at an edge's start (`SER`) as its +//! `SmBoolean`, which is true; ported as such. //! - **Curve identity.** Each edge owns its curve, so `pCurve1 != pCurve2` in //! `IsTopologicalVertex` is false only when neither edge has a curve (upstream two //! edges may share one curve object; the difftest rejects such B-reps). @@ -777,11 +780,6 @@ fn end_tangent_ignoring_status( } } -/// The uninitialised tangent upstream reads after an ignored failed `GetEndTangent`. -fn uninitialised_tangent() -> SolidError { - null("tangent left uninitialised by a failed GetEndTangent") -} - /// `AreEdgesTangent`: whether edges `e1` and `e2`, both ending at the vertex, leave it /// in parallel directions (their end tangents, pointing through the vertex, within /// `angle_tol_deg` degrees). @@ -789,8 +787,8 @@ fn uninitialised_tangent() -> SolidError { /// # Errors /// /// [`SolidError::NotFound`] for a dangling id; those of [`edge_start_vertex`]; -/// [`SolidError::InvalidInput`] for an edge without curve and for a tangent whose -/// evaluation failed (upstream reads it uninitialised). +/// [`SolidError::InvalidInput`] for an edge without curve. A tangent whose evaluation +/// fails is the `SmVector3d` default ([`Vec3::UNDEF`]), as upstream. pub fn are_edges_tangent( k: &Kernel, b: &Brep, @@ -799,18 +797,19 @@ pub fn are_edges_tangent( e2: EdgeId, angle_tol_deg: f64, ) -> Result { + // a failed GetEndTangent leaves the default SmVector3d (SM_UNDEF_DOUBLE) let tan1 = if edge_start_vertex(b, e1)? == Some(v) { - end_tangent_ignoring_status(k, b, e1, true)?.ok_or_else(uninitialised_tangent)? + end_tangent_ignoring_status(k, b, e1, true)?.unwrap_or(Vec3::UNDEF) } else { - -end_tangent_ignoring_status(k, b, e1, false)?.ok_or_else(uninitialised_tangent)? + -end_tangent_ignoring_status(k, b, e1, false)?.unwrap_or(Vec3::UNDEF) }; // Note that sTan1 is pointing to the inside of the curve and sTan2 will be pointing // to the outside of the curve. That should make them parallel. let tan2 = if edge_start_vertex(b, e2)? == Some(v) { - -end_tangent_ignoring_status(k, b, e2, true)?.ok_or_else(uninitialised_tangent)? + -end_tangent_ignoring_status(k, b, e2, true)?.unwrap_or(Vec3::UNDEF) } else { - end_tangent_ignoring_status(k, b, e2, false)?.ok_or_else(uninitialised_tangent)? + end_tangent_ignoring_status(k, b, e2, false)?.unwrap_or(Vec3::UNDEF) }; // AngleBetween sets the angle to 0 on failure (status ignored) @@ -847,8 +846,9 @@ pub fn is_on_pole(k: &Kernel, b: &mut Brep, v: VertexId) -> Result { /// # Errors /// /// Those of [`edges`], [`is_on_pole`], `SmCurve::IsLine` and [`edge_start_vertex`]; -/// [`SolidError::InvalidInput`] for one edge without curve whose tangent is needed and -/// for a tangent read uninitialised upstream. +/// [`SolidError::InvalidInput`] for one edge without curve whose tangent is needed. A +/// tangent neither `IsLine` nor `GetEndTangent` wrote is the `SmVector3d` default +/// ([`Vec3::UNDEF`]), as upstream. pub fn is_topological_vertex(k: &Kernel, b: &mut Brep, v: VertexId) -> Result { let sedges = edges(b, v, None)?; @@ -931,8 +931,9 @@ pub fn is_topological_vertex(k: &Kernel, b: &mut Brep, v: VertexId) -> ResultGetCurve() == nullptr) - return false; - SmVector3d t; - return e->GetEndTangent(e->GetStartVertex() == v ? TRUE : FALSE, t) == SM_SUCCESS; + return ok_start(e) && e->GetCurve() != nullptr; } // IsTopologicalVertex, following its control flow up to the tangent reads (IsOnPole is // evaluated here as upstream does; it only fills caches the real call fills as well). +// Only a null curve whose tangent is read is refused: a failed GetEndTangent leaves the +// tangent IsLine wrote or the default SmVector3d (WR-256). bool ok_topological(SmVertex* v) { if (!ok_edges(v)) @@ -1126,22 +1126,15 @@ bool ok_topological(SmVertex* v) return true; if (c[0] == c[1]) return true; - bool uninit = false; for (int i = 0; i < 2; i++) { if (c[i] == nullptr) return false; SmVector3d t; - SmStatus s = e[i]->GetEndTangent(at_start[i] ? TRUE : FALSE, t); - if (s != SM_SUCCESS) - { - if (at_start[i]) - return !uninit; // SER returns before the angle is read - if (!line[i]) - uninit = true; - } + if (at_start[i] && e[i]->GetEndTangent(TRUE, t) != SM_SUCCESS) + return true; // SER returns before the other edge's tangent is read } - return !uninit; + return true; } // The cached gap states of every vertex use (vertex/edge, vertex/face). @@ -1383,7 +1376,7 @@ void run_brep_op(int32_t op, Rd& rd, const char* path, std::vector& o) SmEdge* e1 = g.edges[(size_t)rd.idx()]; SmEdge* e2 = g.edges[(size_t)rd.idx()]; double tol = rd.num(); - if (!ok_tangent(v, e1) || !ok_tangent(v, e2)) + if (!ok_tangent(e1) || !ok_tangent(e2)) { o.push_back(ST_NULL_DEREF); break; diff --git a/plan/findings.md b/plan/findings.md index 36c2bb2e8..2d4c83685 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18194,6 +18194,46 @@ behaviour is unchanged. - **Result.** `blend.fillet_intersector` 1292/1292 pass, known-mismatch entry removed. No tolerance changed and no floor added. +## #521 audit: values read after a failed call, per site, at `c9c5979` (2026-10-24) + +Issue #521 (filed from the fork) lists sites where SMLib reads a value a failed call never +set. At `c9c5979` none of them reads uninitialised memory: `SmVector3d`/`SmPoint3d`/ +`SmPoint2d` default to `SM_UNDEF_DOUBLE` (`SmVector3d.h:56-58`), `SmExtent1d` to +`[SM_BIG_DOUBLE, -SM_BIG_DOUBLE]`, and `AngleBetween` writes `0` before it can fail. What each +site reads, and the port: + +- **`SmVertex::AreEdgesTangent` / `IsTopologicalVertex`:** the UNDEF tangent (or `IsLine`'s + line vector), angle `0`, answer `TRUE` (WR-256, correcting UB-106). The port returned + `InvalidInput` and the `topology.vertex` shim refused the input; both now follow upstream. + No harness input makes `GetEndTangent` fail (it builds line and B-spline edges only; WR-256 + *Trigger*), so no case changes value: `topology.vertex` 5578/5578 with the 70 new + `end_tangents.json` cases. A composite-curve edge, whose evaluation always fails, is + covered by the unit test `failed_end_tangent_reads_the_default_vector`. +- **`SmLoop::GetUVCornerBoundaries`:** the issue's "keeps the previous edgeuse's curve" does + not hold here: `GetOrCreateUVTrimCurve` sets its out-pointer to the cached curve + (`SmEdgeuse.cpp:4275`) before building, so a failed build leaves NULL and the drop-point + branch runs. A failed `EvaluatePoint`/`DropPoint` there leaves the previous edge use's UV end + (the points are declared outside the loop). `topology::loopuse::uv_corner_boundaries` + is faithful on both. +- **`SmEdge::ReverseOrientation`:** a failed `ReverseParameterization` leaves the default + interval, and the edge uses are still flipped. Faithful (`topology::edge`, Deviations). +- **`SmLoop::ContainsUVPoint`:** the four ignored evaluations (`SmLoop.cpp:546`, `743`, `815`, + `1011`) leave UNDEF points or, at `1011`, the previous step's derivatives; they fail only for + a failing surface evaluation, a NaN parameter or a degenerate knot span. The port returns the + error there (`query::loop`, Deviations); the other ignored calls (`DropPoint`, + `LocalPointSolve`, `smgu_LinePointDistance`, the zero-length-edge loop state) are faithful. +- **Edge-use loop-gap / dog-leg checks:** `GetStartPointUVLoopGap` reads UNDEF points or the + drop guess after failed calls; `topology::edgeuse::p1` is faithful. `GetEndPointUVLoopGap` + and `HasDogLegNearPole` are unreachable (`harness/callgraph/unused_functions.txt`) and not + ported. +- **`sm_CalcDistAndDev`:** a failed `Evaluate1stDerivatives` leaves UNDEF point and tangents + (`Vec3::UNDEF` in `calc_dist_and_dev`, faithful). Open: the port propagates an error from + `evaluate_normal` (`edgeuse/p1.rs`) where upstream ignores it; that differs only for a + zero-radius sphere (upstream's failed `Unitize` has stored `(0, 0, 1)`) or a `SrfInVolume` + whose evaluation fails. +- **`SmFaceProps::SetProps_Stage3`:** no read of an unset value. A failed Stage 3 leaves + `m_ePropStage` at `3a`, so a later call returns success with the reset values; the port + copies that. ## topology.face_p1 UB-137 crash-trap cases are rejected, not deferred Follows the fork crash trap of PRs #467/#696 (entry "UB-137 crash guard replaced by a fork diff --git a/plan/units/topology.vertex.toml b/plan/units/topology.vertex.toml index b20314b6c..c2fc93016 100644 --- a/plan/units/topology.vertex.toml +++ b/plan/units/topology.vertex.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "green" claimed_by = "" claimed_at = "" -notes = "PARTIAL: difftest 5508 cases (5492 fixture: 1408 review (rational degree-1 / quadratic / arc edges meeting lines and arcs at a vertex, B-spline-line and reversed SmLine turns at 0.49..180 degrees, 1e-12..1e12 scales, 2- and 3-edge closed chains, tangent / gap / SetTolerance limits); 2160 on 15 upstream .smb B-reps read by SmBrep::ReadFromFile, some with RebuildUVTrimCurves; 1665 constructed: prog_test my_test_topology's wire network, my_test_edge_geometry_repairs' wire line (SetTolerance/SetPoint), chains of wire edges from real SmLines (re-parameterised), degree-1 B-spline lines and B-spline curves turning by 0 to 180 degrees around the 0.5 degree continuity limit (IsTopologicalVertex / AreEdgesTangent), closed squares, a closed seam curve, wire edges and plane / B-spline / pinched faces (full and sub-domain); 259 adversarial: 1e-6..1e6 scales, zero-length and tiny segments, a point B-spline, tolerance limits incl. SM_USE_DEFAULT and negatives, gap tolerance limits, every notify operation, null selectors, far edges; 16 generated), 4651 pass, 857 deferred (topology.edgeuse_p3 399, surface.surface_p3 382, topology.face_p3 76; SmEdge::SetTolerance wired by topology.edge PR #103; after merging main: SmFace::GetUpwardFaceuse wired to topology::face::p2::upward_faceuse, the shim exports each face's upward face use, and heal.gap gaps are real), max_rel_err 0; --perturb SENSITIVE. Runner imports the shim's exported graph (with each vertex's vertex use list in upstream order, cached gap values, edge curve identity) into topology::Brep. topology::Vertex gains vertexuses (m_pList) and poly_vertex; SM_TOLERANT_WATCH is off in the reference build (no m_bExact: IsExact is TRUE, SetExact no-op). Upstream bugs WR-91 (GetVertexuseOfFace ignores its face), WR-92 (IsTopologicalVertex SER/cusps), UB-105 (GetMaxUpDimGap3d NULL array), UB-106 (uninitialised tangents). Wired UNPORTED(topology.vertex) in geom::tools (geom.tools: 60 deferred now pass); generic-handle stubs in intersect::point_class, geom::solution_array, geom::object, geom::tol, surface::trim_srf_cache, topology::proto_topology::p1::proto and query::poly stay (opaque handles, plan/findings.md); the co-ported vertex use scans in topology::vertexuse now call topology::vertex. Plan notes: 38 fns; largest: SmVertex::IsTopologicalVertex, SmVertex::Notify, SmVertex::IsConnectedTo, SmVertex::GetMaxUpDimGap3d; cycle back-edges (call into later units; stub or co-port): topology.edge(12), heal.gap(4), topology.loopuse(2), blend.fillet_geom_p1(1), topology.face_p3(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmVertex::m_sZoneTol3d -> topology::Vertex::tolerance; CO-PORTED in topology.vertexuse (PR #88): SmVertex::GetVertexuses / GetVertexusesOfEdge / GetVertexusesOfFace (replaced by topology::vertex in PR #96); CO-PORTED in topology.face_p2 (PR #91): SmVertex::GetEdges without face filter (uses found by scanning the B-rep's vertex uses) -> private topology::face::p2::vertex_edges Re-run (PR #52, after merging surface_p4/#49 and surface_p5/#50): 5508 cases, 4984 pass, 524 deferred (topology.edgeuse_p3 448, topology.face_p3 76). [topology.edgeuse_p3 wired (PR #117): 5508 cases, 4896 pass, 0 failed, 612 deferred (surface.cone_p1 104, surface.sphere 104, surface.surf_of_extrusion_p2 104, topology.brep_p10 224, topology.face_p3 76)] [PR #155: the inexact-import deferrals are labelled topology.brep_p10 SmBrep::MakeTopologyFromData, not topology.brep_p7 SmBrep::ReadFromFile] [topology.face_p3 wired (PR #139, SmFace::SetTolerance in RefreshTolerance): 5508 cases, 4972 pass, 0 failed, 536 deferred (surface.cone_p1 104, surface.sphere 104, surface.surf_of_extrusion_p2 104, topology.brep_p10 224)] [2026-10-23 GREEN (PR #605): the shim exports each analytic face surface exactly (sc_analytic_export.h record head) and the runner reads it with util::read_exact_surface, so cone/sphere/revolution/extrusion faces are real SurfaceKinds and the solving ops compare values: PASS topology.vertex cases=5508 passed=5508 failed=0 deferred=0 max_rel_err=0e0 with_values=5506 no_values=2 vacuous=0]" +notes = "PARTIAL: difftest 5508 cases (5492 fixture: 1408 review (rational degree-1 / quadratic / arc edges meeting lines and arcs at a vertex, B-spline-line and reversed SmLine turns at 0.49..180 degrees, 1e-12..1e12 scales, 2- and 3-edge closed chains, tangent / gap / SetTolerance limits); 2160 on 15 upstream .smb B-reps read by SmBrep::ReadFromFile, some with RebuildUVTrimCurves; 1665 constructed: prog_test my_test_topology's wire network, my_test_edge_geometry_repairs' wire line (SetTolerance/SetPoint), chains of wire edges from real SmLines (re-parameterised), degree-1 B-spline lines and B-spline curves turning by 0 to 180 degrees around the 0.5 degree continuity limit (IsTopologicalVertex / AreEdgesTangent), closed squares, a closed seam curve, wire edges and plane / B-spline / pinched faces (full and sub-domain); 259 adversarial: 1e-6..1e6 scales, zero-length and tiny segments, a point B-spline, tolerance limits incl. SM_USE_DEFAULT and negatives, gap tolerance limits, every notify operation, null selectors, far edges; 16 generated), 4651 pass, 857 deferred (topology.edgeuse_p3 399, surface.surface_p3 382, topology.face_p3 76; SmEdge::SetTolerance wired by topology.edge PR #103; after merging main: SmFace::GetUpwardFaceuse wired to topology::face::p2::upward_faceuse, the shim exports each face's upward face use, and heal.gap gaps are real), max_rel_err 0; --perturb SENSITIVE. Runner imports the shim's exported graph (with each vertex's vertex use list in upstream order, cached gap values, edge curve identity) into topology::Brep. topology::Vertex gains vertexuses (m_pList) and poly_vertex; SM_TOLERANT_WATCH is off in the reference build (no m_bExact: IsExact is TRUE, SetExact no-op). Upstream bugs WR-91 (GetVertexuseOfFace ignores its face), WR-92 (IsTopologicalVertex SER/cusps), UB-105 (GetMaxUpDimGap3d NULL array), UB-106 (uninitialised tangents). Wired UNPORTED(topology.vertex) in geom::tools (geom.tools: 60 deferred now pass); generic-handle stubs in intersect::point_class, geom::solution_array, geom::object, geom::tol, surface::trim_srf_cache, topology::proto_topology::p1::proto and query::poly stay (opaque handles, plan/findings.md); the co-ported vertex use scans in topology::vertexuse now call topology::vertex. Plan notes: 38 fns; largest: SmVertex::IsTopologicalVertex, SmVertex::Notify, SmVertex::IsConnectedTo, SmVertex::GetMaxUpDimGap3d; cycle back-edges (call into later units; stub or co-port): topology.edge(12), heal.gap(4), topology.loopuse(2), blend.fillet_geom_p1(1), topology.face_p3(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmVertex::m_sZoneTol3d -> topology::Vertex::tolerance; CO-PORTED in topology.vertexuse (PR #88): SmVertex::GetVertexuses / GetVertexusesOfEdge / GetVertexusesOfFace (replaced by topology::vertex in PR #96); CO-PORTED in topology.face_p2 (PR #91): SmVertex::GetEdges without face filter (uses found by scanning the B-rep's vertex uses) -> private topology::face::p2::vertex_edges Re-run (PR #52, after merging surface_p4/#49 and surface_p5/#50): 5508 cases, 4984 pass, 524 deferred (topology.edgeuse_p3 448, topology.face_p3 76). [topology.edgeuse_p3 wired (PR #117): 5508 cases, 4896 pass, 0 failed, 612 deferred (surface.cone_p1 104, surface.sphere 104, surface.surf_of_extrusion_p2 104, topology.brep_p10 224, topology.face_p3 76)] [PR #155: the inexact-import deferrals are labelled topology.brep_p10 SmBrep::MakeTopologyFromData, not topology.brep_p7 SmBrep::ReadFromFile] [topology.face_p3 wired (PR #139, SmFace::SetTolerance in RefreshTolerance): 5508 cases, 4972 pass, 0 failed, 536 deferred (surface.cone_p1 104, surface.sphere 104, surface.surf_of_extrusion_p2 104, topology.brep_p10 224)] [2026-10-23 GREEN (PR #605): the shim exports each analytic face surface exactly (sc_analytic_export.h record head) and the runner reads it with util::read_exact_surface, so cone/sphere/revolution/extrusion faces are real SurfaceKinds and the solving ops compare values: PASS topology.vertex cases=5508 passed=5508 failed=0 deferred=0 max_rel_err=0e0 with_values=5506 no_values=2 vacuous=0] [#521: a failed GetEndTangent in AreEdgesTangent / IsTopologicalVertex leaves the SmVector3d default (SM_UNDEF_DOUBLE), not an uninitialised tangent: the port reads Vec3::UNDEF (angle 0, TRUE) where it returned InvalidInput, and the shim refuses only a null curve (WR-256, UB-106 corrected); 70 cases in end_tangents.json (zero end tangents, 1e-30 end spans): PASS topology.vertex cases=5578 passed=5578 failed=0 deferred=0 max_rel_err=0e0 with_values=5576 no_values=2 vacuous=0]" diff --git a/plan/upstream-issues.md b/plan/upstream-issues.md index 04221b399..6934d0884 100644 --- a/plan/upstream-issues.md +++ b/plan/upstream-issues.md @@ -1483,6 +1483,7 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Confirmed:** no (the difftest shim checks the evaluations first and does not call these). - **Severity:** crash/UB. - **Rust port:** `SolidError::InvalidInput`. +- **Correction (#521):** only the curveless edge is UB. At `c9c5979` `SmVector3d`'s members default to `SM_UNDEF_DOUBLE` (`source/SMLib/inc/SmVector3d.h:56-58`), so the tangent a failed `GetEndTangent` leaves is defined, and both functions then answer `TRUE`. See WR-256. The port and the shim now follow that; a null curve is still refused. #### UB-130. `sm_CheckEdgeOnAxis` reads `sSolutions[1]` with one solution - **Location:** `source/SMLib/src/SmRotationalSweepGeometry.cpp:224`-`226` (`sm_CheckEdgeOnAxis`, file static), reached by `SmRotationalSweepGeometry::EdgeSweepHigher` and `EdgeSweepSame`. @@ -2855,6 +2856,15 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Severity:** wrong result (latent). - **Rust port:** `MergeAddedOptions::default()` is what the declared defaults actually pass, `(TRUE, FALSE, TRUE, TRUE)`; `MERGE_ADDED_FACE_OPTIONS` is `MergeAddedFace`'s explicit `(FALSE, TRUE, TRUE, TRUE)`. +#### WR-256. `SmVertex::AreEdgesTangent` / `IsTopologicalVertex` answer `TRUE` when an end tangent cannot be evaluated +- **Location:** `source/SMLib/src/SmVertex.cpp:70` `AreEdgesTangent` (its four `SE(GetEndTangent(...))`), `:716` `IsTopologicalVertex` (the `SE(GetEndTangent(FALSE, ...))` branches); `SmEdge::GetEndTangent` (`SmEdge.cpp:476`) writes the tangent only after its `SER` on the curve evaluation. +- **What happens:** The ignored failure leaves the caller's `SmVector3d` as it was: its default `(SM_UNDEF_DOUBLE, SM_UNDEF_DOUBLE, SM_UNDEF_DOUBLE)`, or in `IsTopologicalVertex` the line vector `IsLine` wrote when that edge's curve is a line. Its squared length (about `4.6e-122`) is within `SM_IS_ZERO_SQUARED`, so `AngleBetween` fails, sets the angle to `0` first, and the status is ignored. `AreEdgesTangent` then returns `TRUE` for any positive tolerance, and `IsTopologicalVertex` passes its angle test, so the vertex counts as removable although nothing is known about the edges' directions there. (At an edge's start `IsTopologicalVertex` returns the failed status itself as `TRUE`, WR-92.) This corrects UB-106, which took the tangent to be uninitialised. +- **Trigger:** an edge whose curve cannot be evaluated with one derivative at its interval end. B-spline curves and `SmLine`s cannot get there: `SmBSplineCurve::Evaluate` clamps the parameter into the natural interval, and `N_BasisFindSpan` (`NL_BasisBasic.cpp:1429`) steps past end spans shorter than `1e-12` before `sm_BasisDerivs` could reject a knot difference of `SM_EFF_ZERO_SQ`; a NaN parameter evaluates to NaN without an error. An edge whose curve is an `SmCompositeCurve` always reaches it: that class keeps `SmCurve::Evaluate`, which returns `SM_ERR` (`SmCurve.h:390-404`). So can other curve kinds whose `Evaluate` fails (a curve on a surface whose evaluation fails, for example). +- **How found:** issue #521 (values read after a failed call); traced in the source and probed with the `topology.vertex` difftest (`end_tangents.json`: a 1e-30 end span and zero end tangents, compared, none of which makes `GetEndTangent` fail); the composite-curve edge is the port's unit test `failed_end_tangent_reads_the_default_vector`. +- **Confirmed:** no (the `topology.vertex` harness builds line and B-spline edges only, and none of its inputs makes `GetEndTangent` fail). +- **Severity:** wrong result. +- **Rust port:** Faithful: `topology::vertex` reads `Vec3::UNDEF` for a tangent nothing wrote (it returned `SolidError::InvalidInput` before #521). + ### NLib #### WR-22. `ST_surxsdr` returns a stale error number past its recursion limit