GeometryHelper.SolidGeometry
3.1.0
merge project
dotnet add package GeometryHelper.SolidGeometry --version 3.1.0
NuGet\Install-Package GeometryHelper.SolidGeometry -Version 3.1.0
<PackageReference Include="GeometryHelper.SolidGeometry" Version="3.1.0" />
<PackageVersion Include="GeometryHelper.SolidGeometry" Version="3.1.0" />
<PackageReference Include="GeometryHelper.SolidGeometry" />
paket add GeometryHelper.SolidGeometry --version 3.1.0
#r "nuget: GeometryHelper.SolidGeometry, 3.1.0"
#:package GeometryHelper.SolidGeometry@3.1.0
#addin nuget:?package=GeometryHelper.SolidGeometry&version=3.1.0
#tool nuget:?package=GeometryHelper.SolidGeometry&version=3.1.0
GeometryHelper.SolidGeometry
A 3D geometry library for engineering models, written in C# and targeting netstandard2.0.
It is the 3D counterpart of
GeometryHelper.PlaneGeometry and follows
the same shape: immutable geometry types, operations living in static classes under Core, and every
operation mirrored as an instance method on the type it applies to. The two are independent — neither
references the other, and what they share arrives through
GeometryHelper.CommonGeometry.
Installation
dotnet add package GeometryHelper.SolidGeometry
Structure
| Namespace | Contents |
|---|---|
GeometryHelper.SolidGeometry.Geometry |
GeoPoint3, GeoVector3, GeoLine3, GeoRay3, GeoPlane3, GeoTriangle3, GeoPolyline3, GeoPolygon3, GeoCircle3, GeoFace3, GeoAabb3, GeoObb3, GeoSolid3, GeoCoordinateSystem3, GeoTransform3 |
GeometryHelper.SolidGeometry.Core |
Boolean3, Collision3, Containment3, Distance3, Intersection3, Merge3, Parallel3, Parametrization3, Projection3, Splition3 |
GeometryHelper.SolidGeometry.Spatial |
GeoBvh3 |
GeometryHelper.SolidGeometry.Extension |
EnumerableExtension |
GeometryHelper.CommonGeometry |
Tolerance |
GeometryHelper.CommonGeometry.Datatype |
Angle |
GeometryHelper.CommonGeometry.Enums |
PointLocation, PlaneSide |
The last three come from the GeometryHelper.CommonGeometry package,
which GeometryHelper.PlaneGeometry shares, so a program working in
both dimensions sees one Tolerance and one Angle rather than two of each. It comes with this
package as a dependency.
Quick Start
using GeometryHelper.CommonGeometry;
using GeometryHelper.SolidGeometry.Geometry;
var a = new GeoPoint3(0, 0, 0);
var b = new GeoPoint3(3, 4, 0);
double distance = a.DistanceTo(b); // 5
GeoVector3 direction = a.GetVectorTo(b); // [3, 4, 0]
var plane = new GeoPlane3(GeoPoint3.Origin, GeoVector3.ZAxis);
GeoPoint3 flat = plane.Project(new GeoPoint3(2, 3, 7)); // (2, 3, 0)
double signed = plane.SignedDistanceTo(new GeoPoint3(2, 3, 7)); // 7
var box = new GeoObb3(GeoPoint3.Origin, 10, 20, 30);
double volume = box.Volume; // 6000
Geometric Types
The shapes split into three families, and which family a shape belongs to decides what you can ask of it:
| Family | Types | Encloses an area | Encloses a volume |
|---|---|---|---|
| Curve | GeoLine3, GeoRay3, GeoPolyline3 |
no | no |
| Planar region | GeoTriangle3, GeoPolygon3, GeoCircle3, GeoFace3 |
yes | no |
| Volume | GeoObb3, GeoAabb3, GeoSolid3 |
yes | yes |
Only regions and volumes offer Contains. Every shape offers Locate, and a curve can only ever
answer OnSide or OutSide, because a curve has no interior for a point to be inside of.
A GeoPolyline3 is always an open chain — it has no IsClosed flag and never joins its last vertex back
to its first. Geometry meant to enclose something is a GeoPolygon3, and polyline.ToPolygon() converts
between them. A chain of vertices tracing a square still holds only the points on its path:
var traced = new GeoPolyline3(
new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0),
new GeoPoint3(10, 10, 0), new GeoPoint3(0, 10, 0), new GeoPoint3(0, 0, 0));
var middle = new GeoPoint3(5, 5, 0);
traced.IsPointOn(middle); // false — a curve has no interior
traced.DistanceTo(middle); // 5 — measured to the path
traced.ToPolygon().Contains(middle); // true — now it is a region
traced.ToPolygon().DistanceTo(middle); // 0
A planar region is flat, so a point counts as inside it only when it lies on the carrier plane as well as within the boundary. A point hovering above the middle of a polygon is outside it:
var square = new GeoPolygon3(
new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0),
new GeoPoint3(10, 10, 0), new GeoPoint3(0, 10, 0));
square.Locate(new GeoPoint3(5, 5, 0)); // Inside
square.Locate(new GeoPoint3(5, 0, 0)); // OnSide
square.Locate(new GeoPoint3(5, 5, 3)); // OutSide — off the plane
Flatness is enforced
A polygon that is not flat has no normal, no area and no interior, so GeoPolygon3 refuses one at
construction rather than letting every property on it become quietly meaningless. Geometry that wanders
out of a plane is a GeoPolyline3.
// Throws ArgumentException: these four vertices do not share a plane.
new GeoPolygon3(
new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0),
new GeoPoint3(10, 10, 0), new GeoPoint3(0, 10, 5));
How flat is flat enough is Tolerance.EqualPlanar, which is separate from EqualPoint because
coplanarity is measured far from the reference point: a polygon several metres across turns a hundredth
of a degree of tilt into a deviation of nearly a millimetre.
Two kinds of box
GeoAabb3 is an axis-aligned bound — the cheap test that comes before the expensive one.
GeoObb3 is an oriented shape that carries its own axes and describes a beam running at an angle
tightly, where an axis-aligned box would only bound it loosely.
var bounds = GeoAabb3.FromPoints(new[]
{
new GeoPoint3(1, 5, -2),
new GeoPoint3(-3, 0, 4),
});
bounds.Min; // (-3, 0, -2)
bounds.Max; // (1, 5, 4)
bounds.Volume; // 4 * 5 * 6 = 120
The axes of a GeoObb3 are made orthonormal on the way in, so a Y direction that is not quite square to
X is corrected rather than producing a skewed box:
var box = new GeoObb3(
GeoPoint3.Origin, 2, 2, 2,
GeoVector3.XAxis,
new GeoVector3(0.5, 1, 0)); // not square to X
box.AxisX.IsPerpendicularTo(box.AxisY); // true
box.AxisX.CrossProduct(box.AxisY).IsEqualTo(box.AxisZ); // true
Solids and openings
A GeoSolid3 is the set of faces bounding a body. An opening is a whole solid subtracted from it — a duct
through a slab, a recess in a footing — which is not the same thing as a hole in a GeoFace3, which is
flat and belongs to a single face.
GeoSolid3 slab = new GeoAabb3(GeoPoint3.Origin, new GeoPoint3(10, 10, 10)).ToObb().ToSolid();
GeoSolid3 duct = new GeoAabb3(new GeoPoint3(4, 4, 4), new GeoPoint3(6, 6, 6)).ToObb().ToSolid();
GeoSolid3 pierced = slab.WithOpenings(new[] { duct });
pierced.Volume; // 1000 — the gross body
pierced.NetVolume; // 992 — with the duct removed
pierced.IsClosed(); // true
pierced.Locate(new GeoPoint3(1, 1, 1)); // Inside
pierced.Locate(new GeoPoint3(5, 5, 5)); // OutSide — inside the duct
Volume is measured by the divergence theorem, so it does not depend on where the solid sits and is
reported unsigned: faces wound inwards give the same answer as faces wound outwards. It does depend on the
boundary being closed, which is what IsClosed() is for.
Operations
| Class | Answers |
|---|---|
Parallel3 |
is this parallel, perpendicular, coplanar with that |
Containment3 |
where does a point sit relative to a shape |
Distance3 |
how far apart are two shapes |
Projection3 |
which point of a shape is closest to a point |
Intersection3 |
where exactly do two shapes meet |
Collision3 |
do two shapes overlap at all |
Parametrization3 |
which point sits at a position along a curve |
Merge3 |
put curves that meet end to end back together |
Splition3 |
cut a curve, a region or a body into pieces |
Boolean3 |
join two bodies, keep what they share, take one out of the other |
Every operation is reachable both ways. The static form names the larger shape first and the point or curve second; the instance form sits on whichever of the two reads better where you are calling from:
var line = new GeoLine3(new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0));
var point = new GeoPoint3(4, 3, 0);
Distance3.DistanceTo(line, point); // 3
line.DistanceTo(point); // 3
point.DistanceTo(line); // 3
Intersection3 returns a single result and reports false when there is not exactly one. Two shapes that
overlap along a whole line or a whole area — a segment lying in a plane, two coincident planes, two
collinear segments — have no single crossing to name, so they come back false rather than picking an
arbitrary point out of the overlap:
var crossing = new GeoLine3(new GeoPoint3(0, 0, -5), new GeoPoint3(0, 0, 5));
var lyingIn = new GeoLine3(new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0));
crossing.TryIntersectWith(GeoPlane3.XY, out GeoPoint3 hit); // true, hit is (0, 0, 0)
lyingIn.TryIntersectWith(GeoPlane3.XY, out _); // false
Collision3 answers yes or no and says nothing about where. Two oriented boxes are tested with the
separating axis theorem, which needs the nine cross-product axes as well as the six face axes to catch two
boxes passing each other at an angle:
var first = new GeoObb3(GeoPoint3.Origin, 10, 10, 10);
var beside = new GeoObb3(new GeoPoint3(5, 0, 0), 10, 10, 10);
var apart = new GeoObb3(new GeoPoint3(11, 0, 0), 10, 10, 10);
first.CollidesWith(beside); // true
first.CollidesWith(apart); // false
Parametrization
A parameter is normalized: 0 is the start of a curve and 1 its end. A distance is a true arc length from the start. Values outside the natural range follow the shape of the curve — a line segment extrapolates along the infinite line carrying it, a polyline clamps because an open chain has no single direction to extend along, and a circle wraps.
var segment = new GeoLine3(new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0));
segment.GetPointAtParameter(2.0); // (20, 0, 0) — extrapolates
var chain = new GeoPolyline3(
new GeoPoint3(0, 0, 0), new GeoPoint3(3, 0, 0), new GeoPoint3(3, 4, 0));
chain.GetPointAtDistance(5.0); // (3, 2, 0)
chain.GetPointAtDistance(100.0); // (3, 4, 0) — clamps
var square = new GeoPolygon3(
new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0),
new GeoPoint3(10, 10, 0), new GeoPoint3(0, 10, 0));
square.GetPointAtParameter(0.25); // (10, 0, 0) — a quarter of the way round
square.GetPointAtParameter(1.25); // the same point again — wraps
A polygon is measured around its boundary starting at its first vertex, and a circle around its circumference. Both wrap, so there is no out-of-range parameter for a closed curve.
Splitting
Splition3 cuts a curve at a position along it or wherever a plane crosses it, and cuts a region or a
body by a plane. Pieces of a curve come back in order along the subject, so the first piece always holds
its start point and the last holds its end point. Every overload reports false when there was nothing to
cut and still hands back the subject as a single piece, so the result is usable either way. Each split is
also reachable as an instance method on the shape being cut — it sits on the subject, since
plane.Split(line) would not say which of the two comes back in pieces.
var line = new GeoLine3(new GeoPoint3(0, 0, 0), new GeoPoint3(10, 0, 0));
line.TrySplitAtDistance(4, out GeoLine3[] pieces); // true: lengths 4 and 6
line.TrySplitAtDistance(0, out _); // false — a cut at an endpoint makes no piece
line.TrySplitBy(new GeoPoint3(5, 3, 0), out _); // false — the point is not on the segment
A cutter does not have to be a plane. Cutting by a closed body sorts the pieces into those inside it and those outside, because a body has no side the way a plane does:
var chain = new GeoPolyline3(new GeoPoint3(-5, 5, 5), new GeoPoint3(20, 5, 5));
chain.TrySplitBy(solid, out GeoPolyline3[] inside, out GeoPolyline3[] outside);
// inside holds the stretch within the body, outside the two stretches beyond it
The same works against a GeoObb3 or a GeoAabb3, which are cheaper because the crossings come from the
slab test rather than from walking a surface. Neighbouring pieces that end up on the same side are joined
back up, so what comes out is the longest run on each side rather than a chain chopped at positions that
separate nothing — a chain running down the shaft of an opening meets the caps at each end without ever
entering material, and that is not a cut.
Several bodies can be given at once, and they behave as their union: a stretch is inside when any one of them holds it. Cutting by each in turn would not do the same thing, because the pieces of the first cut would have to be sorted again against the second and the runs joined back up by hand.
var route = new GeoPolyline3(new GeoPoint3(-10, 5, 5), new GeoPoint3(60, 5, 5));
route.TrySplitBy(new[] { beam, slab }, out GeoPolyline3[] embedded, out GeoPolyline3[] clear);
// embedded holds the stretches buried in either body, clear the stretches in the open
Because it is the union being asked about, two bodies that overlap do not each claim their own piece of the answer, and two meeting face to face leave no cut between them — the run through both comes back whole. An empty array is the same rule at its limit: nothing holds anything, so the whole chain is clear.
An array of GeoObb3 or of GeoAabb3 works the same way and is the cheaper form where the cutters really
are boxes. A GeoObb3 is a reference, so a null entry is skipped; a GeoAabb3 is a value with no null to
pass, and the entry left out there is the empty box, which could hold nothing anyway.
Cutting by a bounded region cuts only where the subject really goes through it, which is what is wanted when the cutter stands for a physical plate rather than an endless surface:
chain.TrySplitBy(plate, out GeoPolyline3[] pieces); // only where it pierces the plate
chain.TrySplitBy(plate.GetPlane(), out GeoPolyline3[] more); // anywhere it crosses the carrying plane
Cutting a region sorts the result by side, and a concave subject can fall into more than two pieces, which is why each side comes back as an array:
// A U shape opening upwards, cut horizontally through its two arms.
var uShape = new GeoPolygon3(
new GeoPoint3(0, 0, 0), new GeoPoint3(9, 0, 0), new GeoPoint3(9, 10, 0),
new GeoPoint3(6, 10, 0), new GeoPoint3(6, 4, 0), new GeoPoint3(3, 4, 0),
new GeoPoint3(3, 10, 0), new GeoPoint3(0, 10, 0));
var cutter = new GeoPlane3(new GeoPoint3(0, 7, 0), GeoVector3.YAxis);
uShape.TrySplitBy(cutter, out GeoPolygon3[] above, out GeoPolygon3[] below);
// above.Length == 2 — one piece per arm
// below.Length == 1
A solid is cut the same way, and the two halves come back closed:
GeoSolid3 cube = new GeoAabb3(GeoPoint3.Origin, new GeoPoint3(10, 10, 10)).ToObb().ToSolid();
cube.TrySplitBy(GeoPlane3.XY.Offset(4), out GeoSolid3 upper, out GeoSolid3 lower);
// upper.Volume == 600, lower.Volume == 400, both IsClosed()
The body may be concave and its faces may carry holes. Each face is cut on its own, and the new surface closing each half is built from the edges the cut left behind: in a closed body every edge is traversed once by the face beside it, so the same edge traversed the other way belongs to the cap. That section may be several loops, and one loop may sit inside another, so the loops are nested into faces rather than assumed to be a single boundary:
// A square tube — a block with a shaft through it. The section across it is a ring, not a disc.
tube.TrySplitBy(GeoPlane3.XY.Offset(5), out GeoSolid3 upper, out GeoSolid3 lower);
// each half is closed, and its cap is one face carrying one hole
Two consequences worth knowing. A cut can leave one half as several disconnected shells — a plane
across both arms of a U leaves two bodies above it — and they come back as one GeoSolid3, which measures
and answers containment correctly because each shell is closed and wound outwards. And a plane passing
through a hole in a face turns that rim into part of the boundary, so the piece on each side has no
hole where the subject had one.
A plate can also be cut along a line marked on it, or against a body:
plate.TrySplitBy(cutLine, out GeoPolygon3[] halves); // cut along a drawn chain
plate.TrySplitBy(solid, out GeoPolygon3[] embedded, out GeoPolygon3[] clear);
The second answers which part of a plate is embedded in a body. It works by cutting the plate with the plane of each face of the body in turn — the surface of the body never leaves those planes, so once that is done no piece can straddle the boundary — and then joining the pieces back up where they agree. What comes back covers each side exactly, though it may be in more pieces than strictly necessary.
Combining bodies
Boolean3 joins two solids, keeps the part they share, or takes one out of the other:
first.TryUnion(second, out GeoSolid3 joined);
first.TryIntersect(second, out GeoSolid3 shared);
first.TrySubtract(tool, out GeoSolid3 left);
Each reports false when the answer is nothing at all — two bodies that never touch share nothing, and a
body swallowed whole leaves nothing behind. That is an outcome rather than a failure, which is why it
comes back as false rather than as an exception or an empty body.
The method is the cutting above carried up a dimension. Both bodies are divided by one shared set of planes — the face planes of each of them together — which leaves cells that are each wholly inside or wholly outside the other, since the surface of a body never leaves the planes of its own faces. The cells the operation wants are then glued: a face shared by two kept cells appears twice, once each way round, and dropping both leaves exactly the outer skin.
Two details are worth knowing. Using one shared plane set for both bodies rather than each against the other is what makes the gluing work where they meet — cut that way both sides of the interface are the same plane carved by the same knives, so they come out as the same polygon and cancel. And dividing A by the planes of B already lays a face along every part of the surface of B that runs through A, which is why a difference is just the cells of A that fall outside B: the walls of the cavity are already there.
Merging
Merge3 is the other direction. ConsecutiveLines and ConsecutivePolylines take the pieces in the order
given and only ever join a piece to the one after it. Join ignores order and direction and reassembles
whatever chains the set actually forms, which is what a bag of edges out of a model needs:
GeoPolyline3[] chains = Merge3.Join(new[]
{
new GeoPolyline3(new GeoPoint3(3, 4, 0), new GeoPoint3(3, 0, 0)), // reversed middle
new GeoPolyline3(new GeoPoint3(3, 4, 0), new GeoPoint3(8, 4, 0)), // last
new GeoPolyline3(new GeoPoint3(0, 0, 0), new GeoPoint3(3, 0, 0)), // reversed first
});
// One chain, 4 vertices, total length 12.
CoplanarFaces does the same job for a surface. Cutting a body leaves it more finely divided than it needs
to be, and repeated cutting compounds that, so this puts the surface back into as few faces as describe it:
GeoSolid3 tidied = Merge3.CoplanarFaces(subdivided);
// same volume, same surface area, fewer faces
Faces are grouped by the oriented plane they lie on, so a face and one facing the other way are never merged — they are different surfaces that happen to be flat in the same place. Merging a ring of faces keeps the hole in the middle as a hole. Two faces count as touching only where they share a whole edge, so a T-junction stops that one join rather than the whole group; merging under-joins rather than guessing.
Working with large meshes
Every operation above looks at the whole of what it is given, because nothing rules any of it out in
advance. GeoBvh3 is the index that does: a tree of nested boxes over a triangle mesh, in which a ray that
misses a box misses everything inside it, and a box farther away than the best answer so far cannot hold
anything nearer.
var tree = GeoBvh3.FromSolid(solid);
tree.DistanceTo(point); // nearest point on the surface
tree.GetClosestPoint(point);
tree.GetIntersections(ray); // every crossing of the surface
tree.CollidesWith(otherTree); // surface contact between two meshes
Building the tree costs a sort of the triangles, so it pays for itself over repeated queries rather than on the first one. Build it once and keep it: every geometry type here is immutable, so a mesh never goes stale under its index.
The mesh it indexes comes from GeoSolid3.Triangulate, which follows the material of every face: a
concave face is traced rather than spanned, and a hole in a face is left open. So a ray fired through the
notch of an L-shaped body, or through a bolt hole in a plate, passes clean through, and the nearest point
on the surface is a point that is really on it. That is what separates it from GeoFace3.Triangulate,
which fans the boundary from one vertex and is meant only for the signed sums — area, centroid, volume —
where the part of a fan reaching outside the face cancels against the part overlapping it. Use
GeoFace3.TriangulateSurface wherever the triangles stand for material.
Collision3.CollidesWith(solid, solid) builds one internally once the meshes are large enough to be worth
it, and falls back to comparing every pair below that — below the threshold, the plain scan wins.
One caveat, shared with any triangle mesh: a ray running exactly along an edge is reported by both
triangles that share it. Anything counting crossings to tell inside from outside must keep clear of edges,
which is why Containment3 throws its ray again in another direction when a hit lands near one.
Point chains
GeometryHelper.SolidGeometry.Extension covers the step before a polyline exists: a raw list of points,
usually read out of a model and carrying more of them than the geometry needs.
using GeometryHelper.SolidGeometry.Extension;
var traced = new List<GeoPoint3>
{
new GeoPoint3(0, 0, 0), new GeoPoint3(0.0001, 0, 0), new GeoPoint3(5, 0, 0), new GeoPoint3(5, 0, 5),
};
// The second point is a hair away from the first and goes.
List<GeoPoint3> thinned = traced.RemoveConsecutiveNearPoints(new Tolerance(0.001, 0.001)); // 3 points
// One segment per consecutive pair.
List<GeoLine3> segments = thinned.ToGeoLine3s(); // 2 segments
RemoveConsecutiveNearPoints compares each point against the last one kept, not against its original
neighbour, which is what guarantees no two points of the result are coincident within the tolerance. A
huddle collapses onto its first point, and collapsing stops as soon as one point escapes the tolerance
around that anchor, so a long run thins rather than vanishes. Distance is measured in all three
dimensions, so two points that share X and Y but differ in Z are not fused.
The first point always survives; the last one is not privileged. A final point lying within the tolerance of the one kept before it is dropped like any other, so re-append it yourself when the endpoint matters.
ToGeoLine3s leaves the chain open — nothing joins the last point back to the first, so a ring has to
repeat its first point at the end. It does not filter coincident neighbours either, so run
RemoveConsecutiveNearPoints first if zero length segments would be a problem.
Tolerance
Nothing in this library compares coordinates with ==. Every comparison that floating point error can
affect takes a Tolerance, and every such method has an overload without one that reads Tolerance.Global.
var a = GeoPoint3.Origin;
var b = new GeoPoint3(1e-9, 0, 0);
a.IsEqualTo(b); // true — within the default tolerance
a.Equals(b); // false — exact comparison
a.IsEqualTo(new GeoPoint3(0.05, 0, 0), new Tolerance(0.1, 0.1)); // true
Tolerance carries four thresholds: EqualPoint for coincidence, EqualVector for direction,
EqualAngleRad for parallelism, and EqualPlanar for flatness.
Degenerate input is treated as "no answer" rather than being guessed at. A zero-length vector has no
direction, so it is neither parallel nor perpendicular to anything, and Normalize() on it throws while
TryGetNormal reports the failure:
GeoVector3.Zero.IsParallelTo(GeoVector3.XAxis); // false
GeoVector3.Zero.TryGetNormal(out GeoVector3 unit); // false
GeoVector3.Zero.Normalize(); // throws InvalidOperationException
Working in a local frame
GeoCoordinateSystem3 moves geometry between world coordinates and a local frame, and its axes are always
orthonormal whatever is passed in. ToLocal and ToGlobal are exact inverses of each other.
var frame = new GeoCoordinateSystem3(
new GeoPoint3(10, -20, 30),
new GeoVector3(1, 1, 0),
new GeoVector3(-1, 1, 1));
var point = new GeoPoint3(3, -7, 11);
frame.ToGlobal(frame.ToLocal(point)).IsEqualTo(point); // true
GeoTransform3 is a 4x4 matrix applied on the left, so a.Multiply(b) means "apply b, then a". A plane
normal is carried by the inverse transpose rather than by the matrix itself, so it stays perpendicular to
the surface even under a non-uniform scaling.
GeoTransform3 motion = GeoTransform3.Translation(new GeoVector3(10, 0, 0))
.Multiply(GeoTransform3.RotationZ(Math.PI / 2));
motion.Transform(GeoPoint3.Origin); // (10, 0, 0)
motion.Inverse().Transform(motion.Transform(GeoPoint3.Origin)); // back to the origin
Tekla Structures
GeometryHelper.TeklaConvert converts geometry between Tekla Structures and this library: points, vectors,
segments, planes, coordinate systems, bounding boxes, transformation matrices, and the faces and loops of
a Tekla solid. It is a separate project, so the core library carries no dependency on Tekla.
using GeometryHelper.TeklaConvert;
teklaSolid.TryToGeoSolid3(out GeoSolid3 body, tolerance);
body.Volume;
body.TrySubtract(otherBody, out GeoSolid3 left);
Three things are checked rather than trusted on the way in. A Tekla coordinate system whose Y axis is not
quite square to its X axis is squared up. Each face is turned to agree with the normal Tekla gives it, and
the finished body is turned inside out if its signed volume says the whole surface arrived reversed —
without that, volume still measures the same but every containment query answers backwards. And a face
that cannot be made sense of is skipped rather than thrown on, which leaves the body no longer closed, so
ask IsClosed() before trusting a volume.
Tekla models in millimetres with coordinates that can run to hundreds of thousands, and a face of a twelve
metre member is rarely flat to the last decimal. The default EqualPlanar is often too tight for that, so
pass a Tolerance suited to the model rather than relying on the default.
Build and Test
dotnet build Libraries/GeometryHelper.SolidGeometry/GeometryHelper.SolidGeometry.csproj
dotnet test Tests/GeometryHelper.SolidGeometry.UnitTest/GeometryHelper.SolidGeometry.UnitTest.csproj
dotnet test Tests/GeometryHelper.TeklaConvert.UnitTest/GeometryHelper.TeklaConvert.UnitTest.csproj
Every snippet in this README is covered by a unit test in ReadmeExamplesTests.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- GeometryHelper.CommonGeometry (>= 3.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|
3D geometry for engineering models, built to the same design as GeometryHelper.PlaneGeometry.
The four packages in this repository are versioned and released as one set.
NEW IN 3.1.0
GeoSolid3.Triangulate now describes the surface rather than fanning it. The old fan was correct
for the signed sums it was written for — area, centroid, volume, where the part reaching outside
a concave face cancels against the part overlapping it — but the same triangles were read as an
actual surface by Collision3, Distance3 and GeoBvh3, where nothing cancels. On a U-shaped body
the fan spanned the empty notch, and a ray fired up it reported two hits, the nearest point on
the surface came back in mid-air, and a box sitting in the notch collided with the body. Faces
are now cut into triangles that each lie within the material, holes left open. This is a change
in behaviour, not only a fix: anything reading the mesh gets different, correct answers.
GeoFace3.TriangulateSurface is the new per-face entry point; GeoFace3.Triangulate keeps the fan
for the signed sums.
Openings survive cutting and boolean operations. Splition3.TrySplitBy(solid, plane) and all
three Boolean3 operations never read GeoSolid3.Openings and rebuilt their results without them,
so a slab with a duct came back solid — no exception and no false, the halves closed and
measured correctly in every other respect. Splitting now cuts a straddling opening along with
the body; Boolean3 drops the cells filling one, so the cavity survives as real geometry.
GeoSolid3.GetNetVolume measures what was actually removed, cutting the openings out rather than
deducting their volumes. NetVolume keeps the cheap arithmetic and now documents that it
over-deducts an opening drawn overshooting the far face, which is the usual way to draw a
through-hole, and double-counts two openings that overlap.
GeoTransform3.TryGetInverse judges the determinant against the size of the transformation
instead of against zero. Three unit-length axes tilted nearly into one plane give a determinant
around 1e-12 and were reported invertible, yielding a matrix of numbers around 1e12. Inverse and
TryGetInverse gain Tolerance overloads. GeoSolid3.Centroid guards its division with a length
tolerance cubed rather than with double.Epsilon, which only ever caught an exactly zero volume.
Documentation that contradicted the code is corrected: GeoSolid3.TrySplitBy no longer claims the
solid must be convex, and Newell.GetAreaVector no longer claims to return twice the area.
Tolerance.Global now states that it is unsynchronized process-wide state.
SHARED TYPES
- Tolerance, Angle, PointLocation and PlaneSide come from the GeometryHelper.CommonGeometry
package, which GeometryHelper.PlaneGeometry shares. A program working in both dimensions sees
one Tolerance and one Angle rather than two of each, and Tolerance.Global is one setting for
both.
GEOMETRY
- Fifteen immutable types across three families. A curve (GeoLine3, GeoRay3, GeoPolyline3)
encloses nothing; a planar region (GeoTriangle3, GeoPolygon3, GeoCircle3, GeoFace3) encloses
an area; a volume (GeoObb3, GeoAabb3, GeoSolid3) encloses space. Only the last two
families offer Contains, and a planar region counts a point as inside only when it lies on
the carrier plane as well as within the boundary.
- Coplanarity is enforced at construction for GeoPolygon3 and GeoFace3. A polygon that is not
flat has no normal, no area and no interior, so it is refused rather than accepted with every
property on it quietly meaningless.
- GeoObb3 and GeoCoordinateSystem3 orthonormalize their axes in full, so a Y direction that is
not square to X is corrected rather than producing a skewed frame in which ToLocal and
ToGlobal stop being inverses.
- GeoSolid3 carries openings — whole solids subtracted from it — separately from the holes a
GeoFace3 carries, which are flat and belong to one face.
OPERATIONS
- Nine Core classes: Collision3, Containment3, Distance3, Intersection3, Merge3, Parallel3,
Parametrization3, Projection3, Splition3. Every one is mirrored as instance methods on the
types it applies to, and splitting sits on the subject rather than the cutter.
- Collision3 tests two oriented boxes with the separating axis theorem over all fifteen axes,
so two boxes passing each other at an angle are separated by the cross-product axes that a
face-only test would miss.
- Splition3 cuts by four kinds of cutter, and what the result means follows from which. A
position along the subject or an infinite plane divides it in order or by side; a bounded
region cuts only where the subject really passes through it; a closed body sorts the pieces
into inside and outside, because a body has no side the way a plane does.
- Cutting a body by a plane handles a concave body whose faces carry holes: each face is cut on
its own and the surface closing each half is built from the edges the cut left behind. A cut
can leave one half as several disconnected shells, and a plane through a hole turns that rim
into part of the boundary.
- A plate can be cut along a chain marked on it, or against a body to separate the embedded part
from the part standing clear.
- Merge3.CoplanarFaces puts a surface back into as few faces as describe it, which is what keeps
repeated cutting from growing the face count without changing the shape.
- Boolean3 joins two bodies, keeps the part they share, or takes one out of the other. Both bodies
are divided by one shared set of planes so that the two sides of every interface come out as the
same polygon and cancel when the kept cells are glued.
- Parametrization3 covers closed curves as well as open ones, so a polygon and a circle wrap
where a segment extrapolates and a chain clamps.
- Intersection3 returns a single result and reports false when there is not exactly one, so a
segment lying in a plane or two coincident planes are refused rather than answered with an
arbitrary point out of the overlap.
LARGE MESHES
- GeoBvh3 indexes a triangle mesh as a tree of nested boxes, turning ray casting, nearest-point
and mesh-to-mesh contact from a scan of every triangle into a traversal.
- The mesh it indexes follows the material of every face, so a ray fired through the notch of a
concave body or through a bolt hole in a plate passes clean through. Nothing about a tree
depends on what is asked of it, so a body queried repeatedly is worth indexing once with
GeoBvh3.FromSolid rather than through the convenience paths in Collision3 and Distance3,
which build one per call.
- Collision3.CollidesWith(solid, solid) builds one internally once the meshes are large enough
to be worth it, and keeps the plain scan below that.
- GeoSolid3.IsClosed matches vertices through a spatial grid rather than by scanning what it has
already seen, which takes it from quadratic time to linear.
TOLERANCE
- Nothing compares coordinates with ==. Every affected comparison takes a Tolerance and has an
overload reading Tolerance.Global.
- Four thresholds: EqualPoint, EqualVector, EqualAngleRad, and EqualPlanar. The last is
separate because coplanarity is measured far from the reference point, where a hundredth of a
degree of tilt becomes a deviation the point threshold would not tolerate.
- Degenerate input is treated as no answer rather than guessed at: a zero-length vector is
neither parallel nor perpendicular to anything, Normalize throws, and TryGetNormal reports it.