Project: TessellationAndVoxelizationGeometryLibrary.csproj (net10.0, 161 C# files, ~3.8 MB of source)
Date: 2026-07-12 (analysis of master @ f18113e5)
Method: Every non-vendored source file was read and reviewed. The vendored Clipper2 code (Polygon Operations\Clipper\github.com.AngusJohnson.Clipper2\) was reviewed only for integration, not internals. Findings are grouped into (1) potential errors, (2) redundancies, (3) inefficiencies, (4) other concerns, followed by fix plans, a parallelization strategy, a Span/modern-.NET strategy, and API-design recommendations. Full per-subsystem findings with file:line citations are in Appendices A–H — this body is the synthesis and roadmap. No code was changed.
The library is functionally rich and algorithmically ambitious, but the review found roughly 60 high-severity correctness bugs that were verified by reading the code (not speculation), plus a long tail of medium/low issues. The dominant patterns are not exotic — they are the classic failure modes of a large, long-lived, single-team codebase:
- Copy-paste divergence. Nearly every high-severity bug family comes from duplicated code drifting apart:
Vector2.IsAligneddoesn't normalize whileVector3's does; the X/Y slicers lack the bounds guard the Z slicer has; the dense voxel row'sSubtractcallsUnionwhile the sparse one is correct; three axis-permuted marching-cubes seed functions disagree about which cells to enqueue; the 3MF/AMF/STL/PLY/OFF writers share the same invertedWhere(string.IsNullOrWhiteSpace)comment filter. - Silent failure. Bare
catch { }in mesh repair and triangulation, public methods whose bodies are empty or commented out (Create2DMedialAxis, the fourMinkowski*voxel methods,CrossSectionSolid.Reverse),NotImplementedExceptionbehind public API (Save(OBJ),VoxelizedSolid.Transform,CalculateInertiaTensor), and lazily-deferred LINQ whose side effects are thrown away (SimplifyMinLengthToNewPolygonsreturns unsimplified copies). A caller cannot distinguish "worked" from "quietly did nothing" in dozens of places. - Global mutable state.
Vector3.Zero/One/UnitX...are mutable public static fields (andoperator -is implemented asZero - value, so corruptingZerobreaks negation library-wide);TessellatedSolidBuildOptions.Defaultis a shared mutable singleton that library code writes to;Presenter/Logform an unsynchronized static service locator whose logger factory is disposed before use. - Infrastructure bugs under everything.
UpdatablePriorityQueue.Removecan violate the heap invariant (it only sifts down) — andUpdatePriority, used throughout polygon and tessellation simplification, is built on it. The KD-tree hard-codesDimensions = 3even for 2D points, so 2D nearest-neighbor queries use the metricdx² + 2dy². Comparators violate theIComparercontract in at least five places. - Culture-sensitive IO. ASCII STL, OFF, and PLY parse and write doubles with the current culture: on a German or French machine TVGL writes mesh files no tool can read, and can't read valid files. Compounded by
File.OpenWrite(never truncates → trailing garbage when re-saving smaller files), a binary STL header that can be shorter than 80 bytes, and swapped R/B color channels between the STL reader and writer.
On the performance side, the biggest wins are not micro-optimizations but structural: O(n²) array-copy mutation loops in mesh repair, per-node heap allocations in KD-tree search, full-file regex scans in the STL reader, per-voxel virtual+locked indexer calls in voxel sweeps, and an entire Parallel.For (the most expensive step of voxelization) that is commented out. Span<T>, ArrayPool, BitOperations, and Parallel.For each have specific, high-leverage homes detailed in §7 and §8.
Finally, the shipped build configuration makes ~700+ lines of native polygon boolean code dead (all three configurations define CLIPPER), yet one unguarded overload still routes into the dead-and-buggy native path. Committing to Clipper2 (or fixing and testing the native path) is a decision that unblocks a lot of deletion.
| # | Bug | Location | Why it matters |
|---|---|---|---|
| 1 | UpdatablePriorityQueue.Remove breaks the min-heap invariant (sift-down only); DequeueEnqueue/EnqueueDequeue leak index entries |
UpdatablePriorityQueue.cs:566-584, 278-406 |
UpdatePriority = Remove+Enqueue underpins all polygon & mesh simplification |
| 2 | Simplify(ts, minLength) ignores minLength; loop runs until the queue is empty (collapses the whole mesh); companion MergeVertexAndKill3EdgesAnd2Faces never assigns its keepEdge2 out-param → NRE on first successful merge |
SimplifyTessellation.cs:132-173; ModifyTessellation.AddRemoveElements.cs:119-143 |
Public mesh-simplification entry point is doubly broken |
| 3 | KD-tree always built with Dimensions = 3; 2D queries use metric dx² + 2dy² |
KDTree.cs:26-106 |
Wrong nearest neighbors for all 2D uses, incl. 2D convex hull recovery |
| 4 | Culture-sensitive parse/format in ASCII STL/OFF/PLY (read and write) | IOFunctions.cs:738,762,1121-1249 + writers |
Broken IO on comma-decimal locales, both directions |
| 5 | File.OpenWrite never truncates → re-saved smaller files keep old trailing bytes |
IOFunctions.cs:1270,1351,1435 |
Silent file corruption on every overwrite-save |
| 6 | All ...ToNewPolygons simplify/complexify methods return unmodified copies (deferred LINQ re-enumeration) |
PolygonOperations.Simplify.cs:44-49,143-148,293-298,696-701,1124-1129 |
Public API family silently does nothing |
| 7 | AddEdges gives every batch-added edge the same IndexInList |
TessellatedSolid.cs:1290 |
Corrupts index-based edge removal, serialization, checksums |
| 8 | Duplicate-face checksum multiplier overflows int at ≥ 46,341 vertices |
TessellatedSolid.cs:806 |
Silently drops faces on large meshes |
| 9 | TessellatedSolid.Copy() throws for any solid without primitives; faces+vertices ctor runs repair with SameTolerance == 0 |
TessellatedSolid.cs:1474, 729-730 |
Copy/slice outputs get no working repair; Copy crashes |
| 10 | VoxelRowDense.Subtract calls Union; Intersect with sparse operand is a no-op |
VoxelRowDense.cs:271-276, 253-259 |
Wrong CSG at the row level (currently masked by forced-sparse conversion) |
| 11 | BooleanOperation(PrimitiveSurface,…) drops the trailing range (odd crossing counts) |
VoxelizedSolid.Advanced.cs:126-150 |
Subtract(plane) can be a no-op; Intersect wrong outside bbox |
| 12 | Marching-cubes-on-layers ring buffer off-by-one: every cube reads a stale top layer and caches it | MarchingCubesCrossSectionSolid.cs:141-148, 576-581 |
Systematically wrong surfaces from cross-section solids |
| 13 | BoundingBox family: Bounds, Center, Copy(), MoveFaceOutward, TransformFromUnitBox all use unit Directions where dimension-scaled Vectors are required |
BoundingBox.cs:193, 364, 549-576, 255-264 |
Almost every derived quantity of a non-unit OBB is wrong |
| 14 | GeneralQuadric.Transform conjugates with M instead of M⁻¹; sphere→quadric constant-term typo (Z + Z for Z·Z) |
GeneralQuadric.cs:252, 882 |
Transformed/converted quadrics are wrong surfaces |
| 15 | PolynomialSolve: inverted MoveNext() guards (throws on valid input); complex Quadratic takes √(−disc) |
PolynomialSolve.cs:102-106, 164-168, 181-188, 308-317 |
Root-finding wrong/unusable via the enumerable API; feeds eigen/quartic paths |
| 16 | Matrix4x4.Decompose double-negates (-x.Negate()) — wrong rotation for reflective matrices; determinantBig permutation-sign rule wrong |
Matrix4x4.cs:1483; inversion transpose.cs:675 |
Core numerics silently wrong on pivoted/reflective inputs |
| 17 | Rotating calipers uses the previous rectangle's directions/offsets for side points | MinimumEnclosure.cs:678-684 |
Minimum-bounding-box side-point output wrong |
| 18 | ConvexHull3D/4D.Create out vertexIndices returns [0..n-1] (all inputs) |
ConvexHullAlgorithm.3D.cs:44; .4D.cs:66 |
The advertised hull→input index mapping doesn't work |
| 19 | Binary STL: header can be < 80 bytes; R/B color bits swapped reader-vs-writer; inverted comment filter drops all metadata in 6 writers | STLFileData.cs:406-408, 294-313 vs 446-452; 6 sites |
Corrupt/incorrect files in the most-used format |
| 20 | CrossSectionSolid.Copy() wipes the source solid's layers then throws NRE |
CrossSectionSolid.cs:338 |
Copy destroys the original object |
The ~200 individual findings (Appendices A–H, §1 of each) cluster into recurring mechanisms. Knowing the mechanism tells you where the next bug of the same kind is hiding:
- Assign-to-the-wrong-thing: out-param assigned instead of local (
ARE:119),this.Layer2Dinstead ofsolid.Layer2D(CrossSectionSolid.cs:338),radii.Insert(0,…)instead ofInsert(i,…),bottomPoints.Addinstead oftopPoints.Add,Vertices[...]instead ofp.Vertices[...]inCalculateCentroid. - Boolean-logic inversions:
if (enumerator.MoveNext()) throw(PolynomialSolve),Where(string.IsNullOrWhiteSpace)(six IO writers),!= A || != Balways-true (IsInside.cs:986),&&where||needed (EltMultiplydimension checks, Poisson-disk rejection),if (skip && File.Exists(...)) continue(SolidAssembly). - Deferred-LINQ side effects: the
ToNewPolygonsfamily;SolidAssembly.AllPartsInGlobalCoordinateSystem;ConvexHull3D.LineIntersectionre-sorting. - Empty statements as intended guards:
if (fromNode == intersectNode) ;(Arrangement),if (double.IsNaN(d)) ;(PrimitiveSurface),;//this doesn't work IntersectRange(...)(VoxelRowDense), empty wrap-anomaly branches in CylindricalZBuffer. - Contract violations in comparers and hash/equals:
SortByIndexInList,NoEqualSort,EdgeComparer/NodeComparer(both orderings return 1),TwoDSortXFirst/YFirst,SphericalAnglePair(tolerant Equals vs quantized GetHashCode),Vector.NullNaN sentinel breakingEqualsreflexivity, additiveGetHashCodein Quaternion/Matrix3x3/Matrix4x4. - Integer overflow at scale: face checksum (
TS:806, ≥46,341 vertices), 4D hull(long)(n*n)(>46,340points), voxel center accumulators (intfor 1000³ grids),numVoxelsY * numVoxelsZproducts. - Order-of-operations / stale caches: repair before tolerance is defined (
TS:729),Prismaticctor usingVerticesbefore they exist,startDistanceAlongDirectioncomputed beforestepSize(Slice:905-908), lazy caches (Torustransforms,Polygon.Path,SurfaceGroup.Faces, primitivefaceXDir) never invalidated by property setters orTransform.
Recommended verification approach: nearly every finding above is unit-testable in isolation (a 5-line repro). Before fixing, encode the top items as failing tests in Testing\ — several fixes (heap, checksum, calipers) risk regressions if done blind.
| Item | Location | Notes |
|---|---|---|
MinimumCircleCylinderOld.cs |
Enclosure Operations (23 KB) | Namespace TVGL.Test, zero references, ships in the release assembly, contains a known bug the new file fixed |
GaussianSphere.cs |
Enclosure Operations (31 KB) | Zero references anywhere |
MakeEdgePathsTooNew + MakeStrandsFromEdgePaths |
MakeEdgePaths.cs:111-272 |
~160 lines, no callers, missing a guard the live copy has |
Create2DMedialAxis |
MedialAxis2D.cs:42-281 |
240-line commented body; public method returns [] |
SingularValueDecomposition |
StarMathLib\svd.cs |
Internal, no callers, and broken (returns zeros) |
CreateDistanceGridBruteForce |
MarchingCubesCrossSectionSolid.cs:195-251 |
No callers |
CalculateVolumeAndCenterOLD, TransformToXYPlaneMaybeBetter, CalculateInertiaTensor (throws), OrderedFacesCCWAtVertexNoEdges (throws) |
MiscFunctions / VolumeCenterMoments | Dead or booby-trapped public methods |
Minkowski_sum_by_reduced_convolution_2.h |
Polygon Operations (17 KB) | C++ header checked into the C# tree |
Backup\ and Backup1\ folders |
repo root | Version control is the backup |
Dozens of if (false) SSE blocks, commented algorithm alternates, unused usings, unused private helpers |
All subsystems | Itemized per appendix §2 |
- The CLIPPER triplication (
#if CLIPPER / #elif !COMPARE / #else) inBoolean.cs/Offsetting.cs: three bodies per public operation, one of them dead, one test-harness-only. Decision needed (see §9.4). - Axis-cloned algorithms:
AllSlicesAlongX/Y/Z(three 60-line copies with divergent bug fixes), marching-cubesFindZ/Y/XPointFrom…(three 120-line permutations, one already bug-diverged), dense/sparse voxel row ops,CylindricalZBuffervsZBufferrasterizer (~120 lines copy-pasted). - Type-cloned numerics: StarMathLib's 4–8 int/double overload copies of every routine (~70% of two 100 KB files) — collapse with generic math (
INumber<T>, available on net10.0);VertexvsVector3overload pairs in MiscFunctions (75 identical lines inAreaOf3DPolygon); Quaternion's operator/method body copy-pastes. - Three worlds of linear algebra: TVGL structs, ~90 one-line extension wrappers in
Extensions.cs, and StarMathLibIList<double>routines — three names, two namespaces, four error contracts for the same operations. - Six copies of the IO number-reader if-ladder and three copies of the Open dispatch switch (which is exactly how the OFF→PLY misrouting shipped).
- Four point-in-polygon implementations with different boundary semantics; four+ point-to-segment distance routines.
Ranked by expected real-world impact:
- O(n²) mesh mutation.
RemoveVertex/RemoveFace/AddFaceeach reallocate and copy the whole array, andRemoveVertexre-checksums every edge; repair loops call them per-defect (TIR:267-317). Fix: batch mutations (the code already does this inSimplifyTessellation) or moveFaces/VerticestoList<T>with deferred compaction. - KD-tree allocations. Two
HyperRectclones (fourdouble[]) per node visited during search; per-nodeList<double>+ array pair during construction. ICP multiplies this by points × iterations. - ASCII STL parsing. Every line retained in a
List<string>and regex-scanned repeatedly (~7M regex invocations for a 1M-facet file); twoRegexmatches per facet line. Span-based parsing fixes this and the culture bug together. - Binary IO. A
byte[]allocation per scalar plus LINQReverse().ToArray(); 13 allocations per binary-STL face.stackalloc/BinaryPrimitivesreduce this to zero. - Voxel hot loops. Per-voxel virtual+locked indexer calls in
DraftY/Z and the grid enumerator; byte-at-a-time bit counting instead ofBitOperations.PopCountoverulongspans; the voxelizationParallel.Forcommented out. - Polygon caches.
Polygon.InnerPolygonsre-materializes anImmutableArrayper access (used insideArea/Perimeter); O(n²)RemoveAt/Insert(0,…)patterns in arrangement/dedup code; O(n³) restart-the-loop unions in the native path. - Per-face iterator allocation.
TriangleFace.Edgesis a compiler-generated iterator — everyforeachover a face's edges allocates. This is called mesh-wide in many passes; a struct enumerator or exposingAB/BC/CAis a library-wide win. - Reflection in hot paths.
FindBestPlanarCurvere-scans assembly types and invokesCreateFromPointsviaMethodInfo.Invokeper call. - Marching cubes. Value dictionary grows to the whole grid (eviction commented out); three arrays allocated per cube.
- Dijkstra with full path copies per node (
Prismatic), O(n·m) Delaunay2D insertion scans, double sorts, and the long tail itemized in the appendices.
- Thread safety. The library uses
Parallel.Forinternally (voxels) and invites concurrent use, but: lazy unsynchronized caches onEdge/TriangleFace/Solid/Polygon; boolean ops and slicing mutate input geometry (IndexInList,Visitedflags); the sparse voxel row's indexergetis unlocked while every search mutates a sharedlastIndex; statics (Presenter,Logger,EqualityTolerance, build-option singletons) are unsynchronized. Recommendation: declare and document a model — "solids are not thread-safe; read-only concurrent access is safe only afterX" — then remove the per-row locks that cost every voxel indexer call, and stop mutating inputs in queries (biggest offenders:PolygonBooleanBase.Run,Slice,TriangulateSweepLine,Sphere/Cone/Torus.TransformFrom3DTo2D). - Debug scaffolding in production: writes
errorPolygon*.jsonto CWD (Triangulate),cvxpoints.csvto the user's Desktop (MinimumCircle),Console.WriteLinein hulls/ICP/Delaunay,times.csvunder COMPARE. Remove or gate. - Non-determinism: unseeded
new Random()in Poisson-disk points, 4D hull jiggling (→ Delaunay3D), ICP — reproducibility matters precisely on the degenerate inputs where these fire. Accept seeds. - Tolerance zoo:
BaseTolerance1e-9 (absolute),DefaultEqualityTolerance1e-12,PolygonSameTolerance1e-7 (relative),OBBTolerance1e-5, plus dozens of inline literals (0.0001, 1.0001, 0.517, 45720000).Solid.SameToleranceexists but most free functions never consult it. Adopt one policy: tolerances derived from model extent (the 4D hull already does this) flowing through a context/parameter, withConstantsvalues as documented fallbacks. - Security/robustness (IO): AMF deserialization doesn't prohibit DTD processing (billion-laughs DoS);
TypeNameHandling.Autoon polygon save without a binder on read; no decompression-bomb guard on 3MF/TVGLz. All cheap to close. .csproj/ packaging: deprecatedPackageLicenseUrl(usePackageLicenseExpression/PackageLicenseFile); description says ".NET Standard library (and a legacy portable class library)" while targetingnet10.0only;Copyright 2014; all three configurations define identicalTRACE;CLIPPERconstants (so theSeriesconfiguration and the CLIPPER conditionals are vestigial);Version 1.0.07.2026uses a nonstandard scheme.
A phased plan that keeps the library shippable at every step. Effort labels: S < ½ day, M = 1–3 days, L = 1–2 weeks.
Write a failing test first for each; the fixes themselves are small:
UpdatablePriorityQueue:Remove→ compare relocated node with parent,MoveUporMoveDown; remove stale dictionary entries inDequeueEnqueue/EnqueueDequeue; fixEnqueueRangedictionary build. (M — with tests)SimplifyTessellation: honorminLength; fixkeepEdge2local assignment; fix double-decrement budget; populatefacesToAddinSimplifyFlatPatches. (M)TessellatedSolid:AddEdges+ i;(long)NumberOfVertices * NumberOfVertices; compute tolerance before repair in the faces+vertices ctor; guardDefineBordersinCopy()whenPrimitivesis null/empty; replace theReplaceEdge(edge, null)NRE path. (M)- IO: invariant culture everywhere (parse + format);
File.OpenWrite→File.Create; pad binary STL header to 80 bytes; fix R/B bit swap; fix the six inverted comment filters; route OFF correctly in all three dispatchers; fix OBJvalues[3]and droppedggroups; fix binary-PLY unknown-property skip. (M–L, mechanical but wide) - Polygon ops: materialize (
.ToList()) in the fiveToNewPolygonsmethods; fixCalculateCentroid; makeReverse(bool)honor its parameter; fixIsInside.cs:986||→&&; fix the two comparers to return 0 on equality; fixSimplifyMinLengthToNewListsquared-vs-unsquared threshold. (M) - Numerics: PolynomialSolve
!MoveNext()guards andSqrt(+disc);determinantBigsign via transposition count;Decomposesingle negation;GetEigenVector2pivot;EltMultiply/EltDivide||; makeZero/One/Unit*/Nullfieldsreadonly. (M) - Voxel/MC:
VoxelRowDense.Subtract→ real subtract; implement or throw on dense-sparseIntersect; enumerator start state; trailing-range flush in bothBooleanOperations andMinkowskiSubtractOne;MakeFacesInCube(i, j, k-1)ring-buffer fix;CrossSectionSolid.Copy/Reverse/CalculateSurfaceArea; rewriteCalculateCenter(long accumulators, world coords, dense/sparse-consistent sums). (M–L) - Enclosure/PointCloud: KD-tree dimension parameter (infer 2 for
Vector2/Vertex2D); calipers stale-rectangle fix;vertexIndicesfrom hull vertices;(long)n*n; symmetric jiggle; extreme-point tie fix; iteration caps onMinimumSphere/MinimumGaussSpherePlane. (M) - Primitives:
BoundingBox— useVectorsinBounds/Center/Copy/MoveFaceOutward, add scale toTransformFromUnitBox;GeneralQuadric.Transforminverse conjugation + sphereZ*Z;Cone.TransformFrom2DTo3D;Capsulecone-sectiont;Torustranslation inverse;Plane.DotCoordinatesign;SphericalZBuffer.XLength;Get3DPointradius double-count. (L — many small fixes across many files) - Misc:
SkewedLineIntersectionparentheses;GetMinVertexDistanceAlongVector i++;Get3DLineValuesFromUnique−Z branch;radii.Insert(i,…);OutputServiceslogger-factory lifetime; makeEmptyPresenter2Da true no-op. (S–M)
- Delete the dead-code inventory in §2.1 (verify with a solution-wide reference check per item; several were already grep-verified by this review).
- Remove Desktop/CWD file writes and
Console.WriteLines; route everything throughLog. - Fix
.csprojmetadata; remove theSeriesconfiguration or give it a real purpose. - Remove
Backup\/Backup1\from the repository. - Sweep unused usings and
if (false)blocks (an analyzer pass: enableIDE0005,CA1806, etc. as warnings).
- One failure contract per operation family (see §9.1); eliminate
NotImplementedException/empty bodies from the public surface (implement, throwNotSupportedExceptionwith a message, or delete). - Replace silent
catch { }in repair with aTessellationRepairReportreturned/exposed from the build (counts of merged vertices, flipped faces, patched holes, and what failed). - Introduce
SolidTolerances/context threading (§4) so free functions stop mixing absolute constants with model-scale data. - Document (and enforce with tests) the thread-safety model.
Ordered by measured-impact likelihood: mesh-mutation batching → KD-tree allocation removal → IO span parsing → voxel bit ops + re-enabled parallel voxelization → polygon cache fixes → face-edge struct enumerator → marching-cubes slab windowing. Add BenchmarkDotNet baselines before each change; details in §7–§8.
The reorganization items (god-class splits, option objects, naming) are source-breaking; batch them into a major version with [Obsolete] forwarding shims for one release where practical.
Precondition: parallelism is only safe after Phase 2's "queries don't mutate inputs" cleanup — several current bugs (e.g., Slice mutating IndexInList, boolean ops mutating vertices) make otherwise-embarrassingly-parallel work racy.
| Site | Shape | Notes |
|---|---|---|
VoxelizedSolid.FillInFromTessellation (VoxelizedSolid.cs:262) |
per-k rows, disjoint writes | The Parallel.For is already written and commented out — the single most expensive voxelization step |
Marching cubes Generate (MarchingCubes.Base.cs:204) |
per-z-slab | Needs per-slab value caches merged at the end (also fixes the unbounded dictionary) |
MinimumBoundingCylinder(directions) / FindMinimumBoundingBox ChanTan starts |
13 independent trials | Parallel min-reduction; trivially correct |
IO: building TessellatedSolids for independent meshes (3MF objects, multi-solid STL, OBJ groups) |
per-solid | Construction (edge-making, repair) dominates parse time |
Per-face reductions: CalculateVolumeAndCenter, CalculateSurfaceArea, integrity pre-scan, FindNonSmoothEdges, Transform vertex/face loops |
map/reduce with thread-local sums | Meaningful at 10⁵–10⁶ faces; keep a serial path below a threshold (~50k elements) |
ZBuffer rasterization (ZBuffer.cs:116, cylindrical variant) |
per-face with per-thread buffers or Interlocked max-compare |
Faces independent except the max-store |
Polygon batch ops: SimplifyFast over lists, Clipper path conversion, unions of disjoint groups |
per-polygon | Only after Polygon cache thread-safety is fixed |
PrimitiveSurfaceExtensions.Voxelize k-loop |
idempotent true writes |
Scaffolding already present, commented out |
- Remove the triangulation "race" in
TessellationInspectAndRepair.cs:1269-1301(two speculative tasks + a 1-second sleep task, unsynchronized flag,WaitAnyNRE) — replace with sequential try-fallback or a properly cancelled race. - Keep the row-parallel voxel boolean ops, but hoist the per-row LINQ allocations out of the loop and fix the unlocked sparse getter first.
- Don't parallelize small fixed-size numerics (2×2…4×4), per-edge repair steps with topological dependencies, or anything that walks shared mutable topology (edge paths, arrangement graphs).
Concrete, high-leverage applications — all available on net10.0:
- Text parsing (STL/OBJ/OFF/PLY ASCII): read lines into
ReadOnlySpan<char>, tokenize withMemoryExtensions.Split, parse withdouble.TryParse(span, NumberStyles.Float, CultureInfo.InvariantCulture, …). Kills the per-linestring.Splitarrays, the two-regex-per-facet cost, and the culture bug in one refactor. - Binary IO:
stackalloc byte[50]per STL facet record (or one pooled buffer),BinaryPrimitives.ReadSingleLittleEndian/WriteSingleLittleEndian— removes ~13 allocations/face and the LINQReverse().ToArray(); makes endianness explicit. - Voxel rows:
MemoryMarshal.Cast<byte, ulong>(values)+BitOperations.PopCount/TrailingZeroCountforCount, position sums, and sparse-index extraction (~8× fewer iterations, branch-free); Span-based union/intersect/subtract kernels on wholeulongwords. - Small fixed buffers →
stackalloc/inline arrays: GJK simplex (Vector3[4],bool[3]),Matrix4x4.Decomposebasis arrays, per-face index triples inMakeVertices/MakeFaces,PointCommonToThreePlanes' 3×3 solve,BoundingBox.LineIntersection's 6-plane scratch. ArrayPool<T>: marching-cubes per-layer distance grids (only 2–4 alive at once),RemoveVertices/Faces/Edgesintermediates, KD-tree construction working arrays, LU/SVD/Cholesky scratch in StarMathLib.CollectionsMarshal.AsSpan(list)for linear compaction where the code currently does O(n²)List.RemoveAtloops (MakeVerticesFromPath,RemoveCollinearEdgesDestructiveList).- API-level spans: StarMathLib overloads taking
ReadOnlySpan<double>instead ofIList<double>(removes interface dispatch per element and enables JIT vectorization, replacing the "trust-me length" overloads);Vector3.Coordinates→ exposeToArray()or a span view instead of allocating per property-get. - SIMD (adjacent to Span):
Matrix4x4rows are exactly oneVector256<double>— operators,Transpose,Lerp,FrobeniusNormare mechanical AVX conversions (the abandonedif (false)SSE scaffolding shows intent);System.Numerics.Tensors.TensorPrimitivesfor StarMathLib dot/add kernels.
Today the same conceptual operation fails four+ different ways (null return, bool + out, sentinel Vector3.Null/NaN matrix, exception from deep inside, silent no-op). Pick per family and enforce:
- Queries/fits/solves:
bool TryX(..., out result)— already the style ofConvexHull3D.Create; extend toPlane/Sphere/...TryFit(points, out surface, out error),Matrix.TryInvert,TrySolve. - IO: one
Open/Savepipeline through a single dispatcher, returning a small result object (IOResult { Solids, Warnings, FailureReason }) instead of the current three contracts; magic-byte sniffing so mislabeled files still open; aCanSave(FileType)support matrix; never advertise formats that throwNotImplementedException. - No public no-ops: every empty/commented/
NotImplementedExceptionpublic member either works, throwsNotSupportedExceptionwith an explanatory message, or is deleted.
Vector2/3/4statics →static readonlyfields (one-line fixes with outsized safety impact).TessellatedSolidBuildOptions→ immutable record (with-based customization); library code must not write to caller-supplied options;Default/Minimalbecome get-only fresh instances.- Logging/presentation: accept an
ILoggerFactory(the packages are already referenced) and makePresenteran injected interface; keep the static façade as a thin default for scripts, but fix its lifetime bugs and makeLogpublic so hosts can route it.
- Split
MiscFunctions(~150 public methods across 3 partial files) into themed static classes — a concrete table is in Appendix H §5 (DirectionalSortExtensions,PlanarProjection,AngleFunctions,IntersectionFunctions,ProximityFunctions,ContainmentFunctions,MeshTopologyFunctions,VolumeAndMoments, plus aUnique3DLinereadonly struct). - Split
PrimitiveSurfaceExtensions(65 KB) into border queries / tessellation factories / trimming / voxelization; promoteGetAxis/GetAnchor/GetRadiusto virtual members ofPrimitiveSurfaceso new primitives can't be forgotten. - Consider sub-namespaces (
TVGL.Polygons,TVGL.Voxels,TVGL.Numerics,TVGL.IO) so IntelliSense onTVGL.isn't a 200-type flood; make StarMathLibinternalexcept the genuinely-nD pieces (LU/eigen), funneling users to the struct types.
All shipped configurations define CLIPPER; the native boolean path is dead yet partially reachable through one unguarded overload, and it carries known bugs. Either: (a) commit to Clipper2 — delete the native path and the #if triplication, keep RemoveSelfIntersections if still needed (port it onto Clipper primitives); or (b) make the choice a runtime strategy (PolygonBooleans.Engine = Native|Clipper) and bring the native path up to tested parity. (a) is less work and removes ~1,000 lines; the compile-time define should go away in either case.
TessellatedSolid: replace the four constructors × seven trailing parameters with a builder orTessellatedSolidDescriptionrecord; makenumOfFaces:-1/vertices:nullconventions explicit factory methods (FromFaceVertexLists,FromTriangleSoup).- Separate semantics by name, not parameter type:
SimplifyToFaceCount(int)vsSimplifyByMinEdgeLength(double)(todaySimplify(ts, 10)silently means face-count); same forComplexify. - Name the mutation convention and apply it everywhere:
Transform(in place) vsTransformToNew*already exists — extend to the simplify family and fix the brokenToNew*copies; document whatCopy()deep- vs shallow-copies onSolidand stopCopyElementsPassedToConstructor:falsefrom destructively re-indexing the source solid. - Primitive surfaces: freeze geometry after construction (
initsetters) or make every setter invalidate dependent caches; make query methods (TransformFrom3DTo2D) side-effect-free; document a single signed-distance convention ("negative = inside material") onPrimitiveSurface.DistanceToPointand make all subclasses comply. - Small foot-gun fixes:
Circle.FromRadius/FromRadiusSquaredfactories;Vector4.FromPoint/FromDirectionfor the W convention; rename ConeAperture→ApertureSlope(or provideHalfAngle); renameConvexHull4D.Faces→Tetrahedra?/Edge4D→Triangle4D; fixnumVoxelsXcasing.
- Golden-file IO round-trip tests per format, run under
de-DEas well as invariant culture. - Property-based tests for geometry predicates (point-in-polygon boundary cases, hull of degenerate inputs, slicer at vertex-coincident planes).
- A behavioral test comparing
UpdatablePriorityQueueagainstPriorityQueue<TElement,TPriority>. - XML-doc sweep: the appendices list dozens of copy-paste doc errors ("single precision" on doubles, wrong parameter names, placeholder
<c>true</c> if XXXX); fix alongside each touched file rather than as a big bang.
The appendices that follow contain the complete per-subsystem findings with file:line citations, severity ratings, and per-area fix lists.
Scope: all 19 files under Numerics\. Line numbers refer to current working-tree state.
-
PolynomialSolve— invertedMoveNext()guards make the enumerable-based solvers throw on valid input (and silently accept empty input).Numerics\PolynomialSolve.cs:102-106(QuadraticAsTuple),:181-188(Cubic(IEnumerable)),:308-317(Quartic(IEnumerable)):if (enumerator.MoveNext()) throw new ArgumentException("Missing coefficients to solve quadratic."); var squaredCoeff = enumerator.Current;
MoveNext()returnstruewhen an element exists, so any correctly-sized coefficient list throws "Missing coefficients", while an empty list sails through readingdefaultfromCurrent. Every guard should beif (!enumerator.MoveNext()) throw .... This also breaksGetRoots(PolynomialSolve.cs:55-65) for the 3/4/5-coefficient paths (deferred until enumeration because these are iterators). -
PolynomialSolve.Quadratic(ComplexNumber, ComplexNumber)negates the discriminant before the square root.Numerics\PolynomialSolve.cs:164-168:var radicalTerm = linearCoeff * linearCoeff - 4 * constant; radicalTerm = ComplexNumber.Sqrt(-radicalTerm);
The roots of
x² + bx + care(-b ± √(b²−4c))/2; taking√(−(b²−4c))multiplies the radical byi, giving wrong roots whenever the discriminant is not a negative real. This feedsCubic(ComplexNumber, …)(PolynomialSolve.cs:266), which is used byQuartic(ComplexNumber, …)and eigenvector paths. -
StarMath.SingularValueDecompositionreturns an all-zeros array of singular values.Numerics\StarMathLib\svd.cs:104declaresvar s = new double[sLength];— the algorithm computes everything intostemp(e.g.:131,:296-320,:519-536),sis never written, and:573doesreturn s;. All four public-facing overloads (svd.cs:35,48,58,75) therefore return zeros. (Mitigating: the methods areinternaland appear to have no callers — see Redundancies #4 — but as written the API is broken.) Additionally the SVD mutates the input matrixAin place (svd.cs:138-139,157,207) with no doc warning, unlikeLUDecompositionwhich clones. -
StarMath.determinantBig(double[,])computes the permutation sign incorrectly.Numerics\StarMathLib\inversion transpose.cs:675:if (permute[i] != i) result *= -1;
The determinant sign is
(−1)^(number of row transpositions), not(−1)^(count of displaced indices). Example: permutation(1,2,0)(a 3-cycle, even, sign +1) flips the sign three times → wrong sign. Any ≥4×4 determinant that required pivoting can come back with the wrong sign. Worse, theint[,]twindeterminantBig(inversion transpose.cs:714-723) applies no sign correction at all, so the two overloads disagree even with each other. It also truncates via(int)result(:722), which overflows/truncates for large determinants. -
Matrix4x4.Decomposedouble-negates the basis vector for left-handed matrices.Numerics\Matrix4x4.cs:1483:pVectorBasis[a] = -pVectorBasis[a].Negate();
.Negate()(extension,Extensions.cs:464) already negates; the unary-negates it back, so the basis vector is left unchanged whilescaleanddetare flipped (:1479-1484). The reference implementation does*pVectorBasis[a] = -(*pVectorBasis[a]). Result:Decomposereturns an incorrect rotation quaternion for any matrix containing a reflection/negative scale. -
StarMath.GetEigenVector2picks the smaller pivot — NaN eigenvectors for diagonal/triangular matrices.Numerics\StarMathLib\eigen.cs:856-872:if (y11.LengthSquared() > y22.LengthSquared()) { var f = -x21 / y22; ... } // divides by the SMALLER of the two else { var f = -x12 / y11; ... }
The condition is inverted: when
|y11| > |y22|you should divide byy11(second-row formula divides byy22, first-row formula byy11). Concretely forA = diag(2,1), λ=2 givesy11=0,y22=−1; the code takes theelsebranch and computes−x12/y11 = 0/0→ NaN eigenvector, when the correct answer(1,0)is available from the branch it skipped. -
StarMath.EltMultiply/EltDivide2-D dimension checks use&&instead of||.Numerics\StarMathLib\multiply divide.cs:1578, 1592, 1606, 1621(EltMultiply) and:1809, 1823, 1837, 1852(EltDivide):if (A.GetLength(0) != B.GetLength(0) && A.GetLength(1) != B.GetLength(1)) throw ...
A mismatch in only one dimension passes the check and then either throws
IndexOutOfRangeExceptiondeep in the loop or silently produces a wrong-shaped result (the result is even sized with mixed dims:A.GetLength(0), B.GetLength(1)).
-
Vector2.TransformNoTranslatedivides by zero for typical projective matrices.Numerics\Vector2.cs:689(factor = 1 / (position.X*M13 + position.Y*M23)) and:712(Matrix4x4 variant:1 / (X*M14 + Y*M24)). A matrix flagged projective solely becauseM33/M44≠ 1 hasM13=M23=0(orM14=M24=0), makingfactorinfinite → result(±∞ or NaN). The homogeneous term (M33/M44) is dropped from the denominator; compareVector2.Transformat:644which correctly includesmatrix.M33. -
Matrix4x4.IsNull()never checks the fourth row for NaN.Numerics\Matrix4x4.cs:189-192testsM11…M34for NaN but omitsM41, M42, M43, M44. A matrix that is NaN only in the translation row (a common partial-failure mode) reportsIsNull() == false. The zero-check part (:193-197) does include row 4, making the omission clearly accidental. -
Matrix4x4 * scalarandMatrix3x3 * scalardo not scale the homogeneous column for affine matrices.Numerics\Matrix4x4.cs:1982-1987: the non-projective branch funnels through the 12-argument constructor, which hard-setsM44 = 1(:327), so(2.0 * Matrix4x4.Identity).M44 == 1, not 2 — i.e. the returned matrix is nots·M. Same forMatrix3x3.Multiply(value1, double)atNumerics\Matrix3x3.cs:607-612(M33stays 1). This silently breaks algebraic identities like(sA)·x == s(A·x)andLerpbuilt from scalar ops;Negateon the other hand does produce−M44(via the 16-arg ctor), so scalar−1 * M ≠ -M. If the renormalization is intentional it needs prominent doc; as-is it's a correctness trap. -
StarMath.solveViaCramersRule3returnstruewith infinite answers for singular systems.Numerics\StarMathLib\solve.cs:62-97:oneOverDeterminant = 1 / Determinant(a)with no zero check; whendet == 0and the numerator ≠ 0 the result is±Infinity, which passes thedouble.IsNaN(x)guards (:69, :81, :92) and is returned as a "successful" solve. The 2×2 twin checksdenominator == 0properly (solve.cs:169). -
StarMath.ExpMatrix— Padé loop is one iteration short and the scaling is off by one.Numerics\StarMathLib\multiply divide.cs:2080for (int k = 1; k < q; k++)runs k=1..5; the MATLAB algorithm it cites (expmdemo1, comment at:2057) runsk = 1:q(6 iterations), so the 6th-order Padé term is dropped. Also:2069var s = Math.Max(0, exponent)versus the quoteds = max(0, e + 1)(:2070comment) — the scaled matrix has norm in [0.5, 1) instead of [0.25, 0.5), degrading accuracy of the fixed-order Padé approximation. -
StarMath.RemoveColumnbounds check tests againstnumRowsinstead ofnumCols.Numerics\StarMathLib\make extract.cs:601and:628:if ((colIndex < 0) || (colIndex >= numRows)) // should be numCols
For a wide matrix (cols > rows) valid high column indices throw; for a tall matrix invalid indices pass the check and crash later with
IndexOutOfRangeException. Classic copy-paste fromRemoveRow. -
Vector3.Slerpdivides by zero for parallel/antiparallel inputs.Numerics\Vector3.cs:768-781:sinOmega = Math.Sqrt(1 - dot*dot); whendot = ±1,oneOverSinOmega = 1/0 = ∞and the result is∞·0 = NaN.Quaternion.Slerp(Quaternion.cs:342-347) handles this with a lerp fallback;Vector3.Slerphas no such guard. -
ComplexNumber.ToString()— missingreturnmakes the real-number branch dead.Numerics\ComplexNumber.cs:443:if (IsRealNumber) string.Format(CultureInfo.CurrentCulture, "{0}", Real);
The formatted string is discarded (statement has no effect); purely-real numbers always print as
"x + 0i"/"x - 0i". -
Matrix3x3.ToString()format string is wrong.Numerics\Matrix3x3.cs:744:"{{M11:{0} M12:{1} M12:{2}}} {{M21:{4} M22:{4} M23:{5}}} ..."— the third label should beM13,M21uses index{4}(prints M22's value) instead of{3}, so M21's value is never printed and M22's is printed twice. -
Vector2.IsAlignedOrReverse/IsAlignedcompare a raw dot product against a cosine tolerance without normalizing.Numerics\Vector2.cs:474, 500, 512usethis.Dot(other) >= dotTolerance(DotToleranceForSame ≈ cos(1°),Constants.cs:117). For non-unit vectors this is meaningless (two 0.1-length aligned vectors give dot 0.01 → "not aligned"; two long nearly-perpendicular vectors can exceed the threshold). The Vector3 versions normalize first (Vector3.cs:517, 540, 555) — clear copy-paste divergence. Same problem inVector2.IsPerpendicular(:523) vsVector3.IsPerpendicular(:566). -
StarMath.IsSingularcan throw instead of returningtrue.Numerics\StarMathLib\inversion transpose.cs:567ends withA.Determinant().IsNegligible(), butdeterminantBig→LUDecomposition→findAndPivotRowsthrowsArithmeticExceptionfor singular matrices (inversion transpose.cs:294-296). So the very matricesIsSingularexists to detect can crash it (only the NaN path at:672is caught). -
StarMath.GetEigenVector3/GetEigenVector4returnnullfor degenerate eigenspaces.Numerics\StarMathLib\eigen.cs:829and:776: when all candidate 2×2/3×3 subsystems are singular (e.g. eigenvectors of the identity matrix, or any repeated eigenvalue with eigenspace dimension > 1), the method returnsnullwith no documentation;GetEigenVectors3/4then hand callers arrays containingnullentries (eigen.cs:798-802, 728-733).
-
Vector2/3/4NaN-Nullsentinel breaksEqualsreflexivity.Vector2.Null.Equals(Vector2.Null)isfalsebecauseEqualsuses==on doubles (Vector2.cs:171-174,Vector3.cs:158-163,Vector4.cs:818-821). SinceNullis explicitly promoted as "used in place of null" (Vector2.cs:28-31), aDictionary<Vector3,…>orContainstest on Null vectors silently fails. -
PolynomialSolve.Quartic(double,…)NaN for tiny-negative discriminants.PolynomialSolve.cs:400-402:radicalTermnegligible-but-negative (e.g. −1e−20) falls into theelseandMath.Sqrt(radicalTerm)→ NaN. AlsoQ5/Q7can be zero for degenerate quartics (four equal roots), producingQ1/Q5andQ3/Q7division by zero (:403, :410). -
Quaternion.Slerpuses rawMath.Acos(cosOmega)(Quaternion.cs:350) without clampingcosOmegato [−1,1] (inconsistent with Vector3.Slerp which clamps). -
StatisticsExtensions.NormalizedRootMeanSquareErrordivides by(xMax − xMin)(StatisticCollection.cs:210) → Infinity/NaN when all samples equal;Meanof empty sequence returns0/0 = NaNsilently (:139);CalcPTestdivides byn1 − 1(:321) → Infinity for single-sample groups. -
Vector2.CopyTo—array.Length == 0, index == 0throwsArgumentOutOfRangeExceptionrather thanArgumentException. Same in Vector3/Vector4. -
StarMath.frexp(multiply divide.cs:2100-2136) — themantissaout-param is unused by the only caller; the whole routine could beMath.ILogB.
-
Massive int/double overload matrix in StarMathLib.
add subtract.cs,multiply divide.cs, andmake extract.cscontain 4–8 near-identical copies of every routine (IList<double>×IList<double>,int×double,double×int,int×int; with and without explicit lengths). Examples: 12Addoverloads (add subtract.cs:32-234), 4 identicalcrossProduct7bodies (multiply divide.cs:733-805), 4KronProductcopies (:1945-2050). Generic math (INumber<T>, .NET 7+; the project targets net10.0) or code generation would collapse ~70% of these files. -
Duplication between struct types and StarMathLib. Cramer's-rule solvers exist three times:
Vector4.Solve(Vector4.cs:1008),VectorExtensions.Solvefor Matrix3x3/4x4 (Extensions.cs:732, 748), andsolveViaCramersRule2/3(solve.cs:60, 166). Determinants:Matrix4x4.GetDeterminant(Matrix4x4.cs:1063) vsStarMath.GetDeterminant4(inversion transpose.cs:629) vsDeterminant(double[,])(:582). Cross/dot products exist forVector3and again forIList<double>(multiply divide.cs:566+). -
Duplicated operator/method bodies inside
Quaternion.Multiply/operator *(Quaternion.cs:509-536vs:659-686),Divide/operator /(:562-598vs:720-756),Negate/operator -,Add/operator +,Subtract/operator -are full copy-pastes instead of one delegating to the other (Vector2/3/4 delegate correctly). -
Dead code.
SingularValueDecomposition(all overloads,svd.cs:35-95) isinternaland has no callers in the repository — and is broken anyway.EqualityExtensions.EqualityTolerance(EqualityExtensions.cs:28) is a settable static property that nothing reads — all methods default to the compile-time constantConstants.DefaultEqualityTolerance, so setting it has no effect. Misleading API.PolynomialSolve.scalfactandmeps(PolynomialSolve.cs:42, 46) are never used.IVector.Null(IVector.cs:10) is a static auto-property on the interface that always returnsnull;DefaultPoint.Null(IVector.cs:57) is private and unused.Vector3(Vector3 value)copy-constructor +Copy()(Vector3.cs:65, 404) andVector2.Copy()(Vector2.cs:161) are pointless for immutable structs.- Large
if (false) { /* commented SSE code */ }blocks inMatrix4x4.cs:1570-1588, 1606-1614, 1735-1744, 1761-1768, 1785-1792, 1809-1842, 1965-1973, 1999-2006, 2023-2030— dead scaffolding from the System.Numerics port. - Unused usings:
PolynomialSolve.cs:17,StatisticCollection.cs:17,Vector3.cs:17,19.
-
Redundant overload style: every
Vector2/3/4static method is re-exposed as an identical extension method inExtensions.cs(~90 one-line wrappers,Extensions.cs:52-836). Doubles the API surface and XML-doc maintenance burden. -
GetEigenValuesAndVectorsre-derivation:Extensions.cs:841-927unpacks Matrix3x3/4x4 into 9/16 scalars to call StarMath, whileeigen.cs:68-82re-packsdouble[,]into the same scalar lists — one canonical entry point would do.
-
RemoveLeadingNegIfZeroallocates on every vectorToString.Extensions.cs:35:string.Join(string.Empty, Enumerable.Repeat('0', numString.Length - 3))builds a LINQ enumerator + string per component per call. A simple loop/AsSpan().TrimEnd('0')check avoids all of it. -
Vector3.Coordinates/Vector4.Coordinatesallocate a new array per get (Vector3.cs:476,Vector4.cs:55). ConsiderToArray()methods only, orReadOnlySpan<double>via inline arrays. -
Vector3.Normalizehidden costs in the hottest function of a geometry library.Vector3.cs:678-691: (a)Log.Warningstring on zero-length input — logging in a per-vertex hot path; (b)ls.IsPracticallySame(1.0)early-out costs a branch on every call; (c) branch structure defeats trivial inlining. Suggest a bare fast path plus a checked variant. -
Matrix4x4.Decomposeallocates three arrays per call (Matrix4x4.cs:1365-1369,:1422-1427) — replaceable with locals/Spansince sizes are fixed at 3. -
StarMathLib jagged/2-D arrays and per-call allocations.
- Every operation allocates a fresh result; no in-place or
Span<double>variants.ArrayPool<double>/stackalloccandidates:LUDecomposition'slastZeroIndices— array ofList<int>per call (inversion transpose.cs:198-207),solveBig's LU clone (:208),CholeskyDecomposition's fullA.Clone()(:413),SVD'swork/e/stemp(svd.cs:105-107). GetColumn/SetColumnround-trips inGetColumns,RemoveRow(s),RemoveColumn(s),JoinMatrix*IntoVector(make extract.cs:244, 553, 608, 678, 1178) copy each element twice;Array.Copyon the flat backing store would be one memcpy per row.multiply(double[,] A, double[,] B, …)re-evaluatesA.GetLength(1)in the innermost loop condition (multiply divide.cs:1088, 1110, 1132, 1154); also i-k-j loop order (or blocking) is more cache-friendly than current i-j-k.IList<double>interface dispatch per element in all kernels; overloads takingdouble[]/ReadOnlySpan<double>would let the JIT vectorize.
- Every operation allocates a fresh result; no in-place or
-
Missing SIMD everywhere.
Matrix4x4rows are 4 doubles = oneVector256<double>;operator +/-/*,Lerp,Transpose,FrobeniusNorm(Matrix4x4.cs:1589-1652, 1745-1946) are mechanical AVX candidates. Same forVector4.Dot/operator+and StarMathLibdotProduct/Add. Given net10.0,Vector256/TensorPrimitiveswould give 2–4× with modest effort. -
Missed
[MethodImpl(AggressiveInlining)]onMatrix3x3/Matrix4x4operators andGetDeterminant, while trivialVector2.Addwrappers are attributed. Inconsistent; the matrix ops are the ones that benefit. -
StatisticsExtensions.Medianenumerates the source twice (StatisticCollection.cs:36-37); quickselect uses last-element pivot with no randomization (:97) → O(n²) on sorted data. -
Parallel.Forlargely not applicable here (matrices are 2–4 wide); onlyExpMatrix/LUDecompositionon large matrices would qualify (~250×250+). Fix cache order first.
-
Mutable public static "constants" (High).
Vector2.Zero/One/UnitX/UnitY/Null,Vector3.Zero/One/Null/UnitX/UnitY/UnitZ(Vector3.cs:376-425),Vector4.Zero/UnitX/.../Nullarepublic staticfields, notreadonly. Any code can doVector3.Zero = new Vector3(1,2,3); noteoperator -is implemented asZero - value(Vector3.cs:336), so corruptingZerobreaks negation globally.Matrix3x3.Identity/Matrix4x4.Identity/Quaternion.Identityare correctly get-only properties — the vectors should match. -
Quaternionis a fully mutable struct with public fields (Quaternion.cs:34-46) while all vectors/matrices arereadonly struct. ItsGetHashCodeis an order-insensitive sum of component hashes (Quaternion.cs:829); same additive hashing inMatrix3x3.GetHashCode(Matrix3x3.cs:756-758) andMatrix4x4.GetHashCode(Matrix4x4.cs:2070-2073). UseHashCode.Combine. -
ComplexNumberdoesn't declareIEquatable<ComplexNumber>(ComplexNumber.cs:23) → boxing in generic collections.IsRealNumber(:394-399) uses relative tolerance — scale-dependent semantics;RealImagTolerance(:28) declared but unused. -
Culture-sensitive formatting:
ToStringusesCurrentCulture+NumberGroupSeparatoras separator;RemoveLeadingNegIfZerohard-codes"-0."(Extensions.cs:35-39) — breaks in comma-decimal cultures. -
Inconsistent formatting defaults:
Vector2.ToString()uses"G",Vector3/Vector4use"F3"; Vector3 omits<...>brackets. NoParse/TryParseround-trip. -
Thread safety:
EqualityExtensions.EqualityTolerance,StarMath.MaxSvDiter(svd.cs:29),PolynomialSolvemutable statics (PolynomialSolve.cs:30-42) should beconst/static readonly. -
XML-doc drift (widespread): "single precision" on double types (
Vector2.cs:22,Vector4.cs:24); svd doc says ascending, code gives descending;Quaternion.IsNull()doc says "identity";GetColumnerror text saysnumRows;CrossProductexception text wrong;solve.cs:108-124says "Solve2x2s"; live design doubt comments//is M44 really 0 and not 1?(Matrix4x4.cs:835, 870). -
Naming inconsistencies: StarMathLib mixes public camelCase (
multiply,solve,inverse) with PascalCase (Add,Inverse). File names contain spaces. -
IsNull()conflates NaN and all-zero for matrices (Matrix3x3.cs:124-133,Matrix4x4.cs:187-198): a legitimately all-zero matrix reports "null".
-
Transform convention trap:
Vector3has no matrix-transform operators; three same-namedMultiplyextension overloads with different math:Vector3.Multiply(v, Matrix3x3)(row-vector,Vector3.cs:816) vsMatrix3x3.Multiply(matrix, v)(column-vector,Extensions.cs:377) — differ only in argument order; easy to call the wrong one via extension syntax. -
Vector4(double x, double y, double z)setsW = 1(Vector4.cs:75-81);Coordinates => new[]{X/W, Y/W, Z/W}(Vector4.cs:55) meansUnitX.CoordinatesyieldsInfinity, NaN, NaN. Point-vs-direction semantics of W deserve factory methods (FromPoint,FromDirection). -
NaN-sentinel defaults:
Matrix4x4.IsIdentity(double tolerance = double.NaN)(Matrix4x4.cs:164),VarianceFromMean(…, double mean = double.NaN)(StatisticCollection.cs:148) — non-discoverable; use overloads. -
Snap-to-affine constructors mutate user input silently (
Matrix3x3.cs:191-197,Matrix4x4.cs:271-278, fixed 1e−12 tolerance);Matrix4x4(Matrix3x3)(:335-340) hard-codesm44 = 1discarding the 3x3's projective scale. -
Inconsistent failure contracts for solve/invert: bool+empty array (
solve.cs:36),Vector3.Null(Extensions.cs:732-753), exception from deep LU (inversion transpose.cs:295), false+NaN matrix (Matrix4x4.cs:1124). Four contracts for one conceptual operation. -
PolynomialSolve.GetRootscoefficient ordering undocumented; iterator-based methods defer exceptions to enumeration time. -
StarMathLib "trust-me" length overloads public (e.g.
add subtract.cs:144) — silent truncation risk; should be internal or Span-based. -
Three duplicate worlds (TVGL structs, extension wrappers, StarMathLib IList) in two namespaces with different conventions and error contracts.
PolynomialSolveinvertedMoveNext()guards and complexQuadraticSqrt(−disc).determinantBigpermutation sign (double + int variants).Matrix4x4.Decomposedouble negation.GetEigenVector2inverted pivot condition.EltMultiply/EltDivide&&→||dimension checks.- SVD returning zeros / delete if truly unused.
- Make
Zero/One/Unit*/Nullstaticsreadonly.
Scope: all TVGL-authored files under Polygon Operations\, including PolygonBooleanOperations\ and Clipper\ClipperPolygonOperations.cs. The vendored Clipper2 was not deep-reviewed. Note: the csproj defines TRACE;CLIPPER in all configurations, so all #if CLIPPER branches are live and the TVGL-native boolean/offset code paths are dead in the shipped library.
-
Deferred-LINQ bug:
...ToNewPolygonsmethods return unmodified copies.PolygonOperations.Simplify.cs:44-49(RemoveCollinearEdgesToNewPolygons),:143-148and:293-298(SimplifyMinLengthToNewPolygons),:696-701(SimplifyByAreaChangeToNewPolygons),Simplify.cs:1124-1129(ComplexifyToNewPolygons). Pattern:var copiedPolygons = polygons.Select(p => p.Copy(true, false)); // lazy SimplifyMinLength(copiedPolygons, minAllowableLength); // iterates, mutates throwaway copies return copiedPolygons; // re-enumeration makes FRESH, unsimplified copies
The caller receives brand-new copies of the original polygons; all work discarded. (Contrast
ComplexifyToNewPolygons(double)atSimplify.cs:1028, which correctly calls.ToList()first.) -
AreAllPointsInsidePolygonLinestests the same edge repeatedly and never resets crossing parity.PolygonOperations.IsInside.cs:399-401:for (int i = lineIndex; ...) { var line = sortedLines[lineIndex]; ... }— loop variableinever used to index. AlsoevenNumberOfCrossings(:389) declared outside per-point loop, never reset between points; parity leaks between query points. Public method effectively broken. -
EqualButOppositedetection is unreachable.PolygonOperations.IsInside.cs:986:if (intersect.Relationship != NoOverlap || intersect.Relationship != Abutting || ...)— always true; should be&&.PolyRelInternal.EqualButOpposite(:993-999) can never be returned;PolygonBooleanBase.cs:73-78handling starved. -
UnionPolygons(polygonsA, polygonsB)(TVGL native path) drops non-intersecting B polygons.PolygonOperations.Boolean.cs:344-376(#elif !COMPAREbranch):SeparatedB polygons never added to result. Dead under CLIPPER but wrong if re-enabled. -
Polygon.Reverse(bool reverseInnerPolygons)ignores its parameter.Polygon.cs:387-392:reverseInnerPolygonsnever read. Callers:IOFunctions.cs:613,IsPositivesetter (Polygon.cs:371-379) — hole orientation left inconsistent. -
Polygon.CalculateCentroiditerates the wrong vertex list.Polygon.cs:571-582:foreach (var p in AllPolygons)but inner loop readsVertices[...](this.Vertices), notp.Vertices. Wrong centroid whenever holes exist (outer counted N times, holes never). -
CreateShallowPolygonTreesOrderedListsAndVerticesnever yields the final polygon.PolygonOperations.CreateTrees.cs:157-172: lastpositivePolygonnever yielded after loop. (Currently unused.) -
TriangulateSweepLinefailure path returns known-bad triangles.PolygonOperations.Triangulate.cs:264-281: after 10 failed rotation attempts + simplification, control falls through toAddRange(localTriangleFaceList)(:280) — the failed triangle set — with no re-triangulation. Barecatch { }at:248-252swallows all exceptions.
-
Poisson-disk neighbor rejection inverted / wrong cell test.
InternalPoints.cs:99:if (neighbor.Item1 || neighbor.Item2.DistanceSquared(childPt) < rSqd)— should be&&; unoccupied cells test distance againstdefault (0,0). Also:97:if (i == 0 && j == 0)compares absolute grid indices instead ofi == xIndex && j == yIndex. -
TriangulateDelaunaycan request unlimited internal points.Triangulate.cs:785-786:numNewVerticescan be negative;CreateInternalPointsPoissonDisktreats non-positivemaxPointsToReturnas unlimited (InternalPoints.cs:72-73). -
Slice2D.SliceAtLine: collinear-avoidance shifts computed and discarded.Slice2D.cs:111-113:positiveShift/negativeShiftnever used; helperShiftLineToAvoidCollinearPoints(:246-259) never called. Consequences:SortedListat:137throwsArgumentExceptionon duplicate keys (line through vertex); paired loops:156-162/:201-205index out of range for odd intersection counts (tangency). -
Polygon3D.CopyNRE whenHoles == null.Polygon3D.cs:116-119; vertices-only ctor (:36-41) leavesHolesnull. -
AllPolygonIntersectionPointsAlongVerticalLinesindex overflow.IsInside.cs:661:sortedPoints[pointIndex + 1].Xnot bounds-checked; siblings use[pointIndex](:550,:745). -
AllPolygonIntersectionPointsAlongLinesignores itsnumStepsparameter.IsInside.cs:505-561. -
IsPointInsidePolygon(Polygon, bool, Vector2, out ...)throws on degenerate parity.IsInside.cs:317-321:ArgumentExceptionon tolerance-induced ambiguity; near-identical variant (:147-153) silently continues — inconsistent policies. -
Empty-statement guards in
SplitReplaceOldEdge.Arrangement.cs:163, :173:if (fromNode == intersectNode) ;— dead semicolons; intended guard not applied. -
ExtractPolygonsFromArrangementNodescrash / infinite-loop risk.Arrangement.cs:293-300:current.StartingEdges[0]throws on zero-edge node;while (current != startNode)can cycle forever. -
IComparercontract violations.Arrangement.cs:420-437(EdgeComparer),:438-448(NodeComparer) return1for "equal" —Compare(a,b)andCompare(b,a)both 1. Used withList.Sort(:81,:119) andBinarySearch(:313) — may throw or give unreliable results. -
SimplifyMinLengthToNewListcompares squared lengths against unsquared threshold.Simplify.cs:246enqueuesLengthSquared()but:256compares againstminAllowableLengthdirectly. Callers (PolygonRemoveIntersections.cs:58,Silhouette.cs:229) get an effectively sqrt-scaled threshold. -
SimplifyMinLength(paths, targetNumberOfPoints)double-yields on no-op.Simplify.cs:414-416: missingyield break; polygons yielded twice. -
SimplifyByAreaChange(Polygon, ...)broken for hole polygons and ignores holes.Simplify.cs:547-548: budgets use signedpolygon.Area(negative ⇒ loop exits immediately);origArea(:531) unused; only top loop enqueued (:537). -
IntersectPolygonsvia Clipper computes A ∩ (B₁∪B₂∪…) instead of A₁∩A₂∩….Boolean.cs:608:BooleanViaClipper(..., Intersection, polygons.Take(1), polygons.Skip(1), ...). Wrong for 3+ polygons vs documented behavior (:595-596). -
Complexify(List<Vector2>, double)inserts duplicate points.Simplify.cs:1104-1110:fraction = 0inserts copy ofpolygon[i];numNewPointslacks the1 +used by Polygon overload (:1074). -
HasFlippedSymmetryoff-by-one and phase mixing.Basic.cs:429:i <= twiceNumVertsduplicates i==0; odd shifts compare lengths against angles. -
CreateInternalPointsRadialcorruptsdeltaon wrap-around.InternalPoints.cs:141-151. -
GetMonotonicityChange/PartitionIntoMonotoneBoxesinfinite loops on degenerate polygons.Partitioning.cs:117-123(all-zero-length edges),:234(never terminates if no vertex qualifies;:258then indexesVertices[-1]).
- Stale-
Pathcache condition:Polygon.cs:41(_path.Count < _vertices.Countmisses removals). Polygon.Transformupdates outer bounds with inner-polygon vertices (Polygon.cs:845-852).- Exact
== 0cross-product collinearity tests (IsInside.cs:1126-1129). PolygonEdge.FindYGivenX/FindXGivenYreturndouble.MaxValue/MinValuesentinels (PolygonSegment.cs:280-282, 317-319).Vertex2D.this[int i]returnsYfor anyi != 0(Vertex2D.cs:53-60).DefineDeltaAngleNaN whenmaxCircleDeviation > 2|offset|(Offsetting.cs:120-127).Perimeter(IEnumerable<Vector2>)NaN on empty input (Basic.cs:69-91).IsRectangularNaN/bareException(Basic.cs:173, 183).MinkowskiSumConvex— author's own comment admits uncertainty >180° (Minkowski.cs:96-99).
- The entire TVGL boolean-op machinery is dead code under the shipped build. With
CLIPPERalways defined,Union/Intersect/Subtract/ExclusiveOrroute toBooleanViaClipper;PolygonBooleanBase.cs,PolygonUnion.cs,PolygonIntersection.cs(≈700 lines) reachable only viaRemoveSelfIntersections(Boolean.cs:894-902) and the un-guardedSubtract(minuend, subtrahend, interaction, ...)overload (Boolean.cs:718-723— publicly reachable with native-path bugs). Either commit to Clipper2 and remove/quarantine native path, or fix and test it.
#if/#elif/#elsetriplication throughoutBoolean.cs/Offsetting.cs(CLIPPER / native / COMPARE), e.g.Boolean.cs:158-193, 233-328,Offsetting.cs:139-180, 227-299.Offset(:231-254) duplicatesOffsetJust(:190-215) line for line.- 4 duplicate point-in-polygon implementations:
IsInside.cs:207-257,:272-323,:111-158,:439-488([Obsolete]), with subtly different boundary/ambiguity semantics. - Simplify family sprawl:
RemoveCollinearEdges*(4+2),SimplifyMinLength*(9),SimplifyByAreaChange*(9),SimplifyFast(3),Complexify*(7) — parallel Polygon/list versions duplicate queue logic. - Dead/unused private code (verified):
CreateShallowPolygonTreesOrderedListsAndVertices(CreateTrees.cs:157-172);ShiftLineToAvoidCollinearPoints(Slice2D.cs:246-259);firstAngleIsBetweenOthersCCW(Minkowski.cs:140-146);PolygonFillTypeenum (ClipperPolygonOperations.cs:25-43);RecursiveDelaunay(Triangulate.cs:995-1017— empty loop body, stray#endregion);static List<Vector2> debugPolygon(Triangulate.cs:827); COMPARE harness in library (Boolean.cs:39-130). Create2DMedialAxis= 240-line commented-out block returning empty list (MedialAxis2D.cs:42-281). Public API that silently does nothing.- Large commented-out blocks across
Polygon.cs,IntersectionData.cs,Triangulate.cs,PolygonRemoveIntersections.cs,Boolean.cs,Minkowski.cs:74. - Unreachable code:
Boolean.cs:634-650(code after return),PolygonBooleanBase.cs:233(if (completed == null)on non-nullable bool). - 17 KB C++ CGAL header (
Minkowski_sum_by_reduced_convolution_2.h) checked into the C# tree.
Polygon.InnerPolygonsallocates a newImmutableArrayon every access (Polygon.cs:316-317); accessed in hot paths (Area:406,Perimeter:455, tree recursion, boolean handlers) — O(n) copy each. Cache or exposeIReadOnlyList.Polygon.Perimeterre-sums inner perimeters on every get (Polygon.cs:455).Polygon.Reset()forces edge creation (Polygon.cs:868-869):foreach (var edge in Edges)materializes all edges to reset them.Area(IEnumerable<Vector2>)enumerates 3× (Basic.cs:111-117).GetWindingAnglesmultiple enumeration (IsInside.cs:53-59).SplitArrangementEdgesAtIntersectionsO(n²) (Arrangement.cs:82-116:RemoveAt(0)/Remove(other)in main loop);possibleDuplicates.Insert(0, ...)(IsInside.cs:1211-1231) O(n) front-inserts.UnionPolygonsnative path restarts outer loop after every merge (Boolean.cs:266) → worst-case O(n³) interactions.- 5 LINQ passes over the same intersections list in
GetSinglePolygonRelationshipAndIntersections(IsInside.cs:970-1021). - Per-attempt polygon rotation in TriangulateSweepLine (
Triangulate.cs:239, 259): mutates every vertex +Reset()twice per attempt, up to 10 attempts; FP drift on caller's polygon. Copydoesn't propagatepathArea/perimeter/NumSigDigits(Polygon.cs:700-730) — copies re-derive;NumSigDigitscan differ, changing rounding behavior.- Parallelization:
UnionPolygons/IntersectPolygonsover independent pairs,SimplifyFastmap (Boolean.cs:528-531),BooleanViaClipperpath conversion (ClipperPolygonOperations.cs:151-173) are embarrassingly parallel per polygon; noParallel.ForEachanywhere in folder. - Span/CollectionsMarshal candidates:
MakeVerticesFromPath(Polygon.cs:616-645) per-elementRemoveAtO(n²);RemoveCollinearEdgesDestructiveList(Simplify.cs:117-130) same — linear compaction over a span instead.
matchedPolygonBIndices.Count()LINQ vs.Count(IntersectionData.cs:271).AllPolygonIntersectionPointsAlong*:OrderBy(...).ToArray()per step (IsInside.cs:557, 667, 752) —Array.Sortin place.Polygon.Path.Any(...)linear scan per containment query (IsInside.cs:287).- No-capacity List growth (
PolygonBooleanBase.cs:193,IntersectionData.cs:329-343);.ToList()copies of already-List returns (PolygonBooleanBase.cs:54-55).
- Thread-safety of boolean ops: singletons
polygonUnion/etc. (Boolean.cs:139-147) stateless, butPolygonBooleanBase.Runmutates the input polygons:NumberVerticesAndGetPolygonVertexDelimiterrewritesvertex.IndexInList(PolygonBooleanBase.cs:147-161); traversal mutatesSegmentIntersection.VisitedA/B. Concurrent ops sharing a polygon corrupt each other.TriangulateSweepLinedestructively rotates the input polygon (Triangulate.cs:239; comment:214mentions "threading issues"). Polygonlazy caches not thread-safe despite partial locking:Path/Area/PathArea/OrderedXVerticeslock on_vertices, butPerimeter→Edgesmutates_edgesunlocked (Polygon.cs:131-136);Reset()(:860-870) clears without lock;Areaholds outer lock while taking nested child locks (ordering hazard);MaxX/MinX/...(:469-542) no synchronization.- Library writes files to CWD in production paths:
Triangulate.cs:275:IO.Save(polygon, "errorPolygon" + DateTime.Now.ToOADate() + ".json")in released CLIPPER build fallback. COMPARE-onlytimes.csvand*Fail*.jsondumps in same file. NotImplementedExceptionin reachable public paths:IntersectionData.cs:276(reachable from publicGetPolygonInteraction,IsInside.cs:828);TriangulateDelaunay(vertexLoop/s, ...)(Triangulate.cs:640, 680);CreateInternalPointsVoronoi(InternalPoints.cs:118).
- Exception swallowing: bare
catch { }(Triangulate.cs:248-252); "drop last crossing edge" fallback (Triangulate.cs:980-981). - Magic numbers:
scale = 45720000(ClipperPolygonOperations.cs:56); miter limit2(:83, :121);areaSimplificationFraction = 1e-5with always-true guard (Boolean.cs:34, 526, 603);100 * tolerance(Offsetting.cs:346);0.01(Triangulate.cs:253);0.25(Silhouette.cs:208); mixed tolerance regimes (BaseTolerance 1e-9 abs vs PolygonSameTolerance 1e-7 rel vs DefaultEqualityTolerance 1e-12) applied inconsistently (Basic.cs:245, 286). - Partial-class sprawl:
PolygonOperationsspans 15+ files; nested private types in static class (Arrangement.cs:387-448); invariants (who may mutateIndexInList) hard to reason about. - Junk usings:
Triangulate.cs:16-20,Polygon.cs:20,Basic.cs:16,Simplify.cs:16. - Nondeterminism: unseeded
new Random()(InternalPoints.cs:37, 124) vs seedednew Random(1)(Triangulate.cs:226) — inconsistent; Delaunay meshing non-reproducible. - Copy-paste XML docs:
SegmentIntersection.cs:55-65;Arrangement.cs:9header says "Minkowski.cs";Partitioning.cs:208.
- Boolean API semantics are compile-time-define-dependent — same call runs different algorithms with different tolerance behavior; callers can't tell which they get.
- Inconsistent tolerance conventions:
double tolerance = double.NaN"auto" (Boolean.cs:209,Offsetting.cs:36) vsConstants.DefaultEqualityTolerancedefault (IsInside.cs:175) vsboundaryTolerance(:273) vsconfidencePercentage(Basic.cs:170). PolygonCollectionenum controls return shape but the return type is alwaysList<Polygon>— element meaning changes invisibly;PolygonTreeshandled asdefault:(PolygonBooleanBase.cs:101).- Mutating vs copying variants hard to distinguish:
SimplifyMinLengthvs...ToNewPolygon(s)vs...ToNewList(s); theToNew*family is currently broken. Overloads(polygon, double)vs(polygon, int)differ only in parameter type with different semantics. IEnumerablereturns with deferred side effects — consumers enumerating twice get different results. PreferList<Polygon>for anything with side effects.- Silent-failure public APIs:
Create2DMedialAxisreturns[];CreateInternalPointsVoronoiand 2 of 4TriangulateDelaunayoverloads throwNotImplementedException. - Naming:
PolygonEdgelives inPolygonSegment.cs(doc says "NodeLine");StartLine/EndLinevsFromPoint/ToPointmixed vocabularies;AllPolygonIntersectionPointsAlongVerticalparamXValuedocumented asYValue(IsInside.cs:790);RemoveHole/AddInnerPolygonasymmetry;LargestPolygon(signed) vsLargestAbsAreaPolygon(abs) with identical docs (Basic.cs:31-43). Polygon.IsPositivesetter has heavyweight, partially-incorrect side effects (only reverses outer loop).CreateSilhouettereturns a singlePolygon(Silhouette.cs:33-67) — disjoint silhouette regions beyond the largest are silently discarded.
Scope: TessellatedSolid\ plus Solid.cs. All findings verified by reading cited code.
File abbreviations: TS = TessellatedSolid.cs; Edge/Face/Vtx/EP = Edge.cs/TriangleFace.cs/Vertex.cs/EdgePath.cs; Opts = TessellatedSolidBuildOptions.cs; MEP = MakeEdgePaths\MakeEdgePaths.cs; TIR = ModifyTessellation\TessellationInspectAndRepair.cs; ARE = ModifyTessellation.AddRemoveElements.cs; Simp = SimplifyTessellation.cs; Cplx = ComplexifyTessellation.cs; DIV = DetermineIntermediateVertex.cs; S3PT = Single3DPolygonTriangulation.cs; Slice = Slicing\Slice.cs; CD = Slicing\ContactData.cs; Solid = Solid.cs
-
Simplify(ts, minLength)ignoresminLengthentirely and will collapse the whole mesh —Simp:132-173.minLengthnever referenced in loop body; withnumberOfFaces = -1(Simp:120-123),iterationsdecrements from -1 and never equals 0 → loop runs until priority queue empty, collapsing every edge. -
MergeVertexAndKill3EdgesAnd2Facesnever assignslocalKeepEdge2; out paramkeepEdge2always null —ARE:119-120assigns out param instead of local, thenARE:143overwrites with null local. (a) topology filterARE:123-126misclassifies; (b)Simp:164UpdatePriority(keepEdge2, ...)→ NRE on first successful merge. -
SimplifyFlatPatchesremoves faces but never adds replacements —Simp:37facesToAddnever populated; new triangles only in localnewFaces(Simp:44,60) and Plane primitive (Simp:85).ts.AddFaces(facesToAdd)atSimp:89adds nothing → mesh gets holes. -
AddEdgesassigns the sameIndexInListto every added edge —TS:1290:newEdges[NumberOfEdges + i].IndexInList = NumberOfEdges;(missing+ i). Affects hole repair (TIR:1219),Complexify(Cplx:66); breaksRemoveEdge(int)/RemoveEdgesandEdgePath.CompletePostSerialization(EP:480-481). -
RemoveEdge/RemoveReferencesToEdgecrashes viaReplaceEdge(edge, null)—TS:1377-1380→Face:493dereferencesnewEdge→ NRE for any removed edge with an owned/other face. PublicRemoveEdge(int)(TS:1309) unusable on connected edges. -
Faces+vertices constructor runs repair with
SameTolerance == 0—TS:729-730: repair called beforeDefineAxisAlignedBoundingBoxAndTolerance().SameTolerancedefaults 0 (Solid:254); negligible-area detection uses0*0(TIR:266,364); crack-stitchingmaxTolerance = 100*0 = 0(TIR:859) — can never match, for solids built via this ctor (used byCopy()and allSliceoutputs). Other ctors compute tolerance first (TS:143,208). -
Int overflow in duplicate-face checksum multipliers —
TS:806:NumberOfVertices * NumberOfVerticescomputed in 32-bit int before widening. Overflows at ≥46,341 vertices → false duplicate detection, silently dropped faces; guard atTS:799only kicks in at ~2M. Should be(long)NumberOfVertices * NumberOfVertices. -
Race + null deref in hole triangulation task juggling —
TIR:1269-1301. Three tasks race incl.Task.Run(() => Thread.Sleep(1000)) //why?.Task.WaitAnyreturns first to complete, not succeed: if task 0 throws (caught,triangleFaceList1null) and completes first,TIR:1301NREs. Sharedsuccessflag unsynchronized and never read. If both triangulations >1s, sleep task "wins" and hole silently skipped. -
Complexifyinserts NaN/Null vertices when an edge spans two primitives — two-primitiveDetermineIntermediateVertexPositionreturnsVector3.Null(DIV:165-168, viaDIV:38). InCplx:100-115NaN deviation fails break check (NaN comparisons false) andnew Vertex(c.mpt)(Cplx:115) inserts NaN vertex. -
PropagateFixToNegligibleFacesignores bool result ofCollapseEdgeAndKill2MoreEdgesAnd2Faces—TIR:274. On refusal (ARE:127-139returns false,removeEdges = null), still callsts.RemoveVertex(corrupt mesh) andts.RemoveEdges(null)→ NRE atTS:1330. AlsoOtherFace == null(open mesh) → NRE atTS:1208. -
Copy()throws for any solid without primitives —TS:1474unconditionally callsDefineBorders(copy); first statementsolid.Primitives.First().Borders(TIR:1370) throws. Also breaksTransformToNewSolid(TS:1596-1601),SetToOriginAndSquareToNewSolid(TS:1517-1521). -
Cross-sections by
numSlicesalone produce NaN offsets —Slice:905-908:startDistanceAlongDirectioncomputed beforestepSizederived fromnumSlices; NaN slice distances, all layers empty. Also unconditionally overwrites caller-supplied start (NaN check commented outSlice:903-904).
MakeContactDataForEachSolidtestsface.Btwice, neverface.C—Slice:324-326.Transformrotates inertia tensor incorrectly —TS:1581-1585:I·Rinstead ofR·I·Rᵀ; comment says "I'm not sure this is right". Translation effects ignored.- Mutation methods never invalidate cached
_volume/_surfaceArea/_center/_inertiaTensor—TS:1157-1236, 1015-1122, 1280-1294leaveSolid:89-155caches stale. Post-repairts.Volumereports pre-repair value (TIR:359). Vertex.DefineCurvatureinverted for mixed edges —Vtx:179-190:Any(e => e.Curvature != Convex)→ Concave; final else unreachable. Compare correctAll-basedFace:457-466.Loopctor checks field before assignment —CD:483-486: readsIsClosed(always false) instead ofisClosedparam.GetSliceContactDatacan never returnfalse—Slice:135-155singlereturn true; "plane doesn't cut" handling atSlice:36-41, 81-86, 111-117is dead; surfaces as exception fromDivideUpFaces(Slice:417).AllSlicesAlongX/Yindex past end ofsortedVertices—Slice:1326-1327,:1385-1386; Z variant guards (Slice:1444) but itsbreakleaves remainingloopsAlongZ[step]entries null.DivideUpFacesstarting-edge search throws even when a good edge is found —Slice:485-491(boundary condition on k).EdgePath.IndexOf/Contains((Edge,bool))throws on missing edge —EP:304-306:DirectionList[-1].EdgePath.AddBegin(Edge)corrupts/throws on empty path —EP:273-278.Complexifystuffs a sequence counter intoEdge.EdgeReference—Cplx:130,138,149; since >0,AddEdge(s)(TS:1270,1289) never recompute → bogus checksums breakTIR:968,Simp:64.FlipEdgeNREs on boundary edges; stale primitive face refs —ARE:35no null check onoldOtherFace;ARE:81-85never removes old faces fromprimitive.Faces.DefineBorderSegmentsdereferences before its own null check —TIR:1483-1486.MakeEdgesinconsistent null guard onSingleSidedEdgeData—TIR:612-613vsTIR:639.TriangulateRecursedequeues without count check —S3PT:479-481→InvalidOperationException.- StreamRead color array overflow after degenerate-face skipping —
TS:483-498vsTS:532-552: run-length encodes original face count;colors[k++]sized to shrunkts.Faces.Length(TS:533). Simplifyiteration counting removes ~half the requested faces —Simp:143,151,156: budget decremented per attempt AND per success.
MakeVerticesdecimal-rounding loop can throw for degenerate bounds —TS:938-940(numDecimalPointspast 15).vertsPerFacere-enumerated for count —TS:141-142.RemoveVertices/RemoveFaces/RemoveEdgesmis-handle duplicate indices —TS:1112-1119, 1228-1234, 1349-1355; no dedup at callers.Edge.DefineInternalEdgeAnglediv-by-zero (Edge:336,347,numNeighbors == 0→ NaN).StreamWritecolor block indexesFaces[0]with no empty-mesh guard —TS:313-327.StreamReadNREs ifNumberOfPrimitivesabsent —TS:560vsTS:408.GetFacePatchesBetweenBorderEdgesiteratesface.Edges(may contain nulls) notNonNullEdges—TIR:1193-1196.SortByIndexInListviolates comparer contract on equal indices —Comparators.cs:33-38;SortedSet.RemoveinSimp:139-141can silently fail (likely given #4).FindBestEdgePathPaircan indexedgePaths[-1]—MEP:309-326when all pairs score MaxValue.
- HIGH — Three near-identical 60-line slicers plus a general one:
AllSlicesAlongX/Y/Z(Slice:1296-1344, 1354-1403, 1413-1465) +GetUniformlySpacedCrossSections(Slice:890-958) duplicate the same sweep with divergent bugfixes (Z has bounds guard, X/Y don't;0.001*vs/10.0offsets —Slice:942vs:1327). Also duplicated 30-line boundary-edge region inNewFace(Slice:604-634vs:661-691). - HIGH —
MakeEdgePathsTooNew+MakeStrandsFromEdgePathsare dead code (~160 lines,MEP:111-272); no references outside own file; dead copy lacksep1 != ep2guard (MEP:64vs:159). - MEDIUM — Duplicated edge-matching logic:
TS:805-927checksum machinery (incl. unused private helpersTS:871-879, 892-912, 924-927) vsTIR:365-414; three separate walk-edges/checksum/complete-or-create loops (TIR:1302-1356,Simp:61-83,TS:831-857). - MEDIUM — Dead/commented code: 25-line commented
StartArraybranch (TS:446-472); 50-line commented quadric derivation (DIV:91-144);#if PRESENTblocks (TIR:765-773, 846-853); no-opif (secondVertex == thirdVertex) secondVertex = thirdVertex;(S3PT:459,469). - LOW —
SetNegligibleAreaFaceNormals(TIR:664-686) dead private method. - LOW —
DetermineIntermediateVertexPositioncalled twice back-to-back —ARE:179-184: first result immediately overwritten by two-primitive overload (which returns Null — see Errors #9). - LOW —
RemoveFaces(IEnumerable)sorts twice —TS:1208-1209+TS:1225.
- HIGH — O(n²) incremental element mutation in repair paths: every
RemoveVertex/RemoveFace/AddFacereallocates and copies full array (TS:1002-1008, 1070-1085, 1142-1151);RemoveVertexre-checksums every edge (TS:1084,1121,1127-1132).PropagateFixToNegligibleFaces(TIR:267-317) does this per defect in a while loop → O(k·(V+E)). Batch removals (asSimp:170-172does) or use List. - HIGH — Dead debug allocation in
Complexify—Cplx:91: allocates 2 Vector3 arrays per edge per call; only referenced by commented-out presenter code. - MEDIUM — Linear search in straddle-loop tracing —
Slice:438-446:FirstOrDefaultinside do/while → O(n²) per slice;straddleEdgesDict(Slice:387) exists but maps to raw Edge. - MEDIUM —
GetNeighborsPreEdgeCreationscans pair lists linearly (TIR:1016-1038) — O(n·m) on damaged meshes. - MEDIUM — LINQ in hot loops:
Intersect(...).Any()in O(n²) pair loop (MEP:302-305, per-pair hash sets);Faces.All(...)perStreamWrite(TS:313); iterator allocations in per-face loops (TIR:368-413,TS:288-291);Edges.Any/Allchains (Face:459-465). - MEDIUM — Misused parallelism: only concurrency is the broken triangulation race (
TIR:1269-1298). Genuinely parallelizable per-face passes are serial:CalculateSurfaceArea(TS:1633), integrity face scan (TIR:368-414),Transformloops (TS:1550-1577),FindNonSmoothEdges(TIR:1153-1167). Dominant costs at 10⁵–10⁶ faces. - LOW — Span/ArrayPool candidates:
MakeVerticesper-faceList<int>(TS:950) → stack Span of 3;MakeFacesper-faceorderedIndiceslists (TS:814) — the 3-value sort helper (TS:892) written for this is unused; Remove* intermediate arrays → ArrayPool. - LOW — Lazy values recomputed on every access while Undefined:
Vertex.Curvature,TriangleFace.Curvature(Vtx:159-167,Face:439-447). - LOW —
GetSliceContactDatacopies distances list twice (Slice:145-147,:382-383).
- HIGH — Mutable static build-option singletons + option mutation:
TessellatedSolidBuildOptions.Default/Minimal(Opts:10,18) shared mutable with public setters;CompleteBuildOptionswrites to caller's options object (TIR:115-119). Global/cross-thread behavior mutation. - HIGH —
IndexInListfragility is systemic:Edge.EdgeReferencechecksums (Edge:459-477), primitiveFaceIndices(PrimitiveSurface.cs:268-276— only refreshed when it shrinks, so removals leave stale indices used byCopy()TS:1434),EdgePathserialization (EP:470,481),Slice'sdistancesToPlane[edge.From.IndexInList](Slice:390-391). Findings 1.4/1.23 show indices do drift. No invariant checker outside debug-onlyTIR:457-539. - MEDIUM — Silent repairs and swallowed exceptions: every repair stage wrapped in
catch { }or log-only (TIR:162-251; bare catchesTIR:180-183, 206-209). No summary of changes;out removed*lists discarded by ctors (TS:163,211,729). - MEDIUM — Thread safety of lazy mutable state:
Edge.Length/Vector/InternalAngle/Normal(Edge:104-256),Face.Normal/Center/Area(Face:236-405),Solid.Volume/...(Solid:65-155) unsynchronized lazy fields.Sliceheader admits conflict (Slice:11-15); concurrent slicing mutatesIndexInListon shared vertices (Slice:510,522,548). - MEDIUM — Serialization: malformed primitive entries
breakmid-object (TS:421-444);Type.GetType("TVGL." + primitiveString)(TS:433) fragile;SurfaceAreasilent no-op setter (Solid:120). - MEDIUM — Memory footprint: face = class w/ 3 vertex + 3 edge refs + caches (~100+ B); vertex carries two List<> allocations (
Vtx:53-54). Struct-of-arrays layout would cut memory several-fold and improve locality. - LOW —
EdgePathclaimsIsReadOnly => true(EP:176) while mutable throughRemoveAt/Remove/Clear;Add/Insertthrow (EP:324-351) — violates IList expectations.
- Constructor overload sprawl — 4 public ctors (
TS:136, 179, 201, 650) with trailing(colors, buildOptions, units, name, filename, comments, language);numOfFaces: -1sentinel; colors recycled modulo;vertices: null= "derive from faces". Builder or description object would clarify. - Inconsistent
DuplicateFaceCheckdefault —Opts:102false, but indexed ctor treats null options as true (TS:209); verts-per-face ctor ignores option (TS:162). - Repair entry points hard to discover, inconsistently gated — implicit in ctors; post-hoc handles:
ts.Errors(property named Errors is the repair engine,TS:106),MakeEdgesIfNonExistent(TS:736-749). Some option combos throw (TIR:214-215), others silently auto-correct (TIR:115-119). - Mutation vs immutability confusion —
CopyElementsPassedToConstructor=falsedestructively reindexes the input solid's elements (TS:685-691, 716-727);Transformdestructive vsTransformToNewSolid;TurnModelInsideOuthalf-updates caches (TS:1608-1618negates_volumenot_center); public setters onFaces/Vertices/counts (TS:57-99). Simplify/Complexifyoverload semantics collide —(ts, int)vs(ts, double)(Simp:110-123,Cplx:39-52): integer-typed call silently picks face-count semantics. Named methods safer — esp. given min-length isn't implemented (finding 1.1).Errors == nulloverloaded — means both "checked and clean" and "never checked" (TIR:441-450,TS:739).
All findings verified by reading the source. Citations relative to TessellationAndVoxelizationGeometryLibrary\.
-
All KD-trees are built with
Dimensions = 3, even for 2D points — nearest-neighbor results are wrong for 2D. Every factory passes 3:PointCloud\NearestNeighbor\KDTree.cs:26(new KDTree<Vector2>(3, …)),:42(Vertex2D),:58,:73-74,:89-90,:105-106.Vector2's indexer returns Y for any index ≥ 1 (Numerics\Vector2.cs:65-72), soStraightLineDistanceSquared(KDTree.cs:480-489) computesdx² + 2·dy²— a metric that orders points differently than Euclidean. Radius queries filter with the inflated metric. Directly affectsConvexHullAlgorithm.2D.cs:90/108(CreateConvexHullMaximaluseskdTree.FindNearest(midPoint, radius)), which can miss boundary points it was written to recover. -
GetDistanceToExtremePointsadds max-side ties tobottomPoints.MinimumEnclosure.cs:479-480:if (distance.IsPracticallySame(maxD, ...)) bottomPoints.Add(point);— should betopPoints.Add(point). Consumed byBoundingRectangleAlong(:87-95). -
ConvexHull3D.Create(..., out vertexIndices)returns indices of all input points, not hull vertices.ConvexHullAlgorithm.3D.cs:37-44:vertexIndices = vertices.Select(v => v.IndexInList).ToList()= always[0..n-1]. Should projectconvexHull.Vertices. Identical bug in 4D:ConvexHullAlgorithm.4D.cs:66. -
Rotating calipers: side points computed with the previous best rectangle's directions/offsets.
MinimumEnclosure.cs:678-684:FindSidePointscalled withbestRectangle.Direction1/2/Offsets(i)beforebestRectangleis replaced. First improvement compares against the dummy rectangle's ±∞ offsets (:637-638); later improvements use stale directions. Propagates intoFindOBBAlongDirection'sPointsOnFaces(:875-883). -
ConvexHull4D.JigglePointsAndTryAgain: int overflow and one-sided jiggle.:211:var nSqd = (long)(n * n);overflows for n > 46,340 (correct form at:105), corrupting face-ID hashing.:196-197:p.X + 2*stepSize*(random.NextDouble() - 1)gives perturbation in[-2·step, 0)— every coordinate shifted negatively (should be- 0.5). -
2D convex hull extreme-point tie-breakers reference the wrong extreme.
ConvexHullAlgorithm.2D.cs:170(x > points[maxXIndex].Xshould beminYIndex) and:182(should bemaxYIndex). With AABB ties, wrong seed extreme; CCW-dedup at:212-223assumes correct tie-breaking. -
ICP:
GetAnglesloops over the wrong collection size.IterativeClosestPoint.cs:421-427: bound isstartNormals.Countbut indexes target-cloud-sized lists → IndexOutOfRange or silent truncation for differing cloud sizes. -
ICP: division by zero for
maxIterations < 50.IterativeClosestPoint.cs:145:% ((int)(0.02 * maxIterations))— modulus 0.
-
GetExtremaOnAABBskips every other point.ConvexHullAlgorithm.3D.cs:599and.4D.cs:675:for (int i = 1; i < n; i += 2). Half the input never participates in the Akl-Toussaint extrema search; seed simplex degraded; 3D degenerate-case detection (:108-128) fed by incomplete extrema. -
MinimumSphere/MinimumGaussSpherePlanecan loop forever on oscillation. Stall counter only increments on consecutive same index (MinimumSphere.cs:73-74,MinimumGaussSpherePlane.cs:68-69); a 2-cycle (A,B,A,B from the1.0001slop atMinimumSphere.cs:356-357) resets it; no absolute cap (contrastMinimumCircleCylinder.cs:49maxIterations = 1000). -
ConvexHull4D.Createtolerance ignores the W extent (.4D.cs:28-62). ForDelaunay3D(W = x²+y²+z²,Delaunay3D.cs:400,410) the jiggle step is badly under-scaled. -
Delaunay2D.Create:IndexInListoff-by-one in two branches.Delaunay2D.cs:148, :151:delaunayVertices[i++] = new Vertex(…, i);— side effect before argument evaluation → IndexInList = position + 1 for IVector3D and plain IVector2D inputs. -
Delaunay2Ddegenerate handling.Circle.CreateFrom3Pointssuccess flag ignored (:263, super-circle:172) → collinear/dupe points poison containment; duplicate points silently dropped from mesh while still inVertices(strict<at:305-308). -
Delaunay3D.Create(…, reuseInputVertices: true)never reuses input vertices.Delaunay3D.cs:283-286: result immediately overwritten — dead assignment contradicting docs.CreateInner'spoints is IList<Vector3>check (:394) can never be true (no variance over value types) — dead fast path. -
GJK degenerate path can throw.
ConvexHullGJK.cs:306-308:dotTotal == 2case has emptyelse { // ERROR; }falling tothrow new Exception("This should never happen")(:462).hff1(:589-592) has needless cancellation. -
KD-tree: empty input crashes.
KDTree.cs:274:GetNullObject(OriginalPoints[0])— no Count == 0 guard. -
MinimumCirclefallbacks.FirstCircleignoresCreateFrom3Pointsbool (MinimumCircleCylinder.cs:142);FindCircle'sif (circle.Radius == 0.0) circle = tempCircle;(:210-211) accepts a circle already proven not to contain all four points. -
Coplanar 3D hulls built as zero-thickness, doubled-face solids (
ConvexHullAlgorithm.3D.cs:445-449, both windings fanned from vertex 0). n<4 results (:108-127) have vertices but no faces/edges —IsInside(ConvexHull3D.cs:112-118) then returnstruefor every point.
RotatingCalipers2DMethoditeration cap can end the sweep a step or two early (MinimumEnclosure.cs:640).ConvexHullAlgorithm.2D.cs:359: collinear points kept only when the shared line isn't axis-parallel — inconsistent with "minimal" claims (:40-41).ConvexHullFace4D.GetNormal(true)(ConvexHull4D.cs:111-127):validNeighborCountunused; returnsVector4.Zero.Normalize()(NaN) silently.Delaunay2D.CreateViaConvexHullreductionFactor(Delaunay2D.cs:44-45) subtractsavgXfrom a value that is already a distance — formula wrong (harmless by scale invariance).
MinimumCircleCylinderOld.csis compiled, dead code — delete (High). Lives in namespaceTVGL.Test(:19); nothing references it; ships in the release assembly. Duplicates MaximumInnerCircle/MinimumBoundingCylinder nearly verbatim and retains a bug fixed in the new file (:380-381never updatesminCylinderVolume→ returns last cylinder, not smallest; compareMinimumCircleCylinder.cs:399-404). ~60 lines commented-out algorithms.GaussianSphere.csis entirely dead (High). ~730 lines, zero references, hardcoded 0.0001 tolerances, O(F²) FindIndex scans — remove or archive.- 3D/4D quickhull duplication (Medium).
AddVertexToProperFace(3D:391-416 vs 4D:468-493),GetExtremaOnAABB(3D:596-636 vs 4D:671-717), main queue loop (3D:143-167 vs 4D:132-159) line-for-line; the sharedi += 2bug is present in both — a generic core would land fixes once. - KD-tree duplication (Medium).
GenerateTreecopy-pasted betweenKDTree.cs:336-407andKDTreeGenericWithAccompanying.cs:66-149; ~20 Create overloads reduce to two generic ones (:57, :105). - Dead/commented code in live files (Low):
FindBestExtremaSubsetOLD(.4D.cs:516-543); commented 5-vertex simplex (:620-661); ICPU0/U1computed never used (IterativeClosestPoint.cs:45-46),norm()/AddTtoR/ZipMultiplyAddunreferenced; unused locals (ConvexHullAlgorithm.2D.cs:316-317);GetDistanceToExtremeVertex<T>unused generic param (MinimumEnclosure.cs:409); commented Presenter/CSV debug (MinimumCircleCylinder.cs:83-108).
- KD-tree search allocates two
HyperRects (four double[]) per node visited (High).KDTree.cs:430-434;HyperRect.cs:27-32. Fix: mutate one bound in place, recurse, restore. ICP calls FindNearest per point per iteration (IterativeClosestPoint.cs:65-67, 182). BoundedPriorityListis a sorted List with Insert, not a heap (Medium).BoundedPriorityList.cs:110-136O(k) memmoves per add (doc claims O(log n));numberToFind = -1builds one sized to the whole tree withallocate: false(KDTree.cs:319-321). UsePriorityQueueor fixed-array max-heap.- KD-tree construction garbage (Medium). Per node:
points.Select(p => p[dim]).NthOrderStatistic(…)copies into a fresh List (StatisticCollection.cs:60) + fresh left/right arrays (KDTree.cs:342-350). In-place Span quickselect over one working array. AlsoEnumerable.Repeat(nullPoint, TreeSize).Cast<TPoint>().ToArray()(:279) boxes every slot for struct points. - GJK small fixed buffers (Medium).
ConvexHullGJK.cs:44new Vector3[4],:107new bool[3]per call — stackalloc candidates.support()(:546-565) linearly scans all hull vertices per iteration; hill-climb over hull adjacency would be sub-linear. - Missed parallelism over candidate directions (Medium).
MinimumBoundingCylinder(vertices, directions)runs 13 independent trials sequentially (MinimumCircleCylinder.cs:397-405);FindMinimumBoundingBox's 13 ChanTan starts (MinimumEnclosure.cs:198-204). Natural Parallel.For min-reductions. ConvexHull3D.LineIntersectionsorts all faces twice (Low).ConvexHull3D.cs:129, 139— lazy OrderByDescending re-executed by Reverse(); a linear best/worst scan suffices.Delaunay2D.Createis O(n·m): every insertion scans all triangles (Delaunay2D.cs:185-192); parallel circle List with O(m) RemoveAt (:213-214). Bowyer-Watson with locate-and-flood is the standard fix.- LINQ in hot ICP loop.
IterativeClosestPoint.cs:57-92re-materializes half a dozen Lists per iteration;CalHbCobig_Gaborexplodes matrices into 12+ List (:238-269) per iteration. - 2D hull scratch arrays:
ConvexHullAlgorithm.2D.cs:294-301allocates cvxVNum arrays each full input length (up to 4n tuples) — ArrayPool or growth-on-demand.
- Library writes files to the user's Desktop.
MinimumCircleCylinder.cs:104-107: on iteration cap, writescvxpoints.csvto Desktop in production code. Privacy/sandbox/server problem — behind a debug flag or remove. - Console output from library code.
ConvexHullAlgorithm.4D.cs:186,Delaunay2D.cs:107, per-iterationIterativeClosestPoint.cs:150— route through Log. - Non-deterministic randomness. Unseeded
new Random()in.4D.cs:191(jiggling → ConvexHull4D and Delaunay3D vary run-to-run),Delaunay2D.cs:35,IterativeClosestPoint.cs:192, 203. Accept a seed; reproducibility matters exactly when these degenerate paths fire. - Thread safety.
ConvexHull3D.Create(vertices, connectVerticesToCvxHullFaces/recordPartOfConvexHull)mutates shared Vertex state (.3D.cs:352-377) — concurrent hulls over solids sharing vertices race. KD-tree immutable after construction, but nothing documents any of this. - Tolerance zoo. BaseTolerance 1e-9, DefaultEqualityTolerance 1e-12, OBBTolerance 1e-5 (
Constants.cs:83, 99, 170) + ad-hoc 0.0001/0.00001/1.0001/sqrt(BaseTolerance) literals, mostly absolute, not model-scaled. The 4D AABB-diagonal-derived tolerance (.4D.cs:62) deserves to be the norm. - Readability.
ConvexHullGJK.S3D~370 lines of transliterated C; ICP interleaves MATLAB comments; 2DCreate~250 lines mixing three phases; 4D "stack" is actually a Queue (BFS) while the comment says depth-first (.4D.cs:250-253); self-review comments left in (.3D.cs:520-521).
- Two competing idioms for "get me an enclosure": static try-pattern factories (
ConvexHull3D.Create(pts, out hull, out indices) : bool) vs extension methods returning directly and throwing (points.MinimumCircle(),.MinimumSphere(),.FindMinimumBoundingBox()). Enclosure functionality also split across three entry-point types. - Output-type inconsistency. ConvexHull3D is a full Solid; ConvexHull4D a bare class of arrays; ConvexHull2D returns List + parallel out List of indices; BoundingBox vs BoundingBox both exist (
MinimumEnclosure.cs:794-818); Delaunay return raw arrays. - Naming traps.
ConvexHull4D.FacesareEdge4Dobjects that are actually triangles; true edges calledVertexPairs(ConvexHull4D.cs:25-30). Public lowercasetolerance(ConvexHull3D.cs:27, doc says "The volume of the Convex Hull.");peakVertex/peakDistancepublic-lowercase.CreateConvexHullMinimalaccepts atoleranceit never uses (.2D.cs:48-74). - Silent nulls and misleading flags.
MinimumBoundingCylinder(IList, direction)returns null for null/zero direction (:418-419) while the multi-direction overload then NREs;Delaunay3D reuseInputVerticesdoesn't;out vertexIndicesbroken — the advertised way to map hull points back to inputs doesn't work. - KD-tree surface. Twenty near-identical Create overloads, yet dimension — the one thing that matters — is hardwired to 3 and wrong for 2D.
FindNearest(target, numberToFind = -1)= "all points, sorted" is a surprising default.
- KD-tree Dimensions = 3 for 2D points (wrong nearest neighbors).
- GetDistanceToExtremePoints bottom/top swap.
- Stale-rectangle side points in rotating calipers.
- vertexIndices out-param broken in 3D/4D hull Create.
- (long)(n * n) overflow + one-sided jiggle in 4D.
- Delete MinimumCircleCylinderOld.cs and GaussianSphere.cs (dead, compiled, one carries a known bug).
- Absolute iteration caps for MinimumSphere/MinimumGaussSpherePlane; remove Desktop CSV write and Console.WriteLines.
Scope: all 29 files under Implicits and Primitives\. All findings verified by reading cited code.
- BoundingBox.cs:193 —
Boundscomputed from unit vectors, not scaled vectors.GetBoxBounds(Directions, TranslationFromOrigin)passes normalizedDirections;GetBoxBounds(:198) expects dimension-scaledVectors. Every box with dimensions ≠ 1 gets a wrong AABB; also breaksIntersects(:649) for axis-aligned pairs. - BoundingBox.cs:549–552 —
Copy()loses the box dimensions. Constructor falls back to unit lengths → copy is a 1×1×1 box. (BoundingBox<T>.Copyat :98 passes Dimensions correctly.) - BoundingBox.cs:563–576 —
MoveFaceOutwarddestroys dimensions (builds from unit Directions; result dimensions(1+distance, 1, 1)). - BoundingBox.cs:364 —
Centerignores dimensions (uses unit directions; must use Vectors). - BoundingBox.cs:598 —
LineIntersectionupper point wrong for oriented boxes (TranslationFromOrigin + Dimensionsadds dimensions as global-frame vector). - BoundingBox.cs:255–264 —
TransformFromUnitBoxmissing the scale — byte-for-byte same asTransformFromOrigin; round-trip withTransformToUnitBox(:272) ≠ identity. - StraightLine3D.cs:123 — wrong direction in 2-point fit.
(dir - anchor).Normalize()where dir = p2 − anchor → direction = p2 − 2·anchor. Should bedir.Normalize(). - StraightLine3D.cs:134–139 — singular-matrix fallback always yields UnitZ (missing
else). - Cone.cs:208–210 —
TransformFrom2DTo3Dadds the Y-term twice, omits the X-term. Every 2D→3D mapping on a cone is wrong. - GeneralQuadric.cs:252 — quadric transform uses the matrix instead of its inverse.
M · Q · Mᵀinstead ofM⁻¹ Q M⁻ᵀ— surface transformed by the inverse of the requested transform (correct only for origin rotations). - GeneralQuadric.cs:882 — sphere→quadric W term typo.
Center.Z + Center.Zshould beCenter.Z * Center.Z— every converted sphere has wrong constant term. - GeneralQuadric.cs:1020–1021 —
SetQuadricTypebuilds the A-matrix withYSqdCoeff / 2on the diagonal (should be YSqdCoeff) → quadric classification unreliable whenever YSqdCoeff ≠ 0. - GeneralQuadric.cs:739–742 — least-squares fit error computed incorrectly. Adds
Wonce instead of N·W;errSqd *= errSqd / numVertsgives (ΣQ)²/N not Σ(Q²)/N. - Capsule.cs:264 — double subtraction in cone-section distance.
t = (dxAlong - conePlaneDistance1) / coneLengthbut dxAlong already relative to coneAnchor1 (:258). Should bet = dxAlong / coneLength. Wrong for any capsule not near the origin. - CappedCylinder.cs:50–64 —
DistanceToPointwrong beyond the caps (returns distance to cap rim circle instead of axial distance to cap face);IsPositivesign flip (:68) applied only in-body branch. - Prismatic.cs:180–190 — constructor uses
Verticesbefore they exist (GetDistanceToExtremeVertex(Vertices, …)at :184 beforeSetFacesAndVertices(faces)at :189 → null; compare Cylinder.cs:261–272 correct ordering). - Torus.cs:147 — inverse transform built by negating a matrix.
backTransform * -translate— operator-negates every element, not the inverse of a translation.TransformFromYPlane(typo'd name, :132) returns garbage. - GeneralConicSection.cs:306 — circle center typo:
new Vector2(x, x)should be(x, y). - PrimitiveSurfaceExtensions.cs:1031–1032 —
TessellateHollowCylinderaxis offsets inverted sign (compare correctTessellate(Cylinder):951). Tube mirrored about anchor. - PrimitiveSurfaceExtensions.cs:1073 —
TessellateHollowCylinderomitsinnerFacesfrom the solid → open inner wall; alsobtmPlane(:1076) uses +Axis where :989 correctly uses −axis. - SphericalZBuffer.cs:106 — base
XLengthnever set (local shadows inherited property) → inherited wrap logic (CylindricalZBuffer.cs:185–208) adds XLength (=0) — seam wrapping broken for spherical buffers. MaxX/MaxY/YLength also unset. - CylindricalZBuffer.cs:398 / SphericalZBuffer.cs:148 —
Get3DPointdouble-counts the radius (radius = baseRadius + flatPoint.Zwhere heights already absolute). - Plane.cs:490–496 —
DotCoordinatesign wrong for TVGL's convention. ComputesN·p + DistanceToOriginvsDistanceToPoint(:579)N·p − DistanceToOrigin— System.Numerics port where D = −distance wasn't sign-flipped.
- Sphere.cs:282 —
PointMembershipgradient wrong (distance * v / vLength; gradient of |v|−R is v/|v|; three-out-param overload :298 correct). - Sphere.cs:358–364 — tangent line yields duplicate intersections (missing
yield break; 3 results possible). Cylinder.cs:376–385 identical flaw. - Cylinder.cs:370–386 — line parallel to axis divides by zero → two garbage intersections instead of none.
- CappedCylinder.cs:123–158 —
LineIntersectionmisses axis-parallel lines through the caps; cap hits never radius-checked when only one wall intersection. - Torus.cs:434–452 —
LineIntersectionmixes normalized and unnormalized directions (quartic solved with normalized, results reconstructed with caller's raw direction).lineTconventions inconsistent across surfaces. - Torus.cs:122–150, 155–174 — stale cached transforms — never invalidated in
Transform()or Center/Axis setters. - Torus.cs:430 —
GetSegmentsWithRadius(target, …)compares againstMinorRadiusinstead oftarget(latent); double enumeration via Count() (:415/419). - Plane.cs:446–481 —
Transform(Quaternion, bool)never transforms faces/vertices (calls base with Identity); cached XY transforms not invalidated. - PrimitiveSurface.cs:173 — dead NaN guard (
if (double.IsNaN(d)) ;) — NaN poisons mse. - PrimitiveSurface.cs:953–989 —
Copy(bool)leaves stale cached references (_largestFace,_adjacentSurfacespoint at original solid;ResetFaceDependentValuesdoesn't clear them). - SurfaceGroup.cs:192–211 — closest-surface selection uses signed distance (deeply-inside negative always wins). Use |d|.
- SurfaceGroup.cs:71–117 — Faces/Vertices/KeyString caches never invalidated after AddPrimitiveSurface/Combine.
- UnknownRegion.cs:198–204 — inverted face pick in
GetNormalAtPointedge branch (sentinel +∞ guarantees the excluded face is chosen). - Circle.cs:249–260 —
IntersectWithCirclereturns NaN for contained circles (only> R1+R2rejected;< |R1−R2|→ sqrt(negative), returns true with NaN points). - GeneralConicSection.cs:162–171 — inconsistent discriminant conventions (B² ≈ A·C vs A·C − B² vs correct 4AC − B² at :433);
B = Math.Sqrt(A*C)can be NaN and drops sign (:164). - PrimitiveSurfaceExtensions.cs:796 —
Voxelizecompares a line parameter to a coordinate (lineT > result.XMax; should be XMax − XMin); minJ/minK not clamped ≥ 0 (:781–784);GetPrimitiveAndLineIntersections(:1245) anchors ray at x=0 in faces branch but x=XMin in primitive branch — two frames, filter wrong for one. - ZBuffer.cs:113–124 —
PrimitiveZminspath nearly unreachable and NRE-prone. - CylindricalZBuffer.cs:168–174 — faces with 1 or 3 wrapping edges silently dropped (empty-statement branches; comments admit cases occur).
- BoundingBox.cs:649–668 — OBB
Intersectsincomplete (corner containment both ways misses SAT edge-edge cases). - BorderLoop.cs:118–127 —
Curvaturesetter overridden by lazy getter (doesn't set_curvatureIsSet; silently breaksCopy:261). - BorderLoop.cs:343–355 —
UpdateIsClosedthrows on empty loop (indexesSegmentDirections[^1]). - ImplicitSolid.cs:285–292 — mixed field conventions in CSG evaluation (QuadricValue algebraic vs metric signed distance; some surfaces return unsigned distances in regions) — min/max CSG not commensurate; breaks
PointIsInside.
- Cone.cs:313–336 —
SetPrimitiveLimitslacks the NaN guards Cylinder.cs:328–333 applies; Torus.cs:399–410 likewise. - Cone.cs:254–257 — asymmetric wrap correction (subtracts 2·halfRepeatAngle one way, adds halfRepeatAngle the other).
- Cone.cs:307–308 —
CalculateIsPositivemutatesAxisas a side effect of a lazy property read. - Helix.cs:82 —
DeterminePitchdivides byline.Direction.X, no zero check. - GeneralQuadric.cs:362 —
new Random()inside retry loop; offset biased to positive octant. - GeneralQuadric.cs:842 —
DefineAsConeNotImplementedException for axis-aligned-Y;DefineAsCylinder(:808) divides by axis.Z; sqrt calls can yield NaN (780, 789, 811). - Plane.cs:519–525 —
SameLocationexact==on doubles; doesn't recognize negated normal/distance. - Capsule.cs:229–246 — degenerate
Anchor1 == Anchor2unhandled (NaN). - StraightLine2D.cs:115 — exact
yCoeff == 0branch; 1/xCoeff Infinity if also ~0. - Sphere/Cone/Torus
faceXDirlazily defaults — same 3D point maps to different 2D coords depending on call history. - Serialization: GeneralQuadric.cs:21 uses System.Text.Json
[JsonIgnore]while library serializes with Newtonsoft → attributes ignored, StationaryPoint's solve may run during serialization.PrimitiveSurface.FaceIndices(:274) refreshes only when shrunk → stale after face loss.BorderSegment.CircleCenter(:152) public field.BorderSegment.Copy(:293) triggers SetCurve on source; copy's_curvediscarded.
- CappedCylinder.cs:98–115 duplicates Cylinder.cs:320–346 (
SetPrimitiveLimitsfinite branch verbatim minus NaN guards). High-value cleanup. - CylindricalZBuffer.cs:210–327 duplicates ZBuffer.cs:242–396 — entire ~120-line triangle spine-walk rasterizer copy-pasted (only
xIndex % XCountdiffers). AWrapX(int)hook collapses them. Same for wrap preamble at CylindricalZBuffer.cs:183–209 vs 334–366. - ZBuffer.cs:144–169 vs 175–185 —
UpdateZBufferWithSurfacere-implementsUpdateZBufferWithFaceinline. - Prismatic.cs:408–416 == UnknownRegion.cs:225–233 — identical face-ray LineIntersection loops (also PrimitiveSurfaceExtensions.cs:1243–1249).
- Prismatic.cs:304–374 — DistanceToPoint/ClosestPointOnSurfaceToPoint/GetNormalAtPoint each repeat the same closest-segment scan.
- Sphere/Cylinder/Cone/Torus/Prismatic
Transformall repeat the "transform two perpendicular radius vectors and RMS-average lengths" idiom — extractable helper. - Dead code: Prismatic.cs:379–399 (unreachable after return); Capsule.cs:289–297 (unreachable after throw); PrimitiveSurface.cs:509–544 (~35 commented lines); Cylinder.cs:389–404 (commented AsTessellatedSolid); ImplicitSolid.cs:463–465 (duplicate if); Grid.cs:341–342 (debug no-op); ZBuffer.cs:171–173 orphaned doc.
- Unused usings: GeneralQuadric.cs:14/20, Capsule.cs:17, BoundingRectangle.cs:17, Grid.cs:14.
- SurfaceGroup.cs:171–175 —
new SetColorshadow-hides an equivalent base method. - Overlapping ZBuffer implementations: ZBuffer/CylindricalBuffer/SphericalBuffer carry own
Initializevianew-hiding (CylindricalZBuffer.cs:117, SphericalZBuffer.cs:101) — redundancy + correctness trap (base-typed call runs wrong one; see XLength bug).
- Prismatic.cs:281–298 — Dijkstra node stores full copied path list per node (O(V²)); store parent ref. Line 267 uses
LengthSquared()as additive edge weight — doesn't minimize geometric length. - GeneralQuadric.cs:380–390 —
GetNearbyPointOnQuadricre-enumeratesLineIntersectionviaGetEnumerator().MoveNext()in loop condition and again at :389/390; enumerators never disposed. - UnknownRegion.cs:151–188 —
GetClosestfull O(faces+edges+vertices) scan per query with per-call Concat allocation; three public methods each redo it. - ZBuffer rasterization single-threaded: ZBuffer.cs:116–117, CylindricalZBuffer.cs:99–100;
Voxelizehas Parallel.For scaffolding commented out (:787, :802 — idempotent writes, safe).Grid.ConvertGridToPolygonstriple-N scan (Grid.cs:423–427) also parallel-friendly. - Multiple enumeration of IEnumerable params: StraightLine3D.cs:86/119/177; Circle.cs:105/182; StraightLine2D.cs:79/123; Plane.cs:116–128 (+
faces.Count()in SetFacesAndVertices, PrimitiveSurface.cs:75); Sphere.cs:371–375;SphereIsTooFlat(:381) O(n²) when O(n) suffices. - Cylinder.cs:355 — instance
LineIntersectioncalls.ToList()on 0–2 element iterator per ray test (hot path in Voxelize). - Per-call allocations:
BoundingBox.LineIntersection(:599–601) 6-element array + List per call;SortedDimensionsShortToLong/SortedDirections…(BoundingBox.cs:469–539) fresh arrays per property read;ShortestDimension/LongestDimensionallocate an array to read one element. - Span opportunities:
GetVerticesFromModifyEdges(PrimitiveSurfaceExtensions.cs:663),GetCircleTessellationpooled/stack storage; Grid deltas jagged int[][] (Grid.cs:530) → flat ReadOnlySpan. - Virtual dispatch in tight loops:
GetIndicesCoveredByFace/PixelIsInsidevirtual per pixel; seal leaf classes (ZBuffer doc says "cannot be inherited" but class not sealed, ZBuffer.cs:21–23);SurfaceGroup.DistanceToPointLINQ Min over polymorphic calls.
- Inheritance design.
SurfaceGroup : PrimitiveSurfaceimplements ~10 abstract members asthrow NotImplementedException(177–205, 223) — composition fits better.CappedCylinder : Cylindermeans capped is-a infinite cylinder to type checks (e.g.GeneralQuadric.FromPrimitiveSurface:883 converts capped → infinite quadric cylinder). ZBuffer family'sstatic new Run(...)that throws (CylindricalZBuffer.cs:37–39, SphericalZBuffer.cs:36–38) andnew-hidden Initialize are the weakest parts. - Mutable state after construction. Nearly all primitives expose
{ get; set; }geometry with lazy caches that don't watch them:Cylinder.Axisset doesn't clear faceXDir/faceYDir (Cylinder.cs:50–76);Plane.Normalset doesn't clear_asTransformToXYPlane(Plane.cs:58–94); Torus above. GeneralQuadric uses init coefficients but exposes mutable public fields a,b,c,d,e (:144–148) and settable Type.Sphere/Cone/Torus.TransformFrom3DTo2D(IEnumerable…)mutate faceXDir/faceYDir/faceZDir as a side effect of a query (Sphere.cs:227–238) — order-dependent, thread-unsafe. - Tolerance handling inconsistent. Mix of Constants.BaseTolerance, bare literals (1E-3 RemoveSmallCoefficents, 1E-12 SetQuadricType, 1E-4/1E20 SQP loop GeneralQuadric.cs:419/427, 0.02 rad Circle.cs:173, 1.67π PrimitiveSurfaceExtensions.cs:108, dotAligned 0.999 BoundingBox.cs:136), scale-dependent absolute checks.
PixelIsInside(ZBuffer.cs:403/419) mixes strict with IsLessThanNonNegligible on one barycentric only. - XML docs: boilerplate ("Points the membership." on every DistanceToPoint),
<c>true</c> if XXXXplaceholders, wrong summaries (Cylinder.Tessellate documented "for a cone" PrimitiveSurfaceExtensions.cs:921–928; Torus.KeyString doubled "|"; "Primsatic|" typo Prismatic.cs:144), param lists not matching signatures (Sphere.cs:338–349).Cone.PracticalMinAperture/MaxAperture(Cone.cs:26–27) public consts unused. BoundingRectangle.CornerPointsthrows on 1% area mismatch (BoundingRectangle.cs:146) — assertion as control flow in a read-only accessor.
- Fitting entry points scattered and inconsistent:
Plane.FitToVertices/DefineNormalAndDistanceFromVertices,Sphere.FitToVertices/DefineSphereFromVertices/CreateFrom{2,3,4}Points,GeneralQuadric.DefineFromPoints,Circle.CreateFromPoints,StraightLine3D.CreateFromPoints. Return styles differ (nullable vs bool+out vs always-succeed); error semantics differ. A uniformTryFit(points, out surface, out error)per primitive would help. - Property naming exceptions: Cone
Apertureis a slope not an angle (doc :63–68 must explain; renameHalfAngle/ApertureSlope); Sphere/TorusCentervs CylinderAnchorvs CapsuleAnchor1/2; PrismaticBoundingRadius.Cylinder.MinDistanceAlongAxisis offset from origin along axis, not from Anchor — undocumented; TessellateHollowCylinder got it wrong. Circle(Vector2 center, double radiusSquared)is a foot-gun (Circle.cs:60) — passing a radius compiles fine; no FromRadius factory.IsPositive-dependent signed distances surprising: sign flips with IsPositive on Plane/Sphere/Cylinder/Cone/Torus/Capsule but not Prismatic, partially CappedCylinder, via QuadricValue on GeneralQuadric. Documenting/enforcing one convention on the abstractDistanceToPointis the single highest-leverage doc fix.- 65 KB
PrimitiveSurfaceExtensionsmixes ≥5 concerns: border/axis queries, generic property access (GetAxis/GetAnchor/GetRadius), mesh surgery (TrimTessellationToPositiveVertices ~250 lines), tessellation factories (Tessellate ×9), voxelization. Split into PrimitiveBorderExtensions/PrimitiveTessellation/PrimitiveTrimming. GetAxis/GetAnchor/GetRadius (176–230) are the de-facto polymorphic API — better as virtual members on PrimitiveSurface so new primitives can't be forgotten (Capsule was bolted on; GetRadius mixes if/else-if styles :216–219).GetAxison GeneralQuadric returns unordered eigen axis — unspecified. ICurvestatic-abstractCreateFromPointsconstrained toIVector2D(ICurve.cs:43) forces 3D implementers into runtime downcasts (StraightLine3D.cs:88);Helix.CreateFromPointsjust throws. Split ICurve2D/ICurve3D.- BoundingBox ergonomics: three near-identical transform property names where two are currently identical (bug);
MoveFaceOutward(int, bool, …)hard to call correctly (and broken); the CartesianDirections overload (:583) is the usable one.
Abbrev: IO/ = InputOutput Operations\. Every finding verified by reading the cited code; XmlSerializer collection-initialization behavior verified empirically on net10.0 (several "null collection" suspicions were checked and dropped).
-
Culture-sensitive double parsing breaks read AND write on comma-decimal locales.
IO/IOFunctions.cs:738, 762—Double.TryParsewith noCultureInfo.InvariantCultureinTryParseDoubleArray— the parse path for ASCII STL (STLFileData.cs:183,191) and OFF (OFFFileData.cs:164,182,205). On fr-FR/de-DE,"1.5"fails → IOException or false.IO/IOFunctions.cs:1121–1249— all threeReadNumberAsInt/Float/Double(string, Type)overloads culture-sensitive — the ASCII PLY value parsers (PLYFileData.cs:556,572,621,624).IO/3mf.classes.cs:110— 3MFtransformattributes → NaN on comma locales.- Writers equally broken: ASCII STL
STLFileData.cs:368–372, OFFOFFFileData.cs:288–306, ASCII PLYPLYFileData.cs:812,821use current-culture ToString → files containing1,5no tool can re-read. Only OBJ (OBJFileData.cs:326–331,372,409) and CSV/SVG/DXF polygon readers (IOFunctions.cs:503,555,636) are correct.
-
File.OpenWritenever truncates — saving over a longer existing file leaves trailing garbage.IOFunctions.cs:1270, 1351, 1435. Should beFile.Create. (Polygon saver deletes first,:1516.) -
Binary STL color channels swapped between reader and writer. Reader: blue bits 0–4, red bits 10–14 (
STLFileData.cs:294–313); writer packs red 0–4, blue 10–14 (:446–452). TVGL-written colored binary STL re-opened by TVGL has R/B exchanged. -
Binary STL writer can emit a header shorter than 80 bytes.
STLFileData.cs:406–408truncates >80 but never pads shorter → 4-byte face count lands inside header → unreadable everywhere. Truncation by char count then UTF-8 re-encode — non-ASCII comments can exceed 80 bytes. -
OBJ:
wcoordinate parsed from wrong index.OBJFileData.cs:331—values[2]should bevalues[3]. 4-component vertices divide x,y,z by z. -
OBJ: middle
ggroups silently dropped.OBJFileData.cs:259–277— newobjSolidin the else branch never added toobjData; only the final one rescued at:313–314. Forg A / g B / g C, solid B's faces lost entirely. -
OFF files routed to the PLY parser in two of three Open dispatches.
IOFunctions.cs:313–316and:413–416sendFileType.OFFtoPLYFileData.OpenSolid→ returns null → array overload returnstruewithnew[]{ null }. Only single-solid overload (:148–149) uses OFFFileData. -
Binary PLY header offset wrong for CRLF and BOM.
PLYFileData.cs:143–144, 220–221countline.Length + 1assuming LF-only, no BOM; seek at:151lands mid-header/mid-data for Windows-authored binary PLY. (TVGL's own writer strips CRs at:856–862for this reason.) -
Binary PLY: non-color extra vertex properties desynchronize the stream.
PLYFileData.cs:755–786— when property is not xyz andvertexColorDescriptor == null: NRE on nullvertexColors, and the property's bytes are never read → PLY with nx/ny/nz normals (extremely common) crashes or reads misaligned garbage. Color path also assumes every non-xyz property is a color (:761). -
SaveToStringproduces mojibake for every format.IOFunctions.cs:1330–1336, 1412–1418, 1450–1456decode withEncoding.Unicode(UTF-16) but writers emit UTF-8; also mismatchesOpenFromString(UTF-8,:215) — advertised round-trip cannot work. -
All writers silently drop user comments/metadata: inverted Where predicate.
.Where(string.IsNullOrWhiteSpace)keeps only blank comments in six places:STLFileData.cs:349, 404;OFFFileData.cs:286;PLYFileData.cs:927;3MFFileData.cs:347;AMFFileData.cs:251. Metadata reconstruction blocks (3MFFileData.cs:348–361,AMFFileData.cs:253–265) are dead code in practice.
ReadExpectedLinerewind fundamentally broken with buffered StreamReader.IOFunctions.cs:777–785— saved BaseStream.Position already past read data; reset doesn't discard reader buffer; throws on non-seekable streams. Used by ASCII STL (STLFileData.cs:187,204).- 3MF
OpenModelFileNREs on malformed input; try/catch commented out.3MFFileData.cs:134–135, 169–175;md.typenull → NRE at:154. - 3MF
.relsoutput invalid.3MFFileData.cs:421–423serializes onlyrels[0]as bare<Relationship>root — no<Relationships>container; thumbnail relationship never written yet emptyMetadata/thumbnail.pngentry created (:290). - 3MF comments lost even without the filter bug.
3MFFileData.cs:89–97— Comments getter builds a new list each call;AddRangeat:376mutates a temporary. - Binary STL: strict length check rejects legitimate files and defeats the multi-solid loop.
STLFileData.cs:243–245; re-evaluated with samelength - 84on second do-while iteration (:241–257). Failure falls intoTryReadAsciiwhich always returns true (:172) → silent empty result. - ASCII STL: solid name assignment bug.
STLFileData.cs:157–160— missing else: default name unconditionally overwritten; unnamed solids get "". Successor solids (:168) lose FileName/Units. - PLY vertex-color averaging divides by 4 but sums 3 vertices (
PLYFileData.cs:174–182, 25% darkened). Also both readers push one Color per color component not per vertex (:646, :783inside the per-property loop) → vertex colors scrambled. - Binary PLY with an
edgeelement always throws.PLYFileData.cs:490–492→ReadEdges(BinaryReader)=throw NotImplementedException(:662–664); ASCII version skips gracefully (:534–542). BackoutToFolder/BackoutToFileNRE at filesystem root.IOFunctions.cs:243–264— loop condition dereferencesdir.FullNamebefore the null guard inside.- OFF writer emits corrupt face lines when a face has no color.
OFFFileData.cs:270, 304— missing leading space:"3 0 1 20.5 0.5 0.5". Writes 4 color components (:301–302) while reader accepts exactly 3 (:223) — colors never round-trip;solid.SolidColorNRE if null (:270). - OBJ multi-solid early return.
OBJFileData.cs:132—if (!tsBuildOptions.PredefineAllEdges) return new[] { ts };inside the per-solid loop discards all solids after the first. - Zip/stream disposal gaps in 3MF read.
3MFFileData.cs:115ZipArchive never disposed;:118deflate stream never disposed (XmlReader CloseInput=false default). SolidAssembly.StreamReadre-adds solids to an already-deserialized RootAssembly.SolidAssembly.cs:261–264— duplicates part references with Identity transforms; relies on dictionary insertion order.SaveToString(Solid)with the default argument always throws.IOFunctions.cs:1330defaults toFileType.unspecified→ NotSupportedException (:1319–1322).
- Host little-endianness assumed throughout
ReadNumberAs*(IOFunctions.cs:872–1064) and binary writers;BinaryPrimitiveswould be explicit and faster. - Non-seekable streams break format fallback (
STLFileData.cs:106,OFFFileData.cs:122,PLYFileData.cs:151). OFFFileData.cs:159,177,199—line.Remove(0, 1)result discarded (no-op).TryParseNumberTypeFromString:uint64captured by thelongbranch (IOFunctions.cs:1080–1083, ulong branch unreachable);int8maps tobyte(:1101).InferUnitsFromCommentsregex lowercase-only (:813) — "MM"/"Inches" → mangled tokens.- PLY: NRE on empty file (
:140), missing end_header (:220);element uniform_colorwritten with no count (:947) — non-standard. - 3MF
requiredextensions(3MFFileData.cs:103) and AMFversion(AMFFileData.cs:88) lack[XmlAttribute]. - AMF supports only one
volumeper mesh (amf.classes.cs:76) — multi-volume meshes lose all but first. CurrentCultureIgnoreCasefor token comparisons everywhere — should be OrdinalIgnoreCase (tr-TR I/i).
- High — the Open dispatch switch is written three times with drifting behavior (source of the OFF→PLY bug):
IOFunctions.cs:130–159, 295–332, 390–428. One private dispatcher would collapse these. - High —
ReadNumberAsInt/Float/Doublein six near-identical copies (3× BinaryReader:872–1064, 3× string:1117–1250), each an if-ladder over 9 Types. Generic helper/TypeCode switch removes ~350 lines. - Medium — PLY color-property recognition copy-pasted 3× (
PLYFileData.cs:280–312, 341–374, 381–417); whole ReadMesh/ReadVertices/ReadFaces/ReadUniformColor family duplicated for StreamReader vs BinaryReader (:436–499, 506–792). - Medium —
"type ==> value"metadata split duplicated in 3MF (:347–361) and AMF (:251–265), same//todoin both. - Medium —
TVGLFileData.OpenTVGLzoverloads byte-for-byte duplicates (TVGLFileData.cs:46–67vs:73–93); threeOpenFromStringoverloads too (IOFunctions.cs:213–240, 349–360, 444–455). - Dead code: commented serializer experiments (
TVGLFileData.cs:34–40, 215–239);OFFFileData.TryReadBinary=return false; throw ...(:246–251); unusedConvertDoubleArrayToString/...(IOFunctions.cs:1550–1568);SolidAssemblyserialization hooks gated by always-false flag (:270–297); OBJFaceToNormalIndicespopulated never consumed (OBJFileData.cs:305); PLY dead null check (:408);STLFileData.Normalscollected never used (:206,321). - Legacy cruft: dual 2013/01 vs 2015/02 3MF material models (
3mf.classes.cs:481–645);#if helptoggles across 3mf/amf classes.
- High — ASCII STL keeps the entire file in a List and regex-scans all of it.
STLFileData.cs:146–150adds every line (7/facet) tocomments;InferUnitsFromComments(IOFunctions.cs:809–818) regexes every one at each endsolid. 1M-facet STL ≈ 7M regex invocations + O(file) memory. Only#comment lines should be collected. - High — ASCII STL facet parsing: two Regex objects per line (
STLFileData.cs:50,55at:183,191).ReadOnlySpan<char>slicing +double.TryParse(span, InvariantCulture)= order-of-magnitude win, fixes culture bug simultaneously. - High — binary parsing allocates a byte[] per scalar + LINQ
Reverse().ToArray().IOFunctions.cs:872–1064; a binary STL face = 13 allocations (STLFileData.cs:267–288). Read each 50-byte facet into stackalloc/reused buffer +BinaryPrimitives.ReadSingleLittleEndian. Same for binary PLY (PLYFileData.cs:705–792). Also:288–313decodes attribute word viaConvert.ToString(value, 2).PadLeft(16,'0').ToCharArray()— bit ops suffice. - Medium — per-line string.Split across OBJ/PLY/OFF. OBJ re-splits every vertex token twice (
OBJFileData.cs:303, 348, 370); PLY per vertex/face line (:555,615); OFFSplit().ToList()+ RemoveAll per line (IOFunctions.cs:733–734).MemoryExtensions.Splitover spans removes per-line arrays. - Medium — dispatch on System.Type objects per value (
List<Type>+ if-ladder oftype == typeof(...)per scalar); resolve an enum once in the header. - Medium — no parallelism for independent meshes. 3MF objects (
3MFFileData.cs:161–165), multi-solid ASCII STL (STLFileData.cs:117–123), OBJ sub-solids (OBJFileData.cs:125–138) build TessellatedSolids sequentially; data-independent, trivially Parallel.ForEach-able. - Medium — PLY binary save header hack (
:853–862MemoryStream→ToArray→ToList→RemoveAll(13)→ToArray) —StreamWriter.NewLine = "\n"does it with zero copies. - Low — writers concatenate strings per line;
SaveBinarySTL allocates via BitConverter per float (STLFileData.cs:431–454);GetMostSignificantSolidre-enumerates its lazy Where chain five times (IOFunctions.cs:181–204); no BufferedStream for BinaryReader paths.
- Error reporting inconsistent and mostly invisible (Medium). Failures logged as Information with magic verbosity ints, occasionally Log.Error, sometimes raw Debug.WriteLine. Array-overload catch (
IOFunctions.cs:334–339) swallows everything incl. the NREs above; single-solidOpen<T>has no catch — two contradictory contracts for the same files. - Extension dispatch (Medium). Unknown extension on save → NotImplementedException (
:699); on open → log-and-return. SHELL/DXF/DWG/SVG/CSV only partially wired. - XML (Medium/Low). 3MF read via XmlReader.Create is XXE-safe; AMF via
XmlSerializer.Deserialize(TextReader)(AMFFileData.cs:108–110) doesn't prohibit DTD processing — wrap withDtdProcessing.Prohibitto close billion-laughs DoS. - Newtonsoft.Json (Low/Medium).
Save(Polygon)usesTypeNameHandling.Auto(IOFunctions.cs:1505) whileOpenPolygonFromJsonuses a default serializer (:619–625) — polymorphic content won't round-trip; never enable TypeNameHandling on read without a binder (gadget risk). Hand-rolled streaming via WriteRawValue (SolidAssembly.cs:195–197) is fragile. - Zip (Low). No zip-slip (nothing extracted to disk); no decompression-bomb guard in 3MF/TVGLz; 3MF entry discovery by
.EndsWith(".model")ignores OPC_relsindirection — silently loads nothing for unconventional packages. SolidAssembly.StreamWritesilently drops non-tessellated solids (Medium). Only TessellatedSolids written (SolidAssembly.cs:204); CrossSectionSolid/VoxelizedSolid vanish on save with no warning.TVGLFileData.SaveToTVGL(Stream, Solid)hard-casts (:195); bare catch (:208) converts InvalidCastException into silent false.- No async surface (Low). Everything sync I/O; no non-blocking option for UI/server hosts.
- Three return conventions coexist.
Open(string)→ possibly-null Solid;Open<T>(string, out T)→ bool but throws FileNotFoundException and lets parser exceptions escape;Open(Stream, string, out TessellatedSolid[])→ bool, swallows all. Parsers themselves disagree (STL/OFF/PLY null, 3MF throws NRE, AMF catch-and-null). Pick one contract, enforce in the shared dispatcher. Open<T>conflates "file unreadable" with "wrong solid type" (IOFunctions.cs:160–161).- Hidden data reduction on open. Single-solid Open quietly runs
GetMostSignificantSolid(:134–143), discarding "insignificant" bodies by heuristic (:178–205) with only Debug.WriteLine traces. Should be opt-in or surfaced. - Format auto-detection extension-only at top level. Binary STL renamed .ply fails outright; a magic-byte sniffer ("solid", PK, "ply", "OFF", "#") would make
Open(Stream, filename)robust. TessellatedSolidBuildOptionsintegration uneven. Null-defaulting in OBJ/AMF/TVGL but not STL/3MF/PLY/OFF. OBJ changes the number of solids returned based onPredefineAllEdges(OBJFileData.cs:132).ReferenceIndexdoubles as a TVGL-file selector (TVGLFileData.cs:118–121) — scope creep.- Save API advertises formats it can't write.
Save(..., FileType.OBJ)→ NotImplementedException (OBJFileData.cs:426–429viaIOFunctions.cs:1295–1296);SaveToString(solid)throws with its own default.CanSave(FileType)/support matrix or remove dead cases. - Positives: stream-based overloads, FileType-explicit OpenFromString, unit inference, 3MF component/transform recursion (
3MFFileData.cs:185–223) are good API bones.
- Invariant-culture parsing/formatting everywhere — currently breaks half the world's locales both directions.
File.OpenWrite→File.Create(IOFunctions.cs:1270,1351,1435) — silent file corruption.- Route
FileType.OFFto OFFFileData in all three dispatchers; de-duplicate the dispatch. - Fix inverted
Where(string.IsNullOrWhiteSpace)in all six writers — silent metadata loss. - Pad binary STL header to exactly 80 bytes; fix the R/B bit swap.
All findings verified by reading cited code; call-graph reachability checked by grep where noted.
1.1 VoxelRowDense.Subtract(subtrahends, offset) calls Union instead of Subtract — VoxelRowDense.cs:271-276. Subtracting from a dense row adds the subtrahend's voxels. Latent through public API only because VoxelizedSolid.Subtract (PublicFunctions.cs:155) forces rows sparse first.
1.2 VoxelRowDense.Intersect with a sparse operand is a silent no-op — VoxelRowDense.cs:253-259. Sparse branch's loop body is an empty statement (;//this doesn't work IntersectRange(...)). Even per-range IntersectRange would be wrong — intersection must turn off everything outside the ranges (as VoxelRowSparse.Intersect at VoxelRowSparse.cs:312-320 correctly does).
1.3 VoxelEnumerator.MoveNext never visits voxel (0,0,0) — VoxelHelperClasses.cs:94-113, 118-121. First MoveNext() immediately increments; (0,0,0) never yielded; Reset() restores broken state.
1.4 CrossSectionSolid.Copy() destroys the source solid then throws NRE — CrossSectionSolid.cs:338: Layer2D = new List<Polygon>[NumLayers]; assigns to THIS (the source), then :341 dereferences null layers. Line should be deleted (ctor at :334 already allocates solid.Layer2D).
1.5 CrossSectionSolid.Reverse() does nothing — PublicStaticMethods.cs:216-223. newLayers built/reversed but never assigned back; _firstIndex/_lastIndex not invalidated; only StepDistances replaced → internally inconsistent.
1.6 CrossSectionSolid.CalculateSurfaceArea() never assigns _surfaceArea — CrossSectionSolid.cs:532-552. Computed area discarded (compare VoxelizedSolid.cs:710).
1.7 VoxelizedSolid.CalculateCenter() — wrong formula, missing offset, int overflow — VoxelizedSolid.cs:659-684:
xTotal += rowCount * voxelRow.AverageXPosition()— denseAverageXPosition()(VoxelRowDense.cs:368-389) returns the sum, so multiplying by rowCount over-weights; sparse version (VoxelRowSparse.cs:499-513) returns 2× the sum — dense and sparse differ ~2× for same content._centerin local voxel space (noBounds[0]offset, no half-voxel) while every other Solid reports world coords. Author's//is this right?comment.xTotal/yTotal/zTotalareint; overflow for realistic grids (1000³).Count == 0→ NaN, no guard.
1.8 MarchingCubesCrossSectionSolid.GenerateOnLayers — ring-buffer off-by-one gives every cube a stale top layer — MarchingCubesCrossSectionSolid.cs:141-148 + GetValueFromSolid :576-581. At iteration k, MakeFacesInCube(i, j, k) reads z = k+1 → slot (k+1)%2 = layer k−1, not k+1. GetValue (MarchingCubes.Base.cs:255-275) caches the wrong values permanently. Should be MakeFacesInCube(i, j, k - 1).
1.9 Inconsistent yStartIndex conventions for AllPolygonIntersectionPointsAlongHorizontalLines — implementation (PolygonOperations.IsInside.cs:699-756) appends an entry for every step from startingYValue (contradicting its own doc :691).
FillInFromTessellation(VoxelizedSolid.cs:270-276) matches. ✔CreateFrom(polygons…)(VoxelizedSolid.cs:232-241) shifts rows byyStartIndexand doesn't clampyStartIndex + j→ misregistration + potential IndexOutOfRange. ✘MarchingCubesCrossSectionSolid.CreateDistanceGrid(:262-269) assumes list omits leading empties → every grid row gets the intersections of the row below. ✘
1.10 BooleanOperation(PrimitiveSurface, …) drops the trailing range — VoxelizedSolid.Advanced.cs:126-150. Odd crossings count with startDefined == true → final [start, numVoxelsX) never applied (multi-surface overload handles it, :278). Subtract(plane) can be a no-op on most rows; for Intersect (inverseRange), rows outside the surface bbox and zero-crossing rows (:121) never cleared → AND semantics wrong beyond bbox.
1.11 Multi-surface BooleanOperation inserts a world coordinate into a list of line parameters — Advanced.cs:269-278. Head sentinel ConvertXIndexToCoord(0) = world coord, then XMin added again at :276. Should insert 0.0.
1.12 SliceOnPlane(Plane) returns halves swapped when plane.Normal.X < 0 — PublicFunctions.cs:265-289. X-crossing branch always assigns upper-x side to vs1 regardless of normal sign (x-negligible branch :252-264 handles sign correctly).
1.13 MinkowskiSubtractOne never flushes the last row's pending ranges — Advanced.cs:44-76. Ranges applied only when (y,z) changes; final row's accumulated ranges silently dropped.
1.14 MarchingCubes.Implicit seed functions inconsistently populate cellIDsToCheck — FindZPointFromXandY adds own hashID in both branches (:242, :278, :306); FindYPointFromXandZ omits it in two-intersection branch (:393-405, :419-432); FindXPointFromYandZ omits in both (:480-492, :515-527, :541-554) → holes in generated mesh.
1.15 Single-surface BooleanOperation excludes the last row/slab of the surface bbox — Advanced.cs:105-114: j < maxJ with inclusive maxIndices (multi-surface uses inclusive, :187, :195).
1.16 Both BooleanOperations hard-cast rows to VoxelRowSparse without converting — Advanced.cs:117, 218. After UpdateToAllDense() → InvalidCastException inside Parallel.For.
1.17 VoxelRowDense.Invert flips padding bits — VoxelRowDense.cs:347-352; numBytes = 1 + (numVoxelsX >> 3) always leaves 1–8 spare bits → phantom voxels in Count/AverageXPosition/XIndices/GetDenseRowAsSparseIndices. Latent (public Invert forces sparse first). GetNeighbors (:84-101) ignores upperLimit — reads padding bit as neighbor.
1.18 Sparse indexer get is unlocked while BinarySearch mutates shared state — VoxelRowSparse.cs:71-75 (lock commented out); lastIndex cache (:35, :184, :191, :202) written by every search. Concurrent reads race; reader racing writer can see List mid-Insert → wrong answers or exceptions. Every other member locks; the getter is the hole.
1.19 CreateFrom(IEnumerable<Polygon>, …) writes raw unvalidated sparse indices — VoxelizedSolid.cs:236-242. No sp == ep skip, no merge on touching ranges (compare :281-286) → violates strictly-increasing invariant BinarySearch relies on.
1.20 GetExposedVoxels vs GetExposedVoxelsWithSides disagree about x-boundary voxels — VoxelizedSolid.cs:744 vs :788. Two enumerations can disagree; CalculateSurfaceArea (:698-711) uses the latter.
1.21 VoxelRowSparse.XIndices can overrun indices and return values below start — VoxelRowSparse.cs:526-528. Dense counterpart worse: xValue = start >> 3 (byte count as x coord) yet enumerates from values[0] (VoxelRowDense.cs:391-422). Latent — all call sites use start=0.
1.22 GetCrossSectionsAs3DLoops NREs on empty (null) layers — PublicStaticMethods.cs:126-146 (lazy allocation gaps per CrossSectionSolid.cs:179-183). Same GetTotalPolygonVertices (CrossSectionSolid.cs:567-573).
1.23 MarchingCubesDenseVoxels.GetOffset places the crossing one voxel early — MarchingCubes.VoxelizedSolid.cs:102-127 (offset …*i instead of …*(i+1)); systematic surface bias.
1.24 ConvertXCoordToIndex clamps only the low side — VoxelizedSolid.cs:1019-1043; unchecked double→ushort above bounds (unspecified value).
1.25 VoxelizedSolid.Equals no null-check; ignores VoxelSideLength/Bounds — VoxelizedSolid.cs:1102-1117.
1.26 numVoxelsY * numVoxelsZ products overflow int for extreme grids — VoxelizedSolid.cs:127, 164; individually range-checked (:383-389), product not.
1.27 MarchingCubesCrossSectionSolid ctor — divides by NumLayers - 1 (:80); while overruns Layer2D if all layers empty (:93/:96).
- MEDIUM —
MarchingCubesImplicit.MakeFacesInCube~60-line near-verbatim copy of base (Implicit.cs:122-198vsBase.cs:283-350); only delta isCoordinates.IsNull()guard — hoistable. - MEDIUM —
FindZPointFromXandY/FindYPointFromXandZ/FindXPointFromYandZare three axis-permuted copies of the same ~120-line routine (Implicit.cs:202-570); divergence already caused bug 1.14. - MEDIUM — dead/vestigial code:
CreateDistanceGridBruteForce(MarchingCubesCrossSectionSolid.cs:195-251, no callers; its Parallel.For also drops the i == iMax column);GenerateBetweenLayersunreachable after throw (:165-188);ConvertToLoftedTessellatedSolidbody commented out, returns empty solid (CrossSectionSolid.cs:262-294);EdgeDirectionTableunused (Base.cs:416-421);commentslist built and discarded (Base.cs:208-211and MCCSS :153-157, 183-187); commented-out TurnOff mirror (VoxelRowDense.cs:130-137); unused usings (PublicFunctions.cs:18-21);IncreasingDoublesBinarySearchself-documented as redundant with List.BinarySearch (Advanced.cs:291-315). - LOW — four CreateFrom/CreateEmpty/CreateFullBlock ctors repeat the same 10-line init block (
VoxelizedSolid.cs:150-206, 313-334, 341-381); divergence already: one copiests.SolidColor, other usesConstants.DefaultColor(:158 vs :193). - LOW —
VoxelRowDense.CountandAverageXPositionduplicate the same 8-branch bit ladder (:147-159, :374-386) — collapse under PopCount.
3.1 Per-voxel virtual + lock cost in hot loops. Draft Y/Z directions (PublicFunctions.cs:347-392) set voxels one at a time through public indexer: bounds-check → virtual → lock → binary search + List.Insert = O(n³) locked virtual calls; per-row range ops (as X-direction branches do, :323-346) would be O(rows). Same pattern: VoxelEnumerator.MoveNext (VoxelHelperClasses.cs:110, indexer call per grid cell incl. off-voxels), neighbor probes in GetExposedVoxels* (VoxelizedSolid.cs:746-749, 797-800).
3.2 Bit counting without BitOperations. VoxelRowDense.Count (:143-162) tests 8 bits/byte in branches; BitOperations.PopCount over MemoryMarshal.Cast<byte, ulong>(values) ~10× faster. Same for AverageXPosition (TrailingZeroCount) and GetDenseRowAsSparseIndices (VoxelizedSolid.cs:440-460).
3.3 MarchingCubes valueDictionary grows to the full grid; every cube allocates. GetValue caches every corner forever (Base.cs:255-275, eviction commented out) → numGridX·Y·Z heap StoredValue objects when only two z-slabs are needed. MakeFacesInCube allocates StoredValue[8], Vertex[12], Vertex[3] per cube (:291, :309, :340) — reusable scratch given sequential loop.
3.4 LINQ allocation per row inside Parallel.For — PublicFunctions.cs:93, 128, 161: solids.Select(s => s.voxels[i]).ToArray() per row.
3.5 VoxelRowSparse.Union/Intersect/Subtract copy dense operands per row-op — VoxelRowSparse.cs:288, 309, 337 via yield-based enumerator allocating a List each time; in practice unreachable (everything pre-converted).
3.6 Missed/misused Parallel:
MarchingCubes<>.Generate(Base.cs:204-207) serial triple loop; z-slabs parallelizable with per-slab dictionaries.FillInFromTessellation(VoxelizedSolid.cs:262-263) has its Parallel.For commented out though each k writes disjoint rows — single most expensive voxelization step, trivially parallel.GetExposedVoxelsWithSidesabandoned BlockingCollection parallel version commented out (:758-772). 3.7SliceOnPlanecopies the whole solid twice then clears half of each —PublicFunctions.cs:185-186, 243-244.Copy()itself (VoxelizedSolid.cs:116-135) always converts to sparse and calls fullUpdateProperties()recompute. 3.8 Span/ArrayPool: dense boolean kernels (VoxelRowDense.cs:250-251, 290-291, 330-331) byte-at-a-time → Span 8× fewer iterations. Distance grids per layer inCreateDistanceGrid(:261) perfect ArrayPool candidates (only 2–4 alive at once).
3.9 Row-major layout (x-runs indexed y + numVoxelsY·z) good for z/y probes, bad for Draft-Y sweeps — document for contributors.
3.10 lock held across yield return — VoxelRowDense.XIndices (:394), VoxelRowSparse.XIndices (:523). Monitor held for enumeration lifetime; abandoning enumerator without dispose leaks the scope.
- Thread-safety undocumented and inconsistent (MEDIUM). Dense rows lock on
values, sparse onindices, but sparse getter unlocked (1.18); solid-level ops swap row objects unsynchronized (VoxelizedSolid.cs:405, 419); Count/_center updated non-atomically. Either document "not thread-safe; locks only protect built-in Parallel loops" or remove per-row locks (they cost every indexer call). - Memory footprint undocumented (LOW).
voxels= numY·numZ object refs + per-row List overhead even for empty rows — 2048² y/z grid ≈ 350 MB before voxel data. - ushort limits (LOW). 65535/axis enforced with plain
Exception(VoxelizedSolid.cs:387);ushort.MaxValueused as sentinel (:843-844) makes a true 65535-wide grid ambiguous; y·z product can overflow int. - Tolerance handling (LOW).
ToleranceForSnappingToLayers = 0.517(MarchingCubesCrossSectionSolid.cs:37) > 0.5 makesonLayerstrue for every discretization →GenerateBetweenLayersunreachable, snapping test vacuous.ExpandHorizontally/Verticallyhard-code 1e-9 (:327, :407).MarchingCubesImplicit.IsInsideuses<=whileGetOffsetusesIsPracticallySame(:74, :88-90) — two different boundary tolerances. VoxelizedSolid.Transform/TransformToNewSolidandCrossSectionSolidequivalents throwNotImplementedException(VoxelizedSolid.cs:621-635,CrossSectionSolid.cs:358-373) — runtime landmines for generic Solid consumers.
- Empty public no-op methods ship as API (HIGH).
MinkowskiAddBlock/MinkowskiSubtractBlock/MinkowskiAddSphere/MinkowskiSubtractSphere(Advanced.cs:27-34) have empty bodies — silent success. Implement, throw, or delete. - Construction API asymmetric (MEDIUM).
CreateFrom(TessellatedSolid, int|double, bounds)discoverable and well documented; no from-function/implicit-field constructor;CreateFrom(polygons, …)requires mandatory bounds; the two TS overloads silently differ on SolidColor. - Conversion round-trips (MEDIUM).
ConvertToTessellatedSolidMarchingCubes(int voxelsPerTriangleSpacing)vsCrossSectionSolid.ConvertToTessellatedSolidMarchingCubes(double gridSize)vs approximate-triangle-count overload — different parameter conventions; shared options type would help.ConvertToTessellatedExtrusions(bool extrudeBack, bool createFullVersion)ignorescreateFullVersion(CrossSectionSolid.cs:203-206). - Indexer semantics (MEDIUM). get returns false out of bounds but set throws (
VoxelizedSolid.cs:838-879) — undocumented asymmetry; internal sparse sentinel leaks into public contract (:842-845).ConvertXIndexToCoorddocs say "lower bound" but returns center (+0.5, :1044-1063). - Naming/discoverability (LOW). Public camelCase
numVoxelsX/Y/Z;VoxelsPerSideallocates per read (:48);AverageXPositionreturns a sum (×2 sparse);FractionDenseonly ever 0 or 1;GetLongID/GetIndicesFromID(:565-572) public undocumented 21-bit packers. CrossSectionSolid.Layer2Dis a public mutable field (CrossSectionSolid.cs:50) with cached_firstIndex/_lastIndexonlyAddmaintains (:188-189) — direct assignment (which the class itself does, bug 1.4) desyncs the cache.
Scope: All files in Miscellaneous Functions\ plus listed top-level files. All files read in full; every finding verified against source.
-
MiscFunctions.cs:2165— operator-precedence bug inSkewedLineIntersection.center = intersect1 + intersect2 / 2;computesintersect1 + (intersect2/2)instead of the midpoint. Every caller requesting the "middling point" of two skew lines gets a wrong point. -
MiscFunctions.cs:244–255—GetMinVertexDistanceAlongVectornever increments the index counter.var i = 0declared but (unlike the max version at line 221) never incremented; returnedindexalways 0 (or -1 for empty input). -
MiscFunctions.cs:2987–2993—Get3DLineValuesFromUniqueduplicated nested condition makes the −Z branch unreachable.if (direction.Z > 0) { if (direction.Z > 0) ... }— for −Z lines execution falls through to line 2996 where a near-zero vector is normalized → NaN/garbage axes. Breaks round-trip withUnique3DLine(line 2933) and corruptsUnique3DLineHashLikeCollection.AddIfNotPresent(:256) andFindBestRotationsfor downward axes. -
SphericalHashLikeCollection.cs:320—radiiinserted at index 0 instead ofi. Misaligns the parallel lists;IsTheSame(:497,radii[existingIndex]) compares wrong radius whenignoreRadius == false. Generic subclass (:67–70) does it correctly, confirming typo. -
UpdatablePriorityQueue.cs:278–314, 379–406—DequeueEnqueue/EnqueueDequeueleak stale_elementIndicesentries. Never remove the root's dictionary entry →Contains(removedRoot)true,Removesifts wrong slot (heap corruption),_elementIndices.Add(element, 0)(:296, :308) throws if element ever enqueued before. -
UpdatablePriorityQueue.cs:566–584—Removerestores heap by sift-down only — does not preserve heap invariant. When the relocated last node's priority is smaller than the removed slot's parent, the min-heap property is violated upward. Matters greatly:UpdatePriority(:967–971) is Remove+Enqueue, used pervasively inPolygonOperations.Simplify.cs(:204, 265–266, 743–744, 1274–1355) andSimplifyTessellation.cs:163–164. Fix: compare with parent; MoveUp when smaller else MoveDown. -
UpdatablePriorityQueue.cs:430–435—EnqueueRange(ICollection)builds_elementIndicesfrom the whole backing array, not the firstcountslots. Trailingdefaultslots →ArgumentNullException(ref types) or duplicate-keyArgumentException(value types). -
Proximity.cs:521–526—ReduceDirectionsreduction loop is dead code.sortedProximitiesusesNoEqualSort(never returns 0) soContainsKeyalways false (also passes an index as a key); loop exits first iteration —targetNumberOfDirectionsnever honored. Lines 553/559 test identical condition inifandelse if. -
MiscFunctions.cs:1097–1099—GetMultipleSolidscreates a primitive copy and discards it.primCopycomputed (incl. fragile O(n²)faceGroup.IndexOf(f)mapping) butnewSolid.Primitives.Add(primitive)adds the original wired to the old solid.// does it need to be copied?comment confirms confusion. -
SphericalHashLikeCollection.cs:36–48, 75–81, 192–207(andUnique3DLineHashLikeCollection.cs:36–49, 73–79, 147–161) — method hiding vianewbreaks the parallelitemslist. Base implementsICollection<T>; calls through base-typed reference add tosphericals/cartesians/radiiwithout adding toitems→ silent desync. Needs virtual methods or composition.
SphericalAnglePair.cs:101–118— GetHashCode/Equals contract violation. Equals is tolerance-based (dot ≈ 1 within 5e-8); GetHashCode quantizes into π/32768 buckets — near-equal pairs straddling a boundary hash differently → Dictionary/HashSet misses. Tolerance-based Equals also intransitive.azimuthInt−π special case maps far from −π+ε.SphericalHashLikeCollection.cs:188—TryGet(..., out T, out SphericalAnglePair)indexessphericals[i]on the failure path —i == Countfor misses at end →ArgumentOutOfRangeException; throws on empty collection.MiscFunctions.cs:1503–1547—TransformToXYPlaneMaybeBetter±Y special case returns a reflection (det −1), not a rotation; geometry mirrored; forward/back transforms set to same matrix. No callers currently, but public.MiscFunctions.cs:2658–2675—IsVertexInsideTriangle(IList<Vector3>, …)ignoresonBoundaryIsInside; boundary handling internally inconsistent (< 0vs> 0). Same asymmetry in TriangleFace overload (:2624, 2628);PointOnTriangleFromRay(:2324–2331) never forwards the flag.VolumeCenterMoments.cs:34–80—CalculateVolumeAndCenterOLDaccumulates across refinement iterations without resetting — still public; results ~3× too large. Delete or[Obsolete].VolumeCenterMoments.cs:121–123— division byvolume == 0mid-refinement → NaN center, which then poisons the next pass and skips theIsNegligiblefallback (:126).Comparators.cs:33–38—SortByIndexInListinconsistent comparer (both orderings return 1 for equal indices).TwoDSortXFirst/TwoDSortYFirst(162–167, 184–189) never return 0 while using tolerantIsPracticallySameon the primary key.NoEqualSort(136–140) deliberately never-equal — but that breaksContainsKey(see ReduceDirections) and violatesCompare(x,x)==0.MiscFunctions.cs:154–155 / 181–182— unbounded decimal-digit loop —numDecimalPointscan exceed 15 →Math.Roundthrows. Siblings at 1231/1299 clamp correctly.OutputServices.cs:42–54—SetLoggerdisposes theILoggerFactorybefore the logger is used (usingfactory); subsequentLog.*writes through disposed provider. Lazy static init unsynchronized.Presenter.cs:182–190, 267–280—EmptyPresenter2DthrowsNotImplementedExceptionforSaveToPngand allShowStepsAndHangoverloads while other members are silent no-ops. Null-object default crashes.MiscFunctions.cs:737–784—FindFlatsoff-by-one: seed face excluded fromareaandnumFaces—minNumberOfFacesPerFlat = 2actually requires 3 faces.UpdatablePriorityQueue.cs:207–231—Enqueuedoes not enforce documented uniqueness invariant — duplicate enqueue silently corrupts index tracking.SolidAssembly.cs:618–640—GetAllFilePathsRecursiveaddsnullpaths; skip test inverted —if (skipEmbeddedFiles && filePath != null && File.Exists(filePath)) continue;skips files that exist and collects null/missing ones;Path.GetFileName(null)downstream (:613).Proximity.cs:845–851—FindBestRotationsrelies onTryGet's failure side-effect (out param left as the reflected query,Unique3DLineHashLikeCollection.cs:321); works only because Reflect is idempotent. UseuniqueLinedirectly.
MiscFunctions.cs:301— tuple element misnamed (minPointfor max).MiscFunctions.cs:1840— asymmetric interval test (t_a > 1vst_b >= 1) inSegmentSegment2DIntersectionConventional.MiscFunctions.cs:454–456—DefineInnerOuterEdgesenumeratesfacesthree times.MiscFunctions.cs:2751–2783—SetPositiveAndNegativeShiftsrecursive with no depth bound.MiscFunctions.cs:2686–2697—IsPointInsideSolidhardcodes 1e-8 instead ofts.SameTolerance; slices exactly atPointInQuestion.Z(degenerate on facet planes).SphericalHashLikeCollection.cs:51–57 / 303–309— secondReflectcall is a no-op, misleading.Proximity.cs:783–785—NEquidistantSpherePointsKogan(1)divides by zero.Proximity.cs:16— strayusing System.Drawing;.Presenter.cs:3— strayusing static ...JSType;.Colors.cs:837–866—GetRandomColorNames/GetRandomColorsskip family 0 after first cycle; intentionally infinite enumerables (foreach without Take hangs).Colors.cs:741–805—Distinct64Colorscontains duplicates (DodgerBlue ×2, HotPink ×2) → only 62 distinct.Colors.cs:698–712—GetHueNaN for achromatic colors; comment wrong (result 0..1, not degrees).MiscFunctions.cs:2823–2857—OrderedEdgesAndAnglesCCWAtVertexinfinite-loops/NREs on open border fans.SolidAssembly.cs:130–139— ctor NREs on nullfileName/Name;IsEmpty()(:181) NREs ifSolidsnever set.Constants.cs:113–125— doc comments missing</summary>;CartesianDirections(486–533) garbled nested summaries.
MiscFunctionsis a 3-file partial god-class — MiscFunctions.cs (3104 lines) + Proximity.cs (887) + VolumeCenterMoments.cs (194) ≈ 4200 lines, ~150 public methods mixing sorting, projection, transforms, angles, intersections, distances, point-in-X, mesh topology, reflection-based type discovery, sphere sampling, volume/moments.- Three implementations of "transform direction to XY plane":
TransformToXYPlane(1455–1490),TransformToXYPlaneMaybeBetter(1503–1547 — zero callers, buggy),SphericalAnglePair.TransformToXYPlane(:85–92). - Four point↔segment/line distance routines:
MiscFunctions.DistancePointToLine(2185–2229),Proximity.ClosestVertexOnSegmentToVertex(149–168),Proximity.ClosestPointOnLineSegmentToPoint(181–243),PolygonOperations.SqDistancePointToLineSegment(PolygonOperations.DistanceToPoint.cs:187). - Dead/legacy public methods:
CalculateVolumeAndCenterOLD;TransformToXYPlaneMaybeBetter;CalculateInertiaTensor(VolumeCenterMoments.cs:153–192 — full pass of work then unconditionalthrow new NotImplementedException()at :191);OrderedFacesCCWAtVertexNoEdges(MiscFunctions.cs:2914–2917, throws);SubAssembly.Transform/TransformToNewAssembly/CalculateCenter/CalculateInertiaTensor(SolidAssembly.cs:726–758, all throw).
- Copy-pasted Vertex vs Vector3 overload bodies:
AreaOf3DPolygon(803–877 vs 892–966 — 75 identical lines),GetVertexDistances(149–163 vs 176–190),Length(601–627),ConvertTo1DDoublesCollection(2790–2815). ChooseTightestLeftTurnduplicated verbatim at Proximity.cs:696–713 and 722–739.- Two 2D segment-segment intersectors: conventional (1805) + PGA-based (1863); doc on the first advertises the second.
GetOrthogonalDirectionpure alias ofGetPerpendicularDirection(Proximity.cs:329–330).Length(polyline, isClosed:true)duplicatesPerimeter(573–608);isClosed = truea surprising default.Comparators.ForwardSortreimplementsComparer<double>.Default; none of the comparers handle NaN coherently.- Commented-out blocks: UpdatablePriorityQueue
FindIndex~28 dead lines (912–939), stale remarks (546–551, and ignoredequalityComparerparam :556); SolidAssembly.StreamRead 17-line alt (242–259); MiscFunctions 1461–1465, 1622–1624, 1836–1837. Global.FindIndex(Global.cs:21–33) needlessToList().
Mostly not redundant: it's a ~900-line fork of System.Collections.Generic.PriorityQueue (acknowledged :12–13) whose value-add is O(1) Contains/Remove/UpdatePriority via _elementIndices. .NET's built-in Remove is O(n). Recommendation: keep but (a) fix invariant bugs, (b) delete forked dead code, (c) make UpdatePriority O(log n) single-sift, (d) add behavioral tests vs PriorityQueue.
FacesWithDistinctNormals(699–727): three fullOrderBy().ToList()sorts +RemoveAt(i)in reverse loop — O(n²) per pass.FindFlats(768):Plane.DefineNormalAndDistanceFromVerticesre-fits over the entire accumulated vertex list per candidate edge — O(k²) per flat. Incremental centroid/covariance update = O(1) per test.stack.Any()(752) allocates per iteration.PointOnTriangleFromLineSegment(2284): allocatesList<Vector3>per call in ray-cast inner loop.Proximity.FindBestPlanarCurve(589–601): reflection-driven —TypesImplementingICurve()re-scans assembly types per call (MiscFunctions.cs:3007–3014, no caching);MethodInfo.Invokeboxing. Cache + compiled delegates or static-abstract dispatch.ProjectTo2DCoordinates*(1226–1318): twoMath.Roundper key; scaled-integer key cheaper.SphericalHashLikeCollection/Unique3DLineHashLikeCollectioninsertion O(n) per add (List.Insert into 4 parallel lists) → O(n²) build. "Hash-like" oversells: tolerance-aware sorted list. Bucketed design (quantized polar bands) → near-O(1). Four parallel Lists → one List for locality.CalculateVolumeAndCenter(100–125): enumeratesfacesup to 10× (once per refinement); materialize once. Same multiple-enumeration in UpdatablePriorityQueue items ctor (171–174).TriangleFace.Edgesis a compiler-generated iterator (TriangleFace.cs:327–335) — everyforeach (var edge in face.Edges)allocates. Mesh-wide inner loops (FindFlats 758, GetContiguousFaceGroups 1149, DefineInnerOuterEdges 462...). Exposing AB/BC/CA or struct enumerator = cheap library-wide win.- Boxing:
SphericalHashLikeCollection<T>.Contains(217),Unique3DLineHashLikeCollection<T>.Contains(170) —item.Equals(items[i])on unconstrained T; useEqualityComparer<T>.Default.
ClosestVertexOnTriangleToVertexcomputesDistance(sqrt) up to twice (48, 60, 67).SortAlongDirectiondoc describes O(n) counting-sort that no longer exists (90–97).DebugUtilities.SaveBitmap(229): two passes for Max/Min; ZRange zero-guard missing.- Parallel opportunities: per-face loops in
CalculateVolumeAndCenter(sum reduction),FacesWithDistinctNormals,GetContiguousFaceGroupsseeding — nothing uses TPL today. - Span/stackalloc:
PointCommonToThreePlanes(2011–2015) allocatesdouble[,]+double[]per call for a 3×3 solve.
- High — god-class discoverability:
MiscFunctions.IntelliSense is the de-facto index of the geometry toolbox; doc-comment "Common search terms" (36, 73) is a symptom. - High — static mutable service locator (
OutputServices+Presenter+Log): mutable static ILogger/IPresenter with lazy unsynchronized init (OutputServices.cs:7–40); 40-method static façade; tests order-dependent; two hosts can't use different sinks. Fix disposed-factory bug;Interlocked/Lazy<T>; acceptILoggerFactory. - Medium — mutable public static collections:
Color.ColorDictionary(Colors.cs:884, mutable dictionary of mutable Color objects with public byte fields, :627–644),Distinct64Colors(741),PlatonicConstants.*Directions(154–340, cached arrays returned by reference — caller writes corrupt future callers; benign lazy-init race),Constants.TessellationToVoxelizationIntersectionCombinations(177–187, writable list+arrays). Return ImmutableArray/ReadOnlySpan or freeze. - Constants design: all const/static readonly — no mutable global tolerances. Real issue: fixed absolute tolerances (BaseTolerance 1E-9 :83; PolygonSameTolerance 1e-7 :94) baked into defaults regardless of model scale;
Solid.SameToleranceexists (Solid.cs:254) but misc functions never consult it. - Logging design:
Logis internal (Logger.cs:6) so clients can't route through it; first strayLog.Tracebefore host configuration permanently binds broken console logger;Log.BeginScopediscards the IDisposable. - Naming (Low):
DodechedronDirections(PlatonicConstants.cs:228);TransformToXYPlaneMaybeBetter;CalculateVolumeAndCenterOLD; "HashLike" for sorted-list;oneOverdeterminnant(1833);FileType.ThreeMFdocumented as "Mobile MultiModal Framework" (Constants.cs:337);SolidAssembly._distinctSolidsunderscore-named property (:98). - Dead/debug scaffolding:
BuildAssemblyTreeNodevar debug = false;+ Console.WriteLine blocks (SolidAssembly.cs:328–364), returnsdynamic(anonymous types);useOnSerialization(270–297) never true → serialization hooks dead. SolidAssembly.cs:20—using System.Numerics;shadowed by TVGL's own Matrix4x4 — invites confusion.
| New static class | Members (current locations) |
|---|---|
DirectionalSortExtensions |
SortAlongDirection, GetVertexDistances, GetMin/Max...AlongVector (62–435) |
PlanarProjection |
ProjectTo2DCoordinates*, ConvertTo2D/3DLocation(s), TransformToXYPlane (1191–1582) |
AngleFunctions |
all Angle*/AngleBetween (1585–1782) |
IntersectionFunctions |
SegmentSegment2D*, SegmentLine2D, LineLine2D, plane/three-plane/skew-line, PointOnPlaneFrom*, PointOnTriangleFrom* (1786–2580) |
ProximityFunctions |
Proximity.cs + DistancePointToLine/Plane (2171–2259) |
ContainmentFunctions |
IsVertexInsideTriangle, IsPointInsideSolid, IsPointInsideAABB (2607–2740) |
MeshTopologyFunctions |
DefineInnerOuterEdges, GetVertices, GetMultipleSolids, GetContiguousFaceGroups, FindFlats, FacesWithDistinctNormals, OrderedEdges/FacesCCWAtVertex (438–1189, 2817–2917) |
VolumeAndMoments |
VolumeCenterMoments.cs (minus OLD) |
Line3DDescriptor struct |
Unique3DLine/Get3DLineValuesFromUnique (2928–3001) — a readonly struct with Encode/Decode/Reflect would give the hash-like collection a typed key instead of bare Vector4 (Z-is-polar-angle is tribal knowledge) |
Also: delete *OLD/MaybeBetter; make helper-only members private/internal; [EditorBrowsable(Never)] on reflective TypesImplementingICurve/TypesInheritedFromPrimitiveSurface (3007–3026).
SurfaceAreasilent no-op setter (:120). Public setters on Volume/Center desync caches; prefer protected +InvalidateCaches().Boundssettable publicVector3[2]initialized to zeros → XMin..ZMax return 0 rather than "not computed" (161–192).- Copy ctor (302–325) shallow-copies ConvexHull/SolidColor, doesn't copy Primitives — undocumented contract.
InertiaTensorget-only property backed byCalculateInertiaTensorthat throws (VolumeCenterMoments.cs:191) — landmine.
- Two-phase construction (Add →
CompleteInitialization(), 150–157) easy to misuse; forgetting leavesSolidsnull; adding after silently desyncs. Builder or lazy derivation. GetTessellatedSolids(out IEnumerable, out IEnumerable)(167–171) returns lazy queries via out params; XML doc references removed params.AllPartsInGlobalCoordinateSystem(558–566) caches lazy IEnumerable that re-transforms per enumeration; first enumeration pays full mesh-copy cost invisibly.BuildAssemblyTreeNodereturnsdynamic(326) — consumers can only use via JSON round-trip; small DTO needed.numberOfSolidBodies/numberOfSheetBodiescaches (71, 85) never invalidate when Solids reassigned.
Should be sealed types containing a sorted structure rather than inheritance + new-hiding. AsAnglePairs()/Vector3s (276–284) expose live internal lists — return read-only views.
- UpdatablePriorityQueue.Remove sift-up-or-down + DequeueEnqueue/EnqueueDequeue dictionary removal.
- Get3DLineValuesFromUnique −Z branch.
- radii.Insert(0, …) → Insert(i, …).
- SkewedLineIntersection midpoint precedence.
- GetMinVertexDistanceAlongVector missing i++.
- EnqueueRange dictionary-from-full-array.
- ReduceDirections dead loop / NoEqualSort.ContainsKey.
- GetMultipleSolids discarded primCopy.
- OutputServices.SetLogger disposed factory; EmptyPresenter2D throwing members.
- Remove/obsolete dead public trio: CalculateVolumeAndCenterOLD, TransformToXYPlaneMaybeBetter, CalculateInertiaTensor.