From d7a00b6c33e5643357252f18854ecc4aef0c26c1 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Tue, 27 Oct 2026 21:02:27 +0100 Subject: [PATCH 1/3] fix(geometry): Triangle3D.Reverse reverses the stored vertex order (Q4) Triangle3D.Reverse() called points.Reverse() on its Point3D[]. With System.Linq in scope that binds to Enumerable.Reverse, whose result was discarded, so the method never changed anything. Its two callers, Query.Volume(Shell) and Create.Mesh3D(Shell), rely on it to correct triangle winding against the parent face normal, so both silently kept whatever winding the triangulation produced. Use Array.Reverse(points) so the stored order is reversed in place. Signature, JSON schema, coordinates, tolerances, area filters and shell-normal conventions are unchanged. Adds Triangle3DReverseTests (132 cases): reversal contract (order, normal, signed volume, double reversal, area, centroid, bounds, segments, clone, JSON, Triangle3DObject) and both callers on analytically known closed solids with mixed face winding, translations and rotations. Generated by Michal Dengusiak and CodeClaude Co-Authored-By: Claude Sonnet 5.5 --- .../Geometry/Spatial/Classes/Triangle3D.cs | 2 +- SAM/SAM.Tests/Triangle3DReverseTests.cs | 359 ++++++++++++++++++ 2 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 SAM/SAM.Tests/Triangle3DReverseTests.cs diff --git a/SAM/SAM.Geometry/Geometry/Spatial/Classes/Triangle3D.cs b/SAM/SAM.Geometry/Geometry/Spatial/Classes/Triangle3D.cs index e81ccf9e0..d20067ce7 100644 --- a/SAM/SAM.Geometry/Geometry/Spatial/Classes/Triangle3D.cs +++ b/SAM/SAM.Geometry/Geometry/Spatial/Classes/Triangle3D.cs @@ -138,7 +138,7 @@ public Point3D GetCentroid() public void Reverse() { - points.Reverse(); + Array.Reverse(points); } public Point3D this[int index] diff --git a/SAM/SAM.Tests/Triangle3DReverseTests.cs b/SAM/SAM.Tests/Triangle3DReverseTests.cs new file mode 100644 index 000000000..dbdcaaae0 --- /dev/null +++ b/SAM/SAM.Tests/Triangle3DReverseTests.cs @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020–2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using System.Collections.Generic; +using System.Linq; +using SAM.Geometry.Object.Spatial; +using SAM.Geometry.Spatial; +using Xunit; + +namespace SAM.Tests +{ + /// + /// Triangle3D.Reverse() used to call the LINQ Enumerable.Reverse on its Point3D[] and discard the result, so it + /// never changed anything. Its two callers (Query.Volume(Shell) and Create.Mesh3D(Shell)) rely on it to correct + /// triangle winding against the parent face normal. + /// + public class Triangle3DReverseTests + { + private const double Tol = 1e-9; + + private static readonly Point3D A = new Point3D(0, 0, 0); + private static readonly Point3D B = new Point3D(4, 0, 0); + private static readonly Point3D C = new Point3D(0, 3, 0); + + public static IEnumerable Poses() + { + foreach (string pose in new string[] { "identity", "translation", "rotationZ", "rotationXYZ", "rotationAndTranslation" }) + { + yield return new object[] { pose }; + } + } + + private static Transform3D GetPose(string pose) + { + switch (pose) + { + case "translation": + return Transform3D.GetTranslation(12.5, -7.25, 3.0); + case "rotationZ": + return Transform3D.GetRotationZ(0.7); + case "rotationXYZ": + return Transform3D.GetRotationX(0.9).Multiply(Transform3D.GetRotationY(-1.1)).Multiply(Transform3D.GetRotationZ(0.4)); + case "rotationAndTranslation": + return Transform3D.GetRotationY(2.3).Multiply(Transform3D.GetRotationX(0.6)).Multiply(Transform3D.GetTranslation(-3, 9, 21)); + default: + return Transform3D.GetIdentity(); + } + } + + // ---- Triangle3D unit tests ------------------------------------------------------------------------- + + [Fact] + public void Reverse_Triangle_ReversesVertexOrder() + { + Triangle3D triangle3D = new Triangle3D(A, B, C); + + triangle3D.Reverse(); + + List point3Ds = triangle3D.GetPoints(); + AssertSame(C, point3Ds[0]); + AssertSame(B, point3Ds[1]); + AssertSame(A, point3Ds[2]); + } + + [Fact] + public void Reverse_Triangle_IndexerReflectsNewOrder() + { + Triangle3D triangle3D = new Triangle3D(A, B, C); + + triangle3D.Reverse(); + + AssertSame(C, triangle3D[0]); + AssertSame(B, triangle3D[1]); + AssertSame(A, triangle3D[2]); + } + + [Fact] + public void Reverse_Triangle_FlipsNormal() + { + Triangle3D triangle3D = new Triangle3D(A, B, C); + Vector3D before = triangle3D.GetNormal(); + + triangle3D.Reverse(); + + Vector3D after = triangle3D.GetNormal(); + Assert.Equal(-before.X, after.X, 9); + Assert.Equal(-before.Y, after.Y, 9); + Assert.Equal(-before.Z, after.Z, 9); + Assert.False(after.SameHalf(before)); + } + + [Theory] + [MemberData(nameof(Poses))] + public void Reverse_NonzeroSignedVolume_ChangesSign(string pose) + { + Triangle3D triangle3D = (Triangle3D)new Triangle3D(new Point3D(1, 2, 3), new Point3D(5, 2, 3.5), new Point3D(1, 6, 4)).GetTransformed(GetPose(pose)); + double before = triangle3D.SignedVolume(); + Assert.True(System.Math.Abs(before) > 1e-6, pose); + + triangle3D.Reverse(); + + Assert.Equal(-before, triangle3D.SignedVolume(), 9); + } + + [Theory] + [MemberData(nameof(Poses))] + public void Reverse_Twice_RestoresOriginalVertexSequence(string pose) + { + Triangle3D original = (Triangle3D)new Triangle3D(A, B, C).GetTransformed(GetPose(pose)); + Triangle3D triangle3D = new Triangle3D(original); + triangle3D.Reverse(); + Assert.False(original == triangle3D, pose); + + triangle3D.Reverse(); + + Assert.True(original == triangle3D, pose); + Assert.True(original.Equals(triangle3D), pose); + } + + [Theory] + [MemberData(nameof(Poses))] + public void Reverse_Triangle_PreservesAreaCentroidAndBounds(string pose) + { + Triangle3D triangle3D = (Triangle3D)new Triangle3D(A, B, C).GetTransformed(GetPose(pose)); + double area = triangle3D.GetArea(); + Point3D centroid = triangle3D.GetCentroid(); + BoundingBox3D bounds = triangle3D.GetBoundingBox(); + double length = triangle3D.GetLength(); + + triangle3D.Reverse(); + + Assert.Equal(area, triangle3D.GetArea(), 9); + Assert.Equal(length, triangle3D.GetLength(), 9); + AssertSame(centroid, triangle3D.GetCentroid()); + BoundingBox3D boundsAfter = triangle3D.GetBoundingBox(); + AssertSame(bounds.Min, boundsAfter.Min); + AssertSame(bounds.Max, boundsAfter.Max); + } + + [Fact] + public void Reverse_Triangle_ReversesSegmentsAndKeepsClosedLoop() + { + Triangle3D triangle3D = new Triangle3D(A, B, C); + + triangle3D.Reverse(); + + List segment3Ds = triangle3D.GetSegments(); + Assert.Equal(3, segment3Ds.Count); + AssertSame(C, segment3Ds[0][0]); + AssertSame(B, segment3Ds[0][1]); + AssertSame(B, segment3Ds[1][0]); + AssertSame(A, segment3Ds[1][1]); + AssertSame(A, segment3Ds[2][0]); + AssertSame(C, segment3Ds[2][1]); + } + + [Fact] + public void Reverse_Triangle_SurvivesCloneAndJsonRoundTrip() + { + Triangle3D triangle3D = new Triangle3D(A, B, C); + triangle3D.Reverse(); + + Triangle3D clone = (Triangle3D)triangle3D.Clone(); + Triangle3D fromJson = new Triangle3D(triangle3D.ToJsonObject()); + + Assert.True(triangle3D == clone); + Assert.True(triangle3D == fromJson); + AssertSame(C, fromJson[0]); + AssertSame(A, fromJson[2]); + } + + [Fact] + public void Reverse_Triangle_DoesNotAffectCopiesOrSourcePoints() + { + Triangle3D triangle3D = new Triangle3D(A, B, C); + Triangle3D copy = new Triangle3D(triangle3D); + + triangle3D.Reverse(); + + AssertSame(A, copy[0]); + AssertSame(C, copy[2]); + Assert.Equal(0, A.X); + Assert.Equal(4, B.X); + Assert.Equal(3, C.Y); + } + + [Fact] + public void Reverse_Triangle3DObject_ReversesAndKeepsMetadata() + { + Triangle3DObject triangle3DObject = new Triangle3DObject(new Triangle3D(A, B, C)); + Vector3D before = triangle3DObject.GetNormal(); + + triangle3DObject.Reverse(); + + AssertSame(C, triangle3DObject[0]); + AssertSame(A, triangle3DObject[2]); + Assert.False(triangle3DObject.GetNormal().SameHalf(before)); + } + + // ---- Caller 1: Query.Volume(Shell) ----------------------------------------------------------------- + + public static IEnumerable VolumeCases() + { + foreach (object[] pose in Poses()) + { + foreach (int flipMask in new int[] { 0, 0b111111111, 0b000010101, 0b101010010, 0b000000001 }) + { + yield return new object[] { pose[0], flipMask }; + } + } + } + + [Theory] + [MemberData(nameof(VolumeCases))] + public void Volume_ConcaveLPrismWithMixedFaceWinding_EqualsAnalyticVolume(string pose, int flipMask) + { + // L footprint 2x2 minus 1x1 corner = 3, height 2 -> 6 + Shell shell = CreateLPrism(2.0, GetPose(pose), flipMask); + + Assert.Equal(6.0, shell.Volume(), 5); + } + + [Theory] + [MemberData(nameof(VolumeCases))] + public void Volume_BoxWithMixedFaceWinding_EqualsAnalyticVolume(string pose, int flipMask) + { + Shell shell = CreateBox(2, 3, 4, GetPose(pose), flipMask); + + Assert.Equal(24.0, shell.Volume(), 5); + } + + [Theory] + [MemberData(nameof(VolumeCases))] + public void Volume_TriangularPrismWithMixedFaceWinding_EqualsAnalyticVolume(string pose, int flipMask) + { + // right triangle legs 3 and 4 (area 6), height 5 -> 30 + List footprint = new List() { new Point3D(0, 0, 0), new Point3D(3, 0, 0), new Point3D(0, 4, 0) }; + Shell shell = CreatePrism(footprint, 5.0, GetPose(pose), flipMask); + + Assert.Equal(30.0, shell.Volume(), 5); + } + + // ---- Caller 2: Create.Mesh3D(Shell) ---------------------------------------------------------------- + + [Theory] + [MemberData(nameof(VolumeCases))] + public void Mesh3D_Shell_EveryTriangleFollowsParentFaceNormal(string pose, int flipMask) + { + Shell shell = CreateLPrism(2.0, GetPose(pose), flipMask); + List face3Ds = shell.Face3Ds; + + Mesh3D mesh3D = Geometry.Spatial.Create.Mesh3D(shell); + + List triangle3Ds = mesh3D.GetTriangles(); + Assert.NotEmpty(triangle3Ds); + foreach (Triangle3D triangle3D in triangle3Ds) + { + Point3D centroid = triangle3D.GetCentroid(); + List parents = face3Ds.FindAll(x => x.Inside(centroid, 1e-6)); + Assert.Single(parents); + + Vector3D triangleNormal = triangle3D.GetNormal().GetNormalized(); + Vector3D faceNormal = parents[0].GetPlane().Normal.GetNormalized(); + double dot = (triangleNormal.X * faceNormal.X) + (triangleNormal.Y * faceNormal.Y) + (triangleNormal.Z * faceNormal.Z); + Assert.True(dot > 0.99, $"{pose} mask {flipMask}: triangle normal disagrees with face normal (dot {dot})"); + } + } + + [Theory] + [MemberData(nameof(Poses))] + public void Mesh3D_OutwardShell_HasPositiveClosedSignedVolume(string pose) + { + Shell shell = CreateLPrism(2.0, GetPose(pose), 0); + + Mesh3D mesh3D = Geometry.Spatial.Create.Mesh3D(shell); + + double signedVolume = mesh3D.GetTriangles().Sum(x => x.SignedVolume()); + Assert.Equal(6.0, signedVolume, 5); + } + + [Theory] + [MemberData(nameof(Poses))] + public void Mesh3D_InwardShell_HasNegativeClosedSignedVolume(string pose) + { + Shell shell = CreateLPrism(2.0, GetPose(pose), 0b111111111); + + Mesh3D mesh3D = Geometry.Spatial.Create.Mesh3D(shell); + + double signedVolume = mesh3D.GetTriangles().Sum(x => x.SignedVolume()); + Assert.Equal(-6.0, signedVolume, 5); + } + + // ---- helpers --------------------------------------------------------------------------------------- + + private static void AssertSame(Point3D expected, Point3D actual) + { + Assert.NotNull(actual); + Assert.True(expected.Distance(actual) < Tol, $"expected {expected.X},{expected.Y},{expected.Z} but was {actual.X},{actual.Y},{actual.Z}"); + } + + private static Shell CreateLPrism(double height, Transform3D transform3D, int flipMask) + { + List footprint = new List() + { + new Point3D(0, 0, 0), new Point3D(2, 0, 0), new Point3D(2, 1, 0), + new Point3D(1, 1, 0), new Point3D(1, 2, 0), new Point3D(0, 2, 0) + }; + + return CreatePrism(footprint, height, transform3D, flipMask); + } + + private static Shell CreateBox(double width, double depth, double height, Transform3D transform3D, int flipMask) + { + List footprint = new List() + { + new Point3D(0, 0, 0), new Point3D(width, 0, 0), new Point3D(width, depth, 0), new Point3D(0, depth, 0) + }; + + return CreatePrism(footprint, height, transform3D, flipMask); + } + + /// + /// Outward-wound prism over a counter-clockwise (seen from +Z) footprint; every face whose bit is set in + /// has its vertex order reversed afterwards, giving mixed or inward winding. + /// + private static Shell CreatePrism(List footprint, double height, Transform3D transform3D, int flipMask) + { + int n = footprint.Count; + List> loops = new List>(); + + loops.Add(footprint.AsEnumerable().Reverse().ToList()); + loops.Add(footprint.ConvertAll(x => new Point3D(x.X, x.Y, x.Z + height))); + for (int i = 0; i < n; i++) + { + Point3D p = footprint[i]; + Point3D q = footprint[(i + 1) % n]; + loops.Add(new List() + { + new Point3D(p.X, p.Y, p.Z), new Point3D(q.X, q.Y, q.Z), + new Point3D(q.X, q.Y, q.Z + height), new Point3D(p.X, p.Y, p.Z + height) + }); + } + + List face3Ds = new List(); + for (int i = 0; i < loops.Count; i++) + { + List loop = loops[i].ConvertAll(x => (Point3D)x.GetTransformed(transform3D)); + if ((flipMask & (1 << i)) != 0) + { + loop.Reverse(); + } + + face3Ds.Add(new Face3D(new Polygon3D(loop))); + } + + return new Shell(face3Ds); + } + } +} From cb6bfbbcfaa0fd40ef563c3e9c7d6e01700a0ac0 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Tue, 27 Oct 2026 21:05:07 +0100 Subject: [PATCH 2/3] docs: Triangle3D.Reverse record and Softhouse R1 volume evidence (tested content d7a00b6c) Generated by Michal Dengusiak and CodeClaude Co-Authored-By: Claude Sonnet 5.5 --- documentation/Triangle3D-Reverse-Q4.md | 137 ++++++++++++++++++ .../r1probe.Program.cs.txt | 37 +++++ .../volumes-pr84-R-after-fix.tsv | 60 ++++++++ .../volumes-pr84-R-before-fix.tsv | 60 ++++++++ .../volumes-pr84fix-R-after-fix.tsv | 60 ++++++++ .../volumes-pr84fix-R-before-fix.tsv | 60 ++++++++ 6 files changed, 414 insertions(+) create mode 100644 documentation/Triangle3D-Reverse-Q4.md create mode 100644 documentation/evidence/triangle3d-reverse-q4/r1probe.Program.cs.txt create mode 100644 documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-after-fix.tsv create mode 100644 documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-before-fix.tsv create mode 100644 documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-after-fix.tsv create mode 100644 documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-before-fix.tsv diff --git a/documentation/Triangle3D-Reverse-Q4.md b/documentation/Triangle3D-Reverse-Q4.md new file mode 100644 index 000000000..1726d7bcd --- /dev/null +++ b/documentation/Triangle3D-Reverse-Q4.md @@ -0,0 +1,137 @@ +# Triangle3D.Reverse no-op — Q4 (2026-10-27) + +Status: draft PR into `sow/2026-Q4` (base `9d8961ec`), branch `fix/triangle3d-reverse-q4`. **Tested content `d7a00b6c`**; +the later commit changes this record and its evidence only, and the PR description holds the final head and hosted +checks. Not merged; no merge is authorised. + +## Problem + +`Triangle3D` keeps its vertices in a `Point3D[]`. `Reverse()` was `points.Reverse();`. With `using System.Linq;` in +scope that binds to `Enumerable.Reverse` (`netstandard2.0`, `LangVersion 12`), which returns a new sequence; the result +was discarded, so `Reverse()` never changed the triangle. + +Two callers rely on it to correct a triangle's winding against its parent face normal: + +| Caller | Use | +| --- | --- | +| `Query.Volume(Shell)` (`Geometry/Spatial/Query/Volume.cs:56`) | reverses a triangle whose normal is not in the face-normal half-space, then adds `SignedVolume()` | +| `Create.Mesh3D(Shell)` (`Geometry/Spatial/Create/Mesh3D.cs:173`) | same test; the triangles then feed `Mesh3D(IEnumerable)`, which indexes vertices through `this[i]` | + +With the no-op, the shell volume depended on the raw winding of each triangulation, and the mesh triangles kept +whatever winding `Planar.Query.Triangulate` produced instead of the face's. No other caller of `Triangle3D.Reverse()` +exists in SAM or in the sibling repositories (SAM_OCCT, SAM_Building, SAM_Tas, SAM_Topologic, SAM_gbXML, SAM_Revit, +SAM_SolarCalculator, SAM_Systems, SAM_UI, SAM_IFC, SAM_Rhino_UI, SAM_Tas_Grasshopper; grep of `.cs`). + +## Change + +`SAM/SAM.Geometry/Geometry/Spatial/Classes/Triangle3D.cs`: `points.Reverse();` → `Array.Reverse(points);` (in place, the +same contract as the other `Reverse()` implementations: the stored vertex order is reversed). One line. + +Unchanged: public signatures, the JSON schema (`Points` array, written in stored order), coordinates, tolerances, the +`silverSpacing` area filter in `Volume`, `OrientNormals`, the shell-normal convention, `Shell.Volume` itself, solver +behaviour, panel order, golden files. + +`Triangle2D.Reverse()` has the identical defect (`Geometry/Planar/Classes/Triangle2D.cs:205`). It has **no callers** and +was left alone (scope), recorded under "Separate". + +## Behaviour change (callers) + +| Consumer | Effect | +| --- | --- | +| `Shell.Volume()` | a triangle wound against its face is now corrected; closed shells whose triangulation mixes windings no longer carry a signed per-triangle error. Closed convex or consistently triangulated shells are unchanged | +| `Create.Mesh3D(Shell)` | every mesh triangle now follows its parent face normal; a closed outward shell gives a positive signed mesh volume | +| `Analytical.Query.Volume(Space…)`, `SpaceVolumeFilter`, `Modify.Remove` (via `Shell.Volume()`) | results move only where the volume was wrong; see the Softhouse table | +| `Mesh3D(IEnumerable)`, `Query.Convert`, `Transform`, `Project`, `Offset`, `Face3Ds`, `TryGetSAMGeometries` | none: they never call `Reverse()` | + +## Tests (`SAM/SAM.Tests/Triangle3DReverseTests.cs`, xUnit, AAA, SPDX, 132 cases) + +- Contract (on 5 poses: identity, translation, rotation about Z, composed X·Y·Z rotation, rotation + translation): + vertex order reversed; indexer reflects it; normal flips; non-zero `SignedVolume` changes sign; one reversal changes + the triangle and two restore the sequence (`==` and `Equals`); area, length, centroid and bounds unchanged; segments + reversed and closed; `Clone()` and JSON round-trip keep the reversed order; a copy made earlier is unaffected; + `Triangle3DObject` reverses. +- `Shell.Volume()`: a concave L-shaped prism (analytic 6), a box (24) and a right-triangle prism (30), each on 5 poses + with 5 face-winding masks (outward, all inward, three mixed masks). +- `Create.Mesh3D(Shell)`: for the L prism on 5 poses × 5 masks, every mesh triangle's unit normal agrees with the face + that contains its centroid; an outward shell gives mesh signed volume +6 and a fully inward one −6. + +## Validation (local, machine `michal`, 2026-10-27) + +Build/test commands run from `SAM/SAM` with `APPDATA` and `USERPROFILE` redirected to a scratch profile +(`SAM` and `SAM_OCCT` Grasshopper post-build steps copy into `%APPDATA%\SAM`). The real +`%APPDATA%\SAM` was not used. + +``` +dotnet test SAM.Tests/SAM.Tests.csproj --filter "FullyQualifiedName~Triangle3DReverseTests" +``` + +| Check | Result | +| --- | --- | +| New tests on unchanged source (red; `points.Reverse()`, same build) | **76 fail / 56 pass** of 132. Failing: every contract test that needs a changed sequence (vertex order, indexer, normal, signed-volume sign, segments, JSON/clone, `Triangle3DObject`, double-reversal intermediate state), `Volume` of the concave prism (25 / 25), and all 35 `Mesh3D(Shell)` cases. Passing: the invariance rows (area, centroid, bounds, copy independence) and the `Volume` box and triangular-prism rows, whose triangulations happen to be consistently wound (kept as compatibility guards) | +| New tests on the fix (green) | **132 / 132** | +| SAM.Tests, full, on the fix | **3975 / 3975**, 0 skipped (`SAM.Geometry.dll` `EF2B5D4E`) | +| SAM.Core.Grasshopper.Tests (Debug; built with the scratch profile, run `--no-build` with the normal one), on the fix | **87 / 87** (loaded `SAM.Geometry.dll` `EF2B5D4E`; this project covers `TryGetSAMGeometries`, the only Grasshopper consumer of `Triangle3D` besides `GeometryTriangulate`) | +| Changed expectations | none: no existing test, golden or fixture changed | + +The test poses compose `GetRotationX/Y/Z`. `Transform3D.GetRotation(axis, angle)` was **not** used: it is marked +"Method to be revised" and builds a non-orthonormal matrix (its `[0,2]` and `[2,0]` entries carry the same sign), so a +solid rotated with it is distorted (a 24 m3 box became 23.87). Existing, unrelated, not changed. + +### Saved Softhouse R1 closeout models (read only; no solver rerun, no native rebuild) + +Models: `SAM_daily\2026-10-25-Softthouse\handover\evidence\2026-10-27-sam-root-face2d-loop\chains\pr84-R\model.json` +(baseline) and `...\chains\pr84fix-R\model.json` (corrected SAM), 59 spaces each. A scratch probe +(`documentation/evidence/triangle3d-reverse-q4/r1probe.Program.cs.txt`; a `net8.0` console referencing +`SAM.Core/Geometry/Analytical`) loads each model and, per space, prints `ac.Shell(space).Volume()` and an +independent winding-free volume `|Σ area × n·p / 3|` over the faces after the same `OrientNormals` call. It was run +against the pre-fix `SAM.Geometry.dll` (`558D2B86`) and the fixed one (`EF2B5D4E`), both built from this branch. + +| Cell | Model | `Shell.Volume()` before fix | after fix | independent | +| --- | --- | ---: | ---: | ---: | +| (12.159, 6.705, 4.975) | baseline | 31.242936 | 31.242936 | 31.242939 | +| (12.159, 6.705, 4.975) | corrected | **31.23669** | **31.242936** | 31.242939 | +| (12.853, 4.891, 7.963) | baseline | 36.172684 | 36.17268 | 36.17269 | +| (12.853, 4.891, 7.963) | corrected | **36.178925** | **36.17268** | 36.172689 | + +- Before the fix the probe reproduces the saved defect exactly (31.23669 and 36.178925, the ±0.006 m3 of the R1 + closeout). After it, the two cells agree between the models and with the independent volume to ≤ 1e-5 m3. +- Over the 52 closed cells of each model, the largest `|Shell.Volume() − independent|` is 0.000021 (baseline, both + builds), 0.006249 → 0.000022 (corrected: before → after). No other cell changed (a per-cell diff of the two builds + lists exactly these two cells). 7 cells per model return NaN in every build (open shells, `IsClosed` is false): an + existing, unrelated condition. +- Tables: `documentation/evidence/triangle3d-reverse-q4/volumes---fix.tsv`. + +**This correction therefore fully explains the saved ±0.006 m3 volume defect.** No remaining mechanism was found, so the +"stop and report" condition did not occur. The OCCT panel-order sensitivity from the R1 closeout is separate and +unchanged. + +## Compatibility + +- Public API, JSON schema and serialised models: no change; `Triangle3D` JSON written before or after is identical for + the same vertex order, and a reversed triangle serialises its reversed order. +- Callers: only the two above. Mesh output of `Create.Mesh3D(Shell)` changes only in triangle vertex order (and + therefore normal) for triangles that were wound against their face; positions and counts are unchanged. +- Saved definitions: no component, parameter or GUID is touched. + +## Decisions and assumptions + +- `Array.Reverse(points)` keeps the field type and the existing contract (the other `Reverse()` methods reverse in place). +- `Shell.Volume`'s design is not changed; its per-triangle correction was the intent and now works. +- **Grasshopper versions: recommend no bump in this PR** (owner decision under GH-H2). No component code, parameter or + GUID changes; the results that change are corrected volumes and mesh winding. GH-H2 lists a different geometry result + as minor or major by impact, so the owner decides. Components whose output can change indirectly through + `Space.Volume`: `SAMAnalyticalModifyVentilationProfileByFunction`, `SAMAnalyticalUpdateInfiltrationGains`, the + space-volume filter and `Remove` paths. Deployed Grasshopper needs the new `SAM.Geometry.dll` (a coherent redeploy, + `BuildAlls_v5.bat`); the shared GH version pins in SAM_OCCT#84 are a separate decision and are not changed here. + +## Separate + +- `Triangle2D.Reverse()` has the same `points.Reverse()` no-op; no callers; a one-line fix when wanted. +- `Transform3D.GetRotation(axis, angle)` is non-orthonormal (see above). +- OCCT/AdjacencyCluster panel-order sensitivity and open-shell NaN volumes: unchanged. + +## Next step + +Independent review of the PR; then mark ready for review (not a merge authorisation). After merge: a docs-only +`[skip ci]` closeout in `PROJECT_PROGRESS.md` on `sow/2026-Q4` with the merge SHA, and a coherent GH redeploy. Recommended +for review: Claude Opus 5.5, medium effort, a new session, Plan mode (read-only review of one line and its tests). diff --git a/documentation/evidence/triangle3d-reverse-q4/r1probe.Program.cs.txt b/documentation/evidence/triangle3d-reverse-q4/r1probe.Program.cs.txt new file mode 100644 index 000000000..980c33dcb --- /dev/null +++ b/documentation/evidence/triangle3d-reverse-q4/r1probe.Program.cs.txt @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SAM.Analytical; +using SAM.Geometry.Spatial; + +// Read-only: loads saved models, never writes them. args: label modelPath +class P +{ + // Winding-independent volume: (1/3) * sum(area * n.p) over the oriented faces of the shell as Shell.Volume() orients it. + static double Independent(Shell shell) + { + Shell t = new Shell(shell); + t.OrientNormals(false, SAM.Core.Tolerance.MacroDistance, SAM.Core.Tolerance.Distance); + double v = 0; + foreach (Face3D f in t.Face3Ds) + { + Plane pl = f.GetPlane(); var n = pl.Normal.GetNormalized(); var o = pl.Origin; + v += f.GetArea() * (n.X * o.X + n.Y * o.Y + n.Z * o.Z) / 3.0; + } + return Math.Abs(v); + } + + static void Main(string[] a) + { + var model = SAM.Core.Convert.ToSAM(a[1]).First(); + var ac = model.AdjacencyCluster; + Console.WriteLine("space\tShell.Volume\tindependent\tdelta"); + foreach (var s in ac.GetSpaces().OrderBy(x => x.Location.Z).ThenBy(x => x.Location.X).ThenBy(x => x.Location.Y)) + { + var shell = ac.Shell(s); + double v = shell.Volume(), i = Independent(shell); + Console.WriteLine($"({s.Location.X:0.000},{s.Location.Y:0.000},{s.Location.Z:0.000})\t{v:0.######}\t{i:0.######}\t{v - i:0.######}"); + } + } +} diff --git a/documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-after-fix.tsv b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-after-fix.tsv new file mode 100644 index 000000000..cb29f9dde --- /dev/null +++ b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-after-fix.tsv @@ -0,0 +1,60 @@ +space Shell.Volume independent delta +(-4.151,0.953,1.750) 217.020229 217.020228 0.000001 +(15.880,5.743,1.750) 7.399231 7.399232 -0.000001 +(15.880,3.783,1.750) 9.636162 9.636162 0 +(-5.031,2.115,1.750) 12.33198 12.33198 0 +(4.289,3.779,1.750) 9.775824 9.775821 0.000003 +(6.560,2.115,1.750) 12.62248 12.622481 -0.000001 +(-1.524,3.783,1.750) 9.499276 9.499277 -0.000001 +(-1.524,5.615,1.750) 6.211036 6.211034 0.000002 +(0.735,2.115,1.750) 12.558777 12.558775 0.000002 +(4.289,5.624,1.750) 6.190207 6.190207 -0 +(6.666,5.599,1.750) 68.460367 68.460366 0.000001 +(10.108,3.782,1.750) 9.444463 9.444461 0.000002 +(-4.893,5.599,1.750) 67.693723 67.693712 0.000011 +(13.541,0.953,1.750) NaN 206.206901 NaN +(1.787,0.953,1.750) 208.047704 208.047704 -0 +(7.691,0.953,1.750) NaN 207.521772 NaN +(12.345,2.115,1.750) 12.426243 12.426243 0 +(10.108,5.613,1.750) 6.185971 6.185975 -0.000004 +(12.456,5.599,1.750) 67.554146 67.554146 0.000001 +(0.862,5.599,3.250) 109.754859 109.754845 0.000014 +(7.821,1.563,4.968) NaN 152.772016 NaN +(10.719,-2.707,4.968) 0.007352 0.007353 -0.000001 +(13.618,1.002,4.968) NaN 132.716693 NaN +(15.051,5.743,4.975) 14.902144 14.902144 -0 +(11.102,4.387,4.975) 2.250542 2.25054 0.000002 +(-2.352,6.707,4.975) 31.588133 31.588128 0.000004 +(15.051,7.558,4.975) 16.887604 16.88761 -0.000005 +(6.362,7.268,4.975) 21.768141 21.768143 -0.000001 +(4.905,-2.707,4.975) 0.012389 0.012387 0.000002 +(3.449,7.268,4.975) 22.092446 22.092443 0.000002 +(9.267,7.268,4.975) 21.985391 21.985394 -0.000004 +(-5.019,1.249,4.975) 36.479618 36.479617 0 +(-5.012,3.341,4.975) 3.158507 3.158507 -0 +(-6.261,4.357,4.975) 2.407617 2.407617 -0 +(-4.979,6.013,4.975) 42.220347 42.220341 0.000006 +(-0.522,4.376,4.975) 2.347651 2.347651 -0 +(2.007,1.563,4.975) 133.584632 133.584653 -0.000021 +(5.302,4.387,4.975) 2.357952 2.357951 0.000001 +(12.159,6.705,4.975) 31.242936 31.242939 -0.000003 +(11.102,4.389,7.962) 2.244064 2.244068 -0.000004 +(5.302,4.384,7.962) 2.326787 2.326787 0 +(-0.522,4.376,7.962) 2.328087 2.328087 -0 +(2.007,0.488,7.962) 95.170111 95.170108 0.000002 +(-2.352,6.707,7.962) 31.32489 31.324891 -0.000001 +(7.821,1.002,7.962) NaN 113.341951 NaN +(12.159,6.705,7.963) 30.966147 30.966138 0.000009 +(15.051,6.707,7.963) 31.524703 31.524702 0.000001 +(-2.142,2.150,7.963) 27.619595 27.619598 -0.000004 +(0.540,6.193,7.963) 37.670318 37.670318 -0 +(3.449,7.268,7.963) 21.908339 21.908338 0.000001 +(7.821,6.707,7.963) 62.761861 62.761859 0.000002 +(11.605,4.036,7.963) 6.675357 6.675353 0.000003 +(-5.011,5.851,7.963) NaN 44.885742 NaN +(10.018,1.660,7.963) 18.765156 18.765161 -0.000005 +(13.618,0.148,7.963) NaN 88.698021 NaN +(-3.771,0.146,7.963) 88.542602 88.542597 0.000004 +(-6.261,4.357,7.963) 2.387552 2.387552 0 +(2.196,3.517,7.963) 40.432523 40.432519 0.000004 +(12.853,4.891,7.963) 36.172684 36.17269 -0.000007 diff --git a/documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-before-fix.tsv b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-before-fix.tsv new file mode 100644 index 000000000..ece390733 --- /dev/null +++ b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84-R-before-fix.tsv @@ -0,0 +1,60 @@ +space Shell.Volume independent delta +(-4.151,0.953,1.750) 217.020229 217.020228 0.000001 +(15.880,5.743,1.750) 7.399231 7.399232 -0.000001 +(15.880,3.783,1.750) 9.636162 9.636162 0 +(-5.031,2.115,1.750) 12.33198 12.33198 -0 +(4.289,3.779,1.750) 9.775824 9.775821 0.000003 +(6.560,2.115,1.750) 12.62248 12.622481 -0.000001 +(-1.524,3.783,1.750) 9.499276 9.499277 -0.000001 +(-1.524,5.615,1.750) 6.211036 6.211034 0.000002 +(0.735,2.115,1.750) 12.558777 12.558775 0.000002 +(4.289,5.624,1.750) 6.190207 6.190207 -0 +(6.666,5.599,1.750) 68.460367 68.460366 0.000001 +(10.108,3.782,1.750) 9.444463 9.444461 0.000002 +(-4.893,5.599,1.750) 67.693723 67.693712 0.000011 +(13.541,0.953,1.750) NaN 206.206901 NaN +(1.787,0.953,1.750) 208.047704 208.047704 -0 +(7.691,0.953,1.750) NaN 207.521772 NaN +(12.345,2.115,1.750) 12.426243 12.426243 0 +(10.108,5.613,1.750) 6.185971 6.185975 -0.000004 +(12.456,5.599,1.750) 67.554146 67.554146 0.000001 +(0.862,5.599,3.250) 109.754859 109.754845 0.000014 +(7.821,1.563,4.968) NaN 152.772016 NaN +(10.719,-2.707,4.968) 0.007352 0.007353 -0.000001 +(13.618,1.002,4.968) NaN 132.716693 NaN +(15.051,5.743,4.975) 14.902144 14.902144 -0 +(11.102,4.387,4.975) 2.250542 2.25054 0.000002 +(-2.352,6.707,4.975) 31.588133 31.588128 0.000004 +(15.051,7.558,4.975) 16.887604 16.88761 -0.000005 +(6.362,7.268,4.975) 21.768141 21.768143 -0.000001 +(4.905,-2.707,4.975) 0.012389 0.012387 0.000002 +(3.449,7.268,4.975) 22.092446 22.092443 0.000002 +(9.267,7.268,4.975) 21.985391 21.985394 -0.000004 +(-5.019,1.249,4.975) 36.479618 36.479617 0 +(-5.012,3.341,4.975) 3.158507 3.158507 -0 +(-6.261,4.357,4.975) 2.407617 2.407617 -0 +(-4.979,6.013,4.975) 42.220347 42.220341 0.000006 +(-0.522,4.376,4.975) 2.347651 2.347651 -0 +(2.007,1.563,4.975) 133.584632 133.584653 -0.000021 +(5.302,4.387,4.975) 2.357952 2.357951 0.000001 +(12.159,6.705,4.975) 31.242936 31.242939 -0.000003 +(11.102,4.389,7.962) 2.244064 2.244068 -0.000004 +(5.302,4.384,7.962) 2.326787 2.326787 0 +(-0.522,4.376,7.962) 2.328087 2.328087 -0 +(2.007,0.488,7.962) 95.170111 95.170108 0.000002 +(-2.352,6.707,7.962) 31.32489 31.324891 -0.000001 +(7.821,1.002,7.962) NaN 113.341951 NaN +(12.159,6.705,7.963) 30.966147 30.966138 0.000009 +(15.051,6.707,7.963) 31.524703 31.524702 0.000001 +(-2.142,2.150,7.963) 27.619595 27.619598 -0.000004 +(0.540,6.193,7.963) 37.670318 37.670318 -0 +(3.449,7.268,7.963) 21.908339 21.908338 0.000001 +(7.821,6.707,7.963) 62.761861 62.761859 0.000002 +(11.605,4.036,7.963) 6.675357 6.675353 0.000003 +(-5.011,5.851,7.963) NaN 44.885742 NaN +(10.018,1.660,7.963) 18.765156 18.765161 -0.000005 +(13.618,0.148,7.963) NaN 88.698021 NaN +(-3.771,0.146,7.963) 88.542602 88.542597 0.000004 +(-6.261,4.357,7.963) 2.387552 2.387552 0 +(2.196,3.517,7.963) 40.432523 40.432519 0.000004 +(12.853,4.891,7.963) 36.172684 36.17269 -0.000007 diff --git a/documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-after-fix.tsv b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-after-fix.tsv new file mode 100644 index 000000000..4786e0709 --- /dev/null +++ b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-after-fix.tsv @@ -0,0 +1,60 @@ +space Shell.Volume independent delta +(-4.151,0.953,1.750) 217.020229 217.020228 0.000001 +(15.880,5.743,1.750) 7.399231 7.399232 -0.000001 +(15.880,3.783,1.750) 9.636162 9.636162 0 +(-5.031,2.115,1.750) 12.33198 12.33198 0 +(4.289,3.779,1.750) 9.775824 9.775821 0.000003 +(6.560,2.115,1.750) 12.62248 12.622481 -0.000001 +(-1.524,3.783,1.750) 9.499276 9.499277 -0.000001 +(-1.524,5.615,1.750) 6.211036 6.211034 0.000002 +(0.735,2.115,1.750) 12.558777 12.558775 0.000002 +(4.289,5.624,1.750) 6.190207 6.190207 -0 +(6.666,5.599,1.750) 68.460367 68.460366 0.000001 +(10.108,3.782,1.750) 9.444464 9.444462 0.000002 +(-4.893,5.599,1.750) 67.693723 67.693712 0.000011 +(13.541,0.953,1.750) NaN 206.206901 NaN +(1.787,0.953,1.750) 208.047704 208.047704 -0 +(7.691,0.953,1.750) NaN 207.521772 NaN +(12.345,2.115,1.750) 12.426243 12.426243 0 +(10.108,5.613,1.750) 6.185971 6.185975 -0.000004 +(12.456,5.599,1.750) 67.554146 67.554145 0.000001 +(0.862,5.599,3.250) 109.754859 109.754845 0.000014 +(7.821,1.563,4.968) NaN 152.772016 NaN +(10.719,-2.707,4.968) 0.007352 0.007353 -0.000001 +(13.618,1.002,4.968) NaN 132.716694 NaN +(15.051,5.743,4.975) 14.902144 14.902145 -0 +(11.102,4.387,4.975) 2.250542 2.25054 0.000002 +(-2.352,6.707,4.975) 31.588133 31.588128 0.000004 +(15.051,7.558,4.975) 16.887604 16.88761 -0.000005 +(6.362,7.268,4.975) 21.768141 21.768143 -0.000001 +(4.905,-2.707,4.975) 0.012389 0.012387 0.000002 +(3.449,7.268,4.975) 22.092446 22.092443 0.000002 +(9.267,7.268,4.975) 21.985391 21.985394 -0.000004 +(-5.019,1.249,4.975) 36.479618 36.479618 0 +(-5.012,3.341,4.975) 3.158507 3.158507 -0 +(-6.261,4.357,4.975) 2.407617 2.407617 -0 +(-4.979,6.013,4.975) 42.220339 42.22034 -0.000001 +(-0.522,4.376,4.975) 2.347651 2.347651 0 +(2.007,1.563,4.975) 133.58463 133.584653 -0.000022 +(5.302,4.387,4.975) 2.357952 2.357951 0.000001 +(12.159,6.705,4.975) 31.242936 31.242939 -0.000003 +(11.102,4.389,7.962) 2.244064 2.244068 -0.000004 +(5.302,4.384,7.962) 2.326787 2.326787 0 +(-0.522,4.376,7.962) 2.328087 2.328087 -0 +(2.007,0.488,7.962) 95.170116 95.170109 0.000008 +(-2.352,6.707,7.962) 31.32489 31.324891 -0.000001 +(7.821,1.002,7.962) NaN 113.341951 NaN +(12.159,6.705,7.963) 30.966147 30.966138 0.000009 +(15.051,6.707,7.963) 31.524703 31.524702 0.000001 +(-2.142,2.150,7.963) 27.619594 27.619598 -0.000005 +(0.540,6.193,7.963) 37.670318 37.670318 -0 +(3.449,7.268,7.963) 21.908339 21.908338 0.000001 +(7.821,6.707,7.963) 62.761867 62.761859 0.000007 +(11.605,4.036,7.963) 6.675359 6.675354 0.000005 +(-5.011,5.851,7.963) NaN 44.885744 NaN +(10.018,1.660,7.963) 18.765156 18.765161 -0.000005 +(13.618,0.148,7.963) NaN 88.698021 NaN +(-3.771,0.146,7.963) 88.542601 88.542597 0.000004 +(-6.261,4.357,7.963) 2.387552 2.387552 0 +(2.196,3.517,7.963) 40.432519 40.432519 -0 +(12.853,4.891,7.963) 36.17268 36.172689 -0.00001 diff --git a/documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-before-fix.tsv b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-before-fix.tsv new file mode 100644 index 000000000..a0ccd942a --- /dev/null +++ b/documentation/evidence/triangle3d-reverse-q4/volumes-pr84fix-R-before-fix.tsv @@ -0,0 +1,60 @@ +space Shell.Volume independent delta +(-4.151,0.953,1.750) 217.020229 217.020228 0.000001 +(15.880,5.743,1.750) 7.399231 7.399232 -0.000001 +(15.880,3.783,1.750) 9.636162 9.636162 0 +(-5.031,2.115,1.750) 12.33198 12.33198 -0 +(4.289,3.779,1.750) 9.775824 9.775821 0.000003 +(6.560,2.115,1.750) 12.62248 12.622481 -0.000001 +(-1.524,3.783,1.750) 9.499276 9.499277 -0.000001 +(-1.524,5.615,1.750) 6.211036 6.211034 0.000002 +(0.735,2.115,1.750) 12.558777 12.558775 0.000002 +(4.289,5.624,1.750) 6.190207 6.190207 -0 +(6.666,5.599,1.750) 68.460367 68.460366 0.000001 +(10.108,3.782,1.750) 9.444464 9.444462 0.000002 +(-4.893,5.599,1.750) 67.693723 67.693712 0.000011 +(13.541,0.953,1.750) NaN 206.206901 NaN +(1.787,0.953,1.750) 208.047704 208.047704 -0 +(7.691,0.953,1.750) NaN 207.521772 NaN +(12.345,2.115,1.750) 12.426243 12.426243 0 +(10.108,5.613,1.750) 6.185971 6.185975 -0.000004 +(12.456,5.599,1.750) 67.554146 67.554145 0.000001 +(0.862,5.599,3.250) 109.754859 109.754845 0.000014 +(7.821,1.563,4.968) NaN 152.772016 NaN +(10.719,-2.707,4.968) 0.007352 0.007353 -0.000001 +(13.618,1.002,4.968) NaN 132.716694 NaN +(15.051,5.743,4.975) 14.902144 14.902145 -0 +(11.102,4.387,4.975) 2.250542 2.25054 0.000002 +(-2.352,6.707,4.975) 31.588133 31.588128 0.000004 +(15.051,7.558,4.975) 16.887604 16.88761 -0.000005 +(6.362,7.268,4.975) 21.768141 21.768143 -0.000001 +(4.905,-2.707,4.975) 0.012389 0.012387 0.000002 +(3.449,7.268,4.975) 22.092446 22.092443 0.000002 +(9.267,7.268,4.975) 21.985391 21.985394 -0.000004 +(-5.019,1.249,4.975) 36.479618 36.479618 0 +(-5.012,3.341,4.975) 3.158507 3.158507 -0 +(-6.261,4.357,4.975) 2.407617 2.407617 -0 +(-4.979,6.013,4.975) 42.220339 42.22034 -0.000001 +(-0.522,4.376,4.975) 2.347651 2.347651 0 +(2.007,1.563,4.975) 133.58463 133.584653 -0.000022 +(5.302,4.387,4.975) 2.357952 2.357951 0.000001 +(12.159,6.705,4.975) 31.23669 31.242939 -0.006249 +(11.102,4.389,7.962) 2.244064 2.244068 -0.000004 +(5.302,4.384,7.962) 2.326787 2.326787 0 +(-0.522,4.376,7.962) 2.328087 2.328087 -0 +(2.007,0.488,7.962) 95.170116 95.170109 0.000008 +(-2.352,6.707,7.962) 31.32489 31.324891 -0.000001 +(7.821,1.002,7.962) NaN 113.341951 NaN +(12.159,6.705,7.963) 30.966147 30.966138 0.000009 +(15.051,6.707,7.963) 31.524703 31.524702 0.000001 +(-2.142,2.150,7.963) 27.619594 27.619598 -0.000005 +(0.540,6.193,7.963) 37.670318 37.670318 -0 +(3.449,7.268,7.963) 21.908339 21.908338 0.000001 +(7.821,6.707,7.963) 62.761867 62.761859 0.000007 +(11.605,4.036,7.963) 6.675359 6.675354 0.000005 +(-5.011,5.851,7.963) NaN 44.885744 NaN +(10.018,1.660,7.963) 18.765156 18.765161 -0.000005 +(13.618,0.148,7.963) NaN 88.698021 NaN +(-3.771,0.146,7.963) 88.542601 88.542597 0.000004 +(-6.261,4.357,7.963) 2.387552 2.387552 0 +(2.196,3.517,7.963) 40.432519 40.432519 -0 +(12.853,4.891,7.963) 36.178925 36.172689 0.006236 From d66e2e5b28484b0dd13c0ef779f93597ba23360d Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Tue, 27 Oct 2026 21:12:56 +0100 Subject: [PATCH 3/3] docs: Triangle3D.Reverse record - baseline second-cell volume after fix is unchanged (review) Generated by Michal Dengusiak and CodeClaude Co-Authored-By: Claude Sonnet 5.5 --- documentation/Triangle3D-Reverse-Q4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/Triangle3D-Reverse-Q4.md b/documentation/Triangle3D-Reverse-Q4.md index 1726d7bcd..27008ec70 100644 --- a/documentation/Triangle3D-Reverse-Q4.md +++ b/documentation/Triangle3D-Reverse-Q4.md @@ -90,7 +90,7 @@ against the pre-fix `SAM.Geometry.dll` (`558D2B86`) and the fixed one (`EF2B5D4E | --- | --- | ---: | ---: | ---: | | (12.159, 6.705, 4.975) | baseline | 31.242936 | 31.242936 | 31.242939 | | (12.159, 6.705, 4.975) | corrected | **31.23669** | **31.242936** | 31.242939 | -| (12.853, 4.891, 7.963) | baseline | 36.172684 | 36.17268 | 36.17269 | +| (12.853, 4.891, 7.963) | baseline | 36.172684 | 36.172684 | 36.17269 | | (12.853, 4.891, 7.963) | corrected | **36.178925** | **36.17268** | 36.172689 | - Before the fix the probe reproduces the saved defect exactly (31.23669 and 36.178925, the ±0.006 m3 of the R1