Source
This page is CHANGELOG.md
included verbatim — edit that file, not this page.
Changelog¶
Notable changes to goifc. The API is unstable pre-1.0 — breaking changes land on minor versions, as the README states. Releases before v0.2.0 predate this file.
Unreleased¶
Changed¶
- The documentation moved to https://docs.goifc.org/latest/. The old
blox-eng.github.io/goifc/URLs redirect there once the custom domain is live. Every deploy now checks that the custom domain is still set at thegh-pagesroot and restores it if a deploy dropped it. - goifc has a landing page at https://goifc.org, in
www/. - The landing page animates the goifc mark: a file read into one solid, and two measured parts out, whose quantities show where each number came from. The landing page, README and docs home now describe what goifc does rather than what it is not.
Fixed¶
- The docs' canonical URLs, sitemap and
llms.txtpointed at/latest/dev/, a prefix that does not exist. They now point at/latest/.
v0.13.0 — 2026-09-25¶
Added¶
- IFC4 tessellated bodies are tessellated:
IfcTriangulatedFaceSet, its IFC4X3 subtypeIfcTriangulatedIrregularNetwork, andIfcPolygonalFaceSet. This is the body SketchUp, BlenderBIM and Revit's IFC4 Reference View export. Before, these elements did not even fall back to a box: the fallback only readIfcCartesianPoint, and a face set keeps its points in anIfcCartesianPointList3D, so the element came back with no triangles and a zero bounding box — a total under-report. On a SketchUp 2024 IFC4X3 export, every element with geometry went from vanished to meshed (0 of 10 to 10 of 10). No parity corpus model uses these entities, so the published coverage numbers do not move.
PnIndex remapping is honoured, and only the points triangles reference reach
the mesh, so an unused point in a shared list cannot inflate the bounds. The
original IFC4 release's NormalIndex, which sits where PnIndex now does, is
recognised and ignored rather than boxing the set. An index out of range, a
referenced point without three coordinates, or a face that is not a polygon
declines the whole set rather than meshing it in part, since a partial mesh
would under-report; a declined set now falls back to a box over its point list
instead of vanishing. A polygonal face keeps every loop vertex even where the
ear-clipper gives up on a degenerate loop, so its bounds stay whole. Holes in
IfcIndexedPolygonalFaceWithVoids are filled, which over-reports: the same
trade-off the brep path already makes for inner face bounds.
A triangulated irregular network's triangles flagged invisible (-2 void,
-1 hole) are left out of the mesh, as the spec requires. A network whose
every triangle is invisible yields no geometry rather than a box, since the
spec excludes a void "without falling back on any other geometry".
v0.12.1 — 2026-09-24¶
Fixed¶
- A boolean clip whose second operand is an
IfcPolygonalBoundedHalfSpaceorIfcBoxedHalfSpaceis now applied instead of the element degrading to a bounding box. The gate testedIsA("IfcHalfSpaceSolid"), andstep.Instance.IsAmatches the exact type keyword only — so the subtypes never passed, and the bounded-clipping path below the gate was unreachable. That is Revit's standard mitred wall/slab/beam join. On the public corpus the box-fallback rate goes from 1.9% to 0.0% onduplex_aand from 2.4% to 0.0% onfzk_haus, andUnhandledItemTypesno longer reportsIFCPOLYGONALBOUNDEDHALFSPACE. The freed elements land as extrusions, since a clipped mesh keeps the source of its first operand.
The bounded clip only applies when the boundary polygon is its own axis-aligned bounding box — a rectangle, which is what Revit exports. For any other shape, clipping against the AABB would remove more material than the real boolean does and under-report the element, so the clip declines and the element keeps the conservative box. The rectangle test compares the polygon's shoelace area with its AABB's area at a relative tolerance of 1e-12, computed relative to the first vertex: on a site-placed millimetre model the raw sum cancels coordinates near 1e6 and would misjudge the area.
v0.12.0 — 2026-09-24¶
Added¶
IfcFaceBasedSurfaceModelis tessellated. It had no dispatch case, so every element built from one degraded to a bounding box; it was the largest entry in the measured gap list.duplex_agoes from a 32.1% box-fallback rate to 1.9% — 65 elements move from box to brep, and nothing else moves. The surface model shares its traversal withIfcShellBasedSurfaceModel: both are a set of face sets thatbrepMeshalready tessellates.
v0.11.1 — 2026-09-24¶
Fixed¶
- A self-referencing or cyclic
IfcLocalPlacement.PlacementRelTochain no longer kills the process. Placement composition recursed with no cycle check and no depth bound, so#12=IFCLOCALPLACEMENT(,#12);overflowed the stack — afatal error, not a panic, sorecover()could not catch it. It was reachable fromgoifc.Assembleon arbitrary input, and every release through v0.11.0 is affected: upgrade if you import files you did not author.
The chain walk now detects cycles and is capped at 1024 levels. Both are needed: IFC bounds placement depth nowhere, so a cap alone would have to be tight enough to truncate legal chains, and detection alone would still let a crafted acyclic chain of a million placements exhaust the stack. A cyclic or over-deep chain resolves to the identity for the part it cannot compose — the same answer a missing placement gives. Real buildings nest two to four deep, and at that depth the walk allocates nothing.
v0.11.0 — 2026-09-23¶
Added¶
geometry.UnhandledItemTypesreports, per IFC entity type, how many representation items no tessellation path handles — the items that degrade to a bounding box. It answers "why is this element a box" and ranks which geometry gaps cost the most accuracy on real files. The count is occurrences, once per element that reaches the item, not distinct entities — it resolves throughIfcMappedIteminto the shared geometry it maps to, so a type's count can exceed the number of such entities in the file when several elements share one mapped representation. It also resolves throughIfcBooleanResult/IfcBooleanClippingResultinto their operands, because the wrapper itself is dispatched: without that, a boolean over an operand with no tessellation path at all produced a bounding box and no count, hiding the very gap the map exists to rank. A non-DIFFERENCEboolean is counted as itself, since there the unsupported thing is the operation rather than an operand. It reports the types that have no tessellation path at all, which is not every cause of a fallback: a dispatched path that is attempted and declines partway — an unbuildable profile, an unclosable shell — degrades to a box just the same, with nothing in this map to name it. Rank gaps from it knowing the counts are a lower bound, and that a type's absence is not evidence it never falls back.
v0.10.0 — 2026-09-17¶
Added¶
geometry.Polygon2D— a hole-nested ring set in one plane's (u, v) frame, in metres: an outer ring and the voids inside it, implicitly closed. It is whatElement.SilhouetteOndescribes one step on — that method returns a FLAT[]Loophole-nested by winding, which can hold several outer loops, so the conversion is onePolygon2Dper outer loop. Winding decides nothing: the rings are filled even-odd, so an outline stored and read back still measures correctly.geometry.PolygonsFromLoops(loops)— that conversion: onePolygon2Dper outer loop of one element's outline, each holding the loops nested directly inside it, decided by containment rather than winding. An island inside an opening is an outer again. It refuses loops it cannot nest honestly: too short, non-finite or enclosing nothing; crossing or running along each other; passing through each other at a corner; or touching at every point it could test.geometry.UnionArea2D(polys)— the area the polygons cover TOGETHER, with anything two of them share counted once and holes subtracted unless another polygon fills them. This is the question a facade asks: a cladding band in front of the wall behind it is one surface, and adding the two areas overstates it by the shared strip. Net area already deducts the union of a host's openings rather than their sum; this is that operation exported, for outlines the caller is holding rather than elements the library is holding.geometry.UnionMeasure2D(polys)— the same area plus the length of the union's boundary, from one walk of one boundary so the two cannot describe different shapes. The boundary includes a void the union still has and excludes a seam where two polygons merge, exactly as the shared area is counted once.
Both return ok == false rather than a figure nobody can trust, and a caller
must not substitute zero — zero is a measurement, this is the absence of one.
They refuse when there are no polygons at all, when a ring has fewer than
three points or encloses no area, when a coordinate is NaN or infinite, when
two edges of one polygon cross or run along each other (a bow-tie, a ring
doubling back over its own edge, a hole cutting through its outer ring),
when the rings do not describe the surface they claim (a hole outside its
outer ring, two holes overlapping), or when the union boundary did not
close. Rings may touch at a point, because Element.SilhouetteOn emits such
outlines.
Both measure the same outline the same wherever the model sits. That is worth
stating because SilhouetteOn projects WORLD coordinates and ElevationPlane
has no origin to subtract them against, so a model on a national grid arrives
at x≈4.6e5, y≈4.7e6 — the rings are recentred before measurement so neither
the figure nor the refusal depends on it.
The polygons must already be in ONE plane's frame; nothing here can detect a mismatch. Detail below the boundary walk's existing 1e-5 m weld is not detail it can see. See measuring silhouettes.
v0.9.3 — 2026-09-02¶
Fixed¶
BuildFacingsno longer re-transforms the whole model into every 10 cm mid-height band. It built one occupancy grid per distinct band — 152 of them on kb645.ifc — and rasterized the model into each from scratch: 164,670worldPointscalls for 1,878 elements, 239M triangles transformed to cover 3.3M, 72x redundant. World points are now computed once and shared, and the bands (which are independent) are classified on a bounded worker pool.
15.233s -> 2.139s (7.1x) on kb645.ifc, with the classification bit-identical:
digest c0b435cfc8457ca5, n=1519, on both sides — every field of every Facing,
ids sorted, full float precision. The import pass as a whole goes 27.6s -> 13.7s.
Concurrency is bounded rather than one goroutine per band, because a grid is the large allocation and the serial loop deliberately kept one live at a time. Measured rather than assumed: peak heap went down, 692.4 MB serial to 675.6 MB parallel, and total allocation fell 32%. The retained cache is 54.8 MB.
The bound is on total cells in flight, not merely on the number of grids. A grid
count alone is only a memory ceiling while every grid is small, and
occupancyMaxCells permits a single band ~40M cells — so eight such bands at once
would be roughly eight times anything the serial version could hold, on exactly the
georeferenced, site-scale models that cap exists for. The worker pool is therefore
sized against the largest band's grid so the aggregate never exceeds what one
maximal grid was already allowed: peak grid memory is no higher than before this
change on any model, and parallelism only decides how that fixed budget is split.
Two ordering properties are load-bearing and now have tests: the cache is filled
before the workers start, so it is read-only while shared (filling it lazily would
be a data race whose writers compute identical values — invisible except under
-race); and per-band results merge in sorted key order, never completion order,
so a malformed file repeating a GlobalId across bands resolves the same way it
always did.
v0.9.2 — 2026-08-21¶
Fixed¶
- An elevation outline left open by ONE short seam is now closed across it and drawn, instead of the element being discarded whole (#37). The boundary walk is exact in real arithmetic only; on float32-derived world coordinates it can classify a sub-edge inconsistently and leave the ring open, and the all-or-nothing verdict then threw away everything. On kb645 that lost 45 exterior elements, every one of them to a single open chain with a gap between 0.9 mm and 9.1 mm — in one case 56 segments of correct outline discarded for one 9.1 mm seam. 914 entities drawn before, 959 after, 45 of them bridged.
The repair is deliberately the narrowest thing that recovers that case: EXACTLY one vertex short by one and one long by one — two loose ends, so one open chain — and a gap of at most 10 mm. Several chains, a vertex unbalanced by more than one, or a longer gap is a torn outline rather than a seam that failed to weld, and is refused exactly as before.
New ElevationEntity.OutlineBridged and Element.SilhouetteBridgedOn report it,
and reporting is the point: a bridged outline is a drawing that is right and a
measurement that is invented, because the bridging segment is area no face in the
mesh accounts for. Quantities are unaffected — unionMeasure2D, and every other
measurement path, still goes through unionBoundary and its unchanged strict gate.
Total a facade with Facing.FaceArea or NetAreas, never by integrating an
outline.
v0.9.1 — 2026-08-21¶
Fixed¶
Scene.Elevations/Scene.ElevationsWith/Scene.ElevationOnno longer admit a host to the two sheets perpendicular to it (#28). The membership test wasdot(normal, plane.N) > 0— anything not pointing away. A wall perpendicular to a sheet has a dot of zero in exact arithmetic and a hair above it once a real placement transform has been through the normal, so it was admitted; and becauseSilhouetteOnprojects the whole solid, an edge-on wall admitted on a rounding error still drew its full thickness by height. On kb645 that put 913 of 950 drawn hosts — and all 87 of 87 ETICS hosts — on two perpendicular sheets, with the secondary carrying 10-50% of the dominant sheet's area.
Membership is now a bearing test against a documented threshold: both vectors are
flattened to horizontal, renormalized, and compared against cos(45°) — the same
quantity Facing.Azimuth bins by, which is the point, since the elevation
membership test disagreeing with the engine's own orientation classifier is what
#28 is. Against a four-sheet compass set an unambiguous host is now drawn exactly
once; a host at a true 45° is genuinely diagonal and lands on both, a documented
tie rather than an accident. On kb645: 914 of 914 drawn hosts on exactly one sheet,
87 of 87 ETICS hosts on exactly one sheet.
The threshold carries a documented 1e-12 tolerance, and it is not decoration.
Normalizing the everyday diagonal [1, 1, 0] lands one ULP BELOW cos(45°), so a
bare >= rejects it on both sheets and deletes the wall from the drawing — exactly
the failure the >= exists to prevent, reintroduced by rounding. 1e-12 clears one
ULP by four orders of magnitude while costing ~8e-11 of a degree of angular slack.
Flattening to horizontal is load-bearing rather than tidiness. A raw 3-D dot conflates "pointing the wrong way" with "tilted off vertical", so a wall leaning 45° while squarely facing east has its dot dragged under the threshold by the Z term and is dropped from every sheet — which, measured on kb645, silently deleted 45 exterior proxies from the drawing. A wall nobody draws is worse than a wall drawn twice, because nobody checks it.
Behaviour change worth reading before you upgrade. 36 fewer hosts are drawn on
kb645 (950 → 914). Every one of them was previously drawn ONLY as an edge-on sliver
on a perpendicular sheet: their own facade admits them, but SilhouetteOn returns
an empty outline there, so they never appeared on the elevation they belong to.
That empty face-on silhouette is a separate, pre-existing defect and is not
addressed here. This release stops drawing the sliver; it does not yet draw the
wall.
v0.9.0 — 2026-08-19¶
Added¶
Scene.ElevationsWith(f, r, planes, facings)—Scene.Elevationsover a classification the caller already holds.BuildFacingsdominates the cost of drawing a facade set (on a ~1,900-element model, roughly 15s of a 17s call), and its result is worth more than the drawing:Facing.FaceAreabinned byFacing.Azimuthis the sound way to total a facade, because summing the sheets is not — an element with two exposed sides is drawn on two perpendicular sheets, so per-sheet totals double-count it. A caller wanting both the drawings and the quantities previously had to classify the same scene twice. A nil or empty map is taken at face value, not as a request to build one: silently classifying there would restore the exact cost the call exists to avoid, invisibly, because the drawing would still look right.ImportNode.OpeningDeductionandImportNode.ProjectedGross— the two halvesNetAreais the difference of, on the import contract.NetAreaalone cannot be aggregated: a host with noIfcRelVoidsElementis ABSENT from the reconciliation, so summing nets over a facade silently drops every solid wall. Netting a total means subtracting the DEDUCTION from the gross being totalled.ProjectedGrosscomes with it because both are measured on the host's winning projection axis, which foreshortens gross and deduction by the same factor — so a caller netting an unforeshortened gross (Facing.FaceArea) subtractsOpeningDeduction * (FaceArea / ProjectedGross), notOpeningDeductionraw. WithoutProjectedGrossthat bias is not merely uncorrected, it is invisible. Present exactly whenNetAreais, so an untrusted host never publishes a zero deduction that would read as "this wall has no openings".ImportNode.HasOpenings— whether the element carriesIfcRelVoidsElementopenings at all. This is the fact the nil-able area fields cannot express: they are absent for two OPPOSITE reasons — the host has none, so its net equals its gross; or its reconciliation was refused, so its net is unknown. Reading absence as "no openings" reports a fully-glazed wall as solid; reading it as "unknown" drops every solid wall from a facade total. Always meaningful, so a plain bool rather than a third nil-able field.
v0.8.1 — 2026-08-18¶
Added¶
ImportNode.OpeningPerimeter— the opening union's boundary length (m) on the import contract, so the measurement v0.8.0 added is actually reachable by consumers, which readImportNoderather thanNetArea. Present exactly whenNetAreais: both are published from one trusted reconciliation, so a consumer can never read a confident perimeter beside an absent net.
v0.8.0 — 2026-08-18¶
Added¶
NetArea.OpeningPerimeter— the boundary length, in metres, of the same opening unionOpeningDeductionmeasures the area of. Facade trades bill reveals (the returns around a window or door) per linear metre, and the length they follow is the union's outline rather than the sum of the individual voids' perimeters: where two footprints merge, the seam between them is interior to the union, and it belongs to the outline no more than the shared area belongs to the deduction twice. It is0when the host is untrusted, exactly likeOpeningDeduction, and both numbers come from one boundary walk so they can never describe different shapes. On a real ArchiCAD export it agrees to within 0.1 m with an independent per-void bounding-box measurement, and every host satisfiesperimeter² >= 4·pi·area.
v0.7.0 — 2026-08-17¶
Changed¶
Assembledgained an unexported field recording the*step.Fileit was built from, soBuildImportFromcan reject an assembly paired with a different file rather than silently joining data across models. Breaking for unkeyed composite literals —Assembled{r, s}no longer compiles; callers using keyed fields (Assembled{Result: r, Scene: s}) are unaffected.- The projected-polygon union engine now reports a closed boundary or refuses
the host outright, rather than silently returning the residual of an
unclosed walk.
unionArea2DandunionBoundarygained anokresult,silhouetteRingsreturns no rings instead of a wrong one, andNetAreasmarks such a host untrusted with a reason — the mechanism it already had for exactly this. Fixes a defect where an unclosed boundary's area integral, taken about the world origin, returned the residual scaled by the model's distance from the origin rather than a slightly wrong number (a 0.58 m² panel 47 m out reported 30.69 m²).
Added¶
(geometry.Element).SilhouetteOn(p)— the projected-polygon union of the faces opposingp.N, as seen from the+p.Nside, inp's UV coordinates. UnlikeFootprintOnit never falls back to a bounding box: a caller asking for a projection is asking a question a rectangle does not answer, so an absent outline is reported as absent.geometry.ElevationPlane(dir)— the vertical plane an elevation drawn alongdiruses: world up stays up on the page, unlikePlaneFromNormal's unspecified in-plane orientation.okis false for a non-finitediror one with no horizontal component (straight up or down is a plan, not an elevation).geometry.ElevationView/ElevationEntity— the orthographic facade view a plane projects: each entity's outline and punched-out openings in the plane's UV coordinates, plus its depth from the viewer. Membership is deliberately narrow —ExposureExterioronly, outward normal facing the viewer — so a courtyard wall is excluded and a slab, roof or column (no dominant vertical face family, so noFacing) never appears. Outline area minus opening area reconciles with that host'sNetAreasentry when both are measured on the same plane.(*geometry.Scene).ElevationOn(f, r, p)— projects the scene ontopand returns its elevation, classifying facings on every call.(*geometry.Scene).Elevations(f, r, planes)— one view per plane, classifying the scene's facings at most once (on the first valid plane) and sharing that work across the rest, sinceBuildFacingsdoesn't depend on the plane. An invalid plane yields its zero-value view in that position rather than being dropped, so the result stays index-aligned withplanes.ifc.BuildImportFrom(f, a)—BuildImportsplit into itsAssemblestage plus the import-contract assembly, so a caller that also wants theResultorScene(for an elevation, a net-area check, a GLB) can reuse the tessellationAssemblealready did instead of paying for it twice.Assembledmust be paired with the same*step.Fileit was built from (checked by pointer identity) —BuildImportFromrejects a mismatched pair rather than risk anExpressIDcollision silently joining one model's data onto another's element.
v0.6.0 — 2026-08-17¶
Changed¶
geometry: an opening now deducts its true projected silhouette from a host's net area, rather than its span. (#23)
v0.5.0 — 2026-08-17¶
Changed¶
geometry: the silhouette is now computed as a true projected-polygon union, rather than approximated. (#22)
v0.4.0 — 2026-08-17¶
Changed¶
geometry: net area now deducts the union of a host's opening footprints, rather than their sum — an opening overlapping another no longer gets double-deducted. (#20)
v0.3.0 — 2026-08-17¶
Added¶
geometry.Facing— an element's outward direction in world space: the area-weighted dominant normal of its vertical faces, signed to point at the exposed side, with anExposureand aConfidence.geometry.Exposureand its three values —ExposureExterior(the side reaches open air outside the building, and the only exposure that belongs on a compass elevation),ExposureEnclosed(a void the building encloses with nothing overhead: a courtyard, a lightwell — weather-exposed but on no elevation),ExposureInterior(no exposed side; an internal partition).geometry.FacingOf(e)— the facing of one element, orok == falsewhen it has no dominant vertical face family (a column, a slab, a degenerate mesh). With no neighbours both sides reach open air, so an element classified alone gets its axis,ExposureExterior, an ARBITRARY sign and low confidence.geometry.BuildFacings(elems)— classifies every element against the others, keyed byGlobalID. This is what lets the sign be decided at all; prefer it toFacingOfwhenever the neighbours are available. Elements with no facade are absent from the map rather than present with a zero value.(geometry.Facing).Azimuth(trueNorth)— the compass bearing in degrees clockwise fromtrueNorth, in[0, 360). Passmodel.TrueNorth(file)for a real bearing, or{0,1}for a model-space one.geometry.LayerAxis(e, ls)— the world direction a layer stack runs along, from the first declared layer toward the last. Compare it against aFacing.Normalto learn whether the declared order already runs from the exposed face inward: a negative dot product means the first declared layer is the outermost. The library reports the direction; reordering is the consumer's decision.model.TrueNorth(f)— the model's north direction in world XY, unit length, offIfcGeometricRepresentationContextattribute 5. Absent, malformed and zero-length norths all yield(0,1).
Known limitations¶
- Grid resolution is the failure mode. The outward sign comes from a
horizontal-slice occupancy grid at a fixed 10 cm cell, so a gap narrower than
one cell seals and an element thinner than one cell vanishes. Rasterization
is conservative — a cell the cross-section touches counts as occupied — so
the failure leans toward sealing, which reads as
ExposureInteriorat low confidence rather than leaking open air into a room. A slice taken through a fully glazed storey with no modelled mullions finds no occupancy to enclose it and reports its walls freestanding, again at low confidence. Threshold onConfidence: below ~0.5 the sign is a guess, and in a quantity context a wrong bin is a wrong invoice. Facing.FaceAreais the GROSS facade area of one side. It is measured against the resolvedNormalafter the sign is known, so summing it over the elements binned to one elevation gives that elevation's area. Openings are not subtracted — for net quantities useNetArea. It is the only place this package reads triangle winding: a mesh wound inward throughout reports the element's inner face, equal on a plain wall and smaller on a stepped one.- The sign is probed outward from the element's bounding-box centre, so an L-shaped or strongly curved element whose centre falls outside its own body degrades to low confidence.
BuildFacingscosts O(bands × elements), where a band is a distinct quantized element mid-height rather than a storey. Peak memory is one grid regardless of the band count, but the time is not bounded that way.
v0.2.0 — 2026-08-13¶
Breaking¶
geometry.LoopBelowis renamed togeometry.LoopSilhouette. The constant's string value is UNCHANGED ("below"), so drawing data already on disk stays valid and renderers matching the literal need no change. The fix is source-only: rename the identifier at the call site.
The old name described the only plane the package could cut — a horizontal one, always seen from above. Now that any plane can be cut, "below" is no longer what the role means, but it is still what the wire format says.
Added¶
geometry.Plane— a cutting plane as an origin plus an orthonormal right-handed basis (U,Vspan the plane;Nis the normal).geometry.HorizontalPlane(cutZ)— the planeFootprinthas always cut.geometry.PlaneFromNormal(origin, n)— derives a valid basis from a normal.ok == trueguaranteesValid(). The in-plane orientation ofUis deterministic but unspecified; build thePlaneyourself if you need a particular one.geometry.Plane.Valid()— reports whether a basis is finite, orthonormal and right-handed.SectionOnandFootprintOnemit no rings for a plane that fails it, so a caller that hand-builds a basis can check up front and tell a bad basis apart from a plane that genuinely missed the mesh.(geometry.Element).SectionOn(p)— the closed cut rings where an element's mesh crossesp, inp's UV coordinates. Cut rings only: it never falls back to a silhouette or a bounding box, so a caller building a section learns that the plane missed rather than receiving a fabricated outline.geometry.FootprintOn(e, p)—Footprintgeneralized to any plane, keeping the cut → silhouette → bounding-box fallback ladder.
Changed¶
geometry.Footprint(e, cutZ)keeps its signature and behaviour for every finitecutZ; it now delegates throughHorizontalPlane. A non-finitecutZyields nil, where it previously returned a ring built around NaN.
Fixed¶
PlaneFromNormalno longer reports success for a finite normal whose squared length overflows to infinity, which left an all-zero basis behind.- The bounding-box fallback no longer emits NaN corners for an element whose bounds were never measured.
Known limitations¶
- A plane that contains two edges of a solid while bisecting it yields no cut
rings. A triangle contributes a crossing segment only when it has a vertex
strictly above and one strictly below, so faces meeting the plane along an
edge contribute nothing and the ring cannot close. Through
FootprintOnthis degrades further: the cut is returned taggedLoopSilhouetterather thanLoopCut. - For a non-closed mesh the silhouette depends on which way
Npoints, and one of the two directions can fall through to the bounding-box fallback. For a closed solid the outline is invariant under flippingN.