Documentation
¶
Overview ¶
Package geometry builds proxy geometry (a view of the semantic model, no CAD kernel) from a parsed IFC step.File plus the model package's model.Result, and emits a single Y-up GLB whose node names are element GlobalIds.
COORDINATE FRAMES — meshes are local, placements and bounding boxes are world. Element.Verts are element-local meters; Element.Placement maps them into world space; Element.BBoxMin and Element.BBoxMax are already world. Deriving a direction from Verts and a position from the BBox mixes the two frames and is wrong without erroring — use Element.WorldVerts for positions and Element.WorldNormal for directions.
Index ¶
- func BuildFacings(elems []Element) map[string]Facing
- func LayerAxis(e Element, ls model.LayerSet) ([3]float64, bool)
- type Element
- type ElevationEntity
- type ElevationView
- type Exposure
- type Facing
- type GeomSource
- type Loop
- type LoopRole
- type NetArea
- type Plane
- type Scene
- func (s *Scene) DerivedQuantities() map[string]model.Quantities
- func (s *Scene) ElevationOn(f *step.File, r *model.Result, p Plane) ElevationView
- func (s *Scene) Elevations(f *step.File, r *model.Result, planes []Plane) []ElevationView
- func (s *Scene) ElevationsWith(f *step.File, r *model.Result, planes []Plane, facings map[string]Facing) []ElevationView
- func (s *Scene) NetAreas(f *step.File, r *model.Result) map[string]NetArea
- func (s *Scene) Stats() Stats
- func (s *Scene) WriteGLB(w io.Writer) error
- type Stats
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildFacings ¶ added in v0.3.0
BuildFacings classifies every element against the others, keyed by GlobalID. Elements with no facade are absent from the result rather than present with a zero value, so a caller cannot mistake "declined" for "faces +X".
Elements sharing a GlobalID collapse to the last one classified; that is a malformed model, and de-duplicating silently would hide it.
func LayerAxis ¶ added in v0.3.0
LayerAxis returns the world direction ls stacks along, from the FIRST declared layer toward the last, or ok=false when the set carries no resolvable direction (a bare IfcMaterialLayerSet with no usage, or an unrecognized LayerSetDirection).
This closes the gap model.MaterialLayers documents but cannot fill: the declared sense says which way the stack runs from the reference line, which is not enough on its own to say which end is outside, because that needs the element's placement — which is here and not there.
Compare the result against a Facing.Normal to learn whether the declared order already runs from the exposed face inward: a NEGATIVE dot product means the first declared layer is the outermost one. Consumers that describe a build-up outside-in need exactly this; taking the declared order on trust is a coin flip per element, and getting it backwards silently swaps the outer cladding with the inner finish.
This library reports the direction and does not reorder Layers — mapping it onto a build-up convention is the consumer's job, as with LayerSet.Direction.
Types ¶
type Element ¶
type Element struct {
GlobalID string
// Verts are ELEMENT-LOCAL X,Y,Z triples in meters. They are NOT world
// coordinates: apply Placement (or call [Element.WorldVerts]) to obtain
// world positions. BBoxMin/BBoxMax below ARE world — mixing a
// Verts-derived direction with a BBox-derived position silently yields
// wrong results in the local frame. For directions use
// [Element.WorldNormal], which drops the translation.
Verts []float32
Tris []uint32
Placement model.Mat4 // local -> world, meters, IFC-native Z-up (-> GLB node.matrix)
BBoxMin [3]float64 // world-space AABB, meters
BBoxMax [3]float64
Source GeomSource
}
Element is one element's proxy mesh in ELEMENT-LOCAL meters, plus its world placement and world-space AABB. Verts is X,Y,Z triples; Tris indexes them.
func (Element) SectionOn ¶ added in v0.2.0
SectionOn returns the closed CUT rings where e's mesh crosses p, in p's UV coordinates (meters), hole-nested and tagged LoopCut. Winding and determinism guarantees match the horizontal path exactly.
Returns nil when the plane misses the mesh, the mesh is degenerate, p's basis is invalid, or the plane contains a solid's edges while bisecting it (a triangle only emits a crossing segment when it has a vertex strictly above AND a vertex strictly below the plane; a face that merely touches the plane along an edge contributes nothing, so the cut ring cannot close — a known limitation, not a genuine miss; see TestSectionOnPlaneContainingEdgesIsKnownGap). Unlike FootprintOn it never falls back to a silhouette or a bounding box: a caller building a section wants to know the plane missed rather than receive a fabricated outline.
func (Element) SilhouetteBridgedOn ¶ added in v0.9.2
SilhouetteBridgedOn is Element.SilhouetteOn plus whether the outline had to be closed across a short gap to exist at all.
bridged=true means the drawing is right and a measurement taken off it would not be: a segment no face in the mesh accounts for was added, so up to the gap length times the local extent of the area is invented. Measure with NetAreas or Facing.FaceArea, never off a bridged outline. See bridgeOpenBoundary for the bound and why the repair exists.
func (Element) SilhouetteOn ¶ added in v0.7.0
SilhouetteOn returns the outline of e as seen from the +p.N side: the projected-polygon UNION of the faces opposing p.N, in p's UV coordinates (meters), hole-nested and tagged LoopSilhouette.
This is the projection primitive FootprintOn has always used internally and never exposed. Where SectionOn answers "what does this plane cut through", this answers "what do I see looking at the solid from here" — the shape a facade drawing is made of, and the shape a quantity is measured on.
Faces at DIFFERENT depths merge into one filled outline rather than leaving internal edges, so a recessed balcony, a set-back storey or a projecting bay reads as the one silhouette it looks like.
Returns nil when p's basis is invalid, when the mesh is missing or degenerate, or when the boundary walk could not close the outline (see unionBoundary). It never substitutes a bounding box the way FootprintOn's last-resort branch does: a caller asking for a projection is asking a question a rectangle does not answer, so an absent outline is reported as absent.
For a CLOSED solid the outline is invariant under flipping p.N. An OPEN mesh has no such symmetry — a one-sided surface opposes exactly one of the two directions and yields nothing for the other — so a caller holding non-closed geometry must choose p.N deliberately.
func (Element) WorldNormal ¶ added in v0.1.2
WorldNormal rotates a local DIRECTION (a face normal, an extrusion axis) into world space using only the 3x3 rotation part of e.Placement — a direction must not pick up the placement's translation. Use this, not differencing two WorldVerts, whenever the quantity is a direction.
Magnitude is preserved, not normalized: a placement composed from IfcAxis2Placement3D is orthonormal and right-handed, so a unit local direction comes back unit-length and a scaled one comes back scaled by the same factor. That orthonormality is also why rotating the direction is the correct transform here and no inverse-transpose is needed.
func (Element) WorldVerts ¶ added in v0.1.2
WorldVerts returns e.Verts transformed by e.Placement: world X,Y,Z triples in meters, IFC-native Z-up. Verts holding fewer than 3 floats returns nil; a trailing partial triple is dropped rather than emitted half-transformed.
Allocates a fresh slice on every call; callers in a hot loop should hoist it. The transform runs in float64 and rounds to float32 on the way out, so the result agrees with BBoxMin/BBoxMax to float32 precision, not exactly — that gap grows with distance from the origin, so code needing exact world positions on a georeferenced model should transform through Placement in float64 itself.
type ElevationEntity ¶ added in v0.7.0
type ElevationEntity struct {
GlobalID string
IFCClass string
// Outline is the element's silhouette on the view plane, hole-nested. Outer
// rings CCW, holes CW, meters.
Outline []Loop
// Openings are the IfcRelVoidsElement voids projected onto the SAME plane,
// wound CW where they fall inside an outline ring so an even-odd or nonzero
// fill renders them as cutouts.
Openings []Loop
// Depth is the distance from the view plane along −N to the element's
// NEAREST point, meters. Smaller is nearer the viewer.
Depth float64
// OutlineBridged reports that Outline exists only because a gap of at most
// 10 mm was closed across — a segment no face in the mesh accounts for.
//
// The drawing is right; a measurement taken off this outline is not. Up to
// the gap length times its local extent of area is invented, so total a
// facade with Facing.FaceArea or NetAreas, never by integrating Outline.
// Without the repair the element is not drawn AT ALL: on a 29.5 MB ArchiCAD
// export 45 exterior elements were being discarded whole for one sub-
// centimetre seam apiece. See bridgeOpenBoundary.
OutlineBridged bool
}
ElevationEntity is one element's contribution to an elevation: its outline in the view plane's UV coordinates, plus the openings punched through it.
type ElevationView ¶ added in v0.7.0
type ElevationView struct {
Plane Plane
Entities []ElevationEntity // sorted by (Depth, GlobalID)
Bounds [2][2]float64 // {{uMin, vMin}, {uMax, vMax}}, meters
}
ElevationView is the orthographic view of a set of elements from one direction — the drawable counterpart of a floor plan.
MEMBERSHIP is deliberately narrow: an element appears only when its Facing says it is ExposureExterior and its outward normal points at the viewer. Two consequences a consumer must know. A courtyard or lightwell wall is weather- exposed but belongs on no compass elevation, and counting it would inflate the drawing and the quantity together. And an element with no dominant vertical face family — a slab, a roof, a column — has no Facing at all, so it is ABSENT: this is an elevation of the facade elements, NOT a full orthographic render, and it carries no roof line or exposed slab edge.
That narrowness is what keeps every entity here a host whose outline can be reconciled against its NetAreas entry.
RECONCILING WITH NetAreas: outline area minus opening area equals that host's Net, but ONLY where the two are measured on the same plane. NetAreas projects each host onto ITS OWN winning axis, which for a wall square to this view is this plane and for a wall at any other orientation is not. On such a host the two numbers are both right and describe different projections, and the elevation's is the foreshortened one. Compare the host's winning axis with this plane's normal before reading a difference as drift. Measured on a 29 MB ArchiCAD IFC2X3 export over four compass directions: 73 of 74 same-plane voided hosts agree to 1e-6.
type Exposure ¶ added in v0.3.0
type Exposure string
Exposure is what the Facing.Normal side of an element reaches.
const ( // ExposureExterior — the side reaches open air outside the building. This // is the only exposure that belongs on a compass elevation. ExposureExterior Exposure = "exterior" // ExposureEnclosed — the side reaches a void the building encloses with // nothing overhead: a courtyard, a lightwell. Weather-exposed, but on no // elevation, so counting it into one over-reports the facade. ExposureEnclosed Exposure = "enclosed" // ExposureInterior — no exposed side was found; an internal partition. ExposureInterior Exposure = "interior" )
type Facing ¶ added in v0.3.0
type Facing struct {
// Normal is unit length in world space and points at the exposed side.
Normal [3]float64
// FaceArea is the area of the faces pointing along Normal, in m². ONE side:
// the outer face of a wall, not the sum of its two faces, so summing it over
// the elements binned to one elevation gives that elevation's gross area.
//
// Gross, not net — openings are not subtracted here.
FaceArea float64
// Exposure is what the Normal side reaches.
Exposure Exposure
// Confidence is 0..1. Below ~0.5 the sign is a guess; a consumer that would
// rather show "unclassified" than file a wall under the wrong elevation
// thresholds here. In a quantity context a wrong bin is a wrong invoice.
Confidence float64
}
Facing is an element's outward direction in world space: the area-weighted dominant normal of its vertical faces, signed to point at the exposed side.
The sign is resolved by probing outward from the element's BBox CENTRE, so an L-shaped or strongly curved element whose centre falls outside its own body probes from a point that is not in the element at all and degrades to low confidence.
func FacingOf ¶ added in v0.3.0
FacingOf returns the facing of e, or ok=false when the element has no dominant vertical face family (a column, a slab, a degenerate mesh).
Outwardness is not a property of one element: with no neighbours both sides reach open air, so an element classified alone gets its axis, ExposureExterior, an ARBITRARY sign and low confidence. Prefer BuildFacings whenever the neighbours are available — it is what lets the sign be decided at all.
func (Facing) Azimuth ¶ added in v0.3.0
Azimuth returns the compass bearing of f in degrees CLOCKWISE from trueNorth, in [0, 360). Pass model.TrueNorth(file) for a real bearing, or {0,1} for a model-space one.
Only the XY part of the normal has a bearing. A normal with no horizontal component — which FacingOf never returns, since it excludes near-vertical faces — has no bearing at all and yields 0.
type GeomSource ¶
type GeomSource string
GeomSource records which tessellation path produced an element's mesh: extrude/brep are real geometry, obb is the bounding-box fallback.
const ( SourceExtrude GeomSource = "extrude" SourceBrep GeomSource = "brep" SourceOBB GeomSource = "obb" )
type Loop ¶
Loop is one closed ring of an element's plan footprint, in the cutting plane's UV coordinates, meters (for HorizontalPlane these are world X and Y). Coordinates are emitted in the IFC-native orientation; a renderer whose Y axis points down applies its own flip. Outer rings are wound CCW; HOLE rings (an inner boundary of a hollow/annular section) are wound CW and share the outer ring's Role — so an even-odd or nonzero polygon fill renders them as cutouts with no extra field.
func Footprint ¶
Footprint is FootprintOn at the horizontal plane z = cutZ, for callers that only ever wanted a floor plan. Behaviour is identical to the pre-Plane implementation for every finite cutZ.
One deliberate difference: a non-finite cutZ now yields nil, where the old implementation returned a silhouette or AABB ring built around NaN. Callers that indexed the result unconditionally should check its length.
func FootprintOn ¶ added in v0.2.0
FootprintOn is the plan geometry of ONE element on plane p (world meters, in p's UV frame): the section-cut rings (poché, hole-nested) if the plane crosses the solid, else the silhouette of faces opposing p.N drawn as context, else the element's world AABB projected into p's UV.
Returns nil when p's basis is invalid (see Plane) — no rings rather than wrongly-wound ones — and likewise when the last-resort AABB fallback is reached with a non-finite bounding box, since projecting one yields NaN coordinates rather than a rectangle.
The plane-contains-edges gap documented on SectionOn is WORSE here: instead of returning nil, FootprintOn falls through to the silhouette branch and returns one ring tagged LoopSilhouette — a genuine section rendered as light context rather than as cut poché, with no signal that a real cut was missed. A caller that needs to reliably distinguish a real cut from context must not rely on Role alone in this case.
For a NON-CLOSED mesh the silhouette branch is direction-dependent: an open or one-sided surface opposes only one of the two normal directions, so the flipped direction yields no silhouette and falls through to the bounding-box fallback — a rectangle tagged LoopSilhouette in place of the real outline, with no signal. Closed solids are unaffected: their outline is invariant under flipping p.N (see silhouetteRings).
type LoopRole ¶
type LoopRole string
LoopRole tags a footprint loop as section poché or light context.
The string VALUES are a serialization contract: consumers persist them in drawing data and match them as literals in renderer code. LoopSilhouette's value stays "below" for that reason — it is the name that was wrong, not the value. Do not change the values without a coordinated consumer and data migration.
const ( LoopCut LoopRole = "cut" // section poché — the plane crosses the solid // LoopSilhouette is the outline of faces opposing the plane normal, drawn // as light context. Named "below" historically, when the only supported // plane was horizontal and this was always the view from above. LoopSilhouette LoopRole = "below" )
type NetArea ¶
type NetArea struct {
// Gross is the host's largest projected silhouette, m² — the outward faces
// on the winning axis counted once per covered square metre.
//
// This is NOT DerivedQuantities Area, which is the ifcopenshell-parity
// get_max_side_area Σ. The two agree on any host whose outward faces do not
// hide one another (every prismatic wall) and diverge on a pilaster,
// projecting bay or brise-soleil, where only the union is drawable. Parity
// is that quantity's job; exactness is this one's.
Gross float64
OpeningDeduction float64 // union of the opening footprints on the host axis, m² (0 when untrusted)
// OpeningPerimeter is the boundary length of that SAME union, in metres
// (0 when untrusted). Reveals — the returns around a window or door — are
// billed per linear metre in facade trades, and the length they follow is
// this outline, not the Σ of the individual voids' perimeters: where two
// footprints merge, the seam between them is interior, and it belongs to
// the outline no more than the shared area belongs to the deduction twice.
OpeningPerimeter float64
Net *float64 // net area, m² — populated ONLY when Trusted; nil otherwise.
// Never fabricated (matches the engine's pos()/nil-means-absent contract).
Trusted bool
Reason string // when !Trusted, why (short, human-readable)
}
NetArea is one host element's gross→net elevational reconciliation.
type Plane ¶ added in v0.2.0
Plane is a cutting plane: a point on it plus an orthonormal basis. U and V span the plane and define the 2D coordinates of emitted rings; N is the normal.
The basis MUST be orthonormal and RIGHT-HANDED (N == U x V). This is load-bearing, not decorative: ring winding, hole classification via nestEvenOdd, and representativePoint's interior nudge all assume it. A left-handed basis silently swaps outer rings and holes rather than failing, so SectionOn and FootprintOn validate it and emit no rings when it is wrong. Prefer PlaneFromNormal over hand-building a basis.
func ElevationPlane ¶ added in v0.7.0
ElevationPlane returns the vertical plane an elevation along dir is drawn on: V is world up, U is horizontal, and N is dir normalized. dir points from the building TOWARD the viewer, matching Plane.N and the convention SilhouetteOn reads.
This exists rather than PlaneFromNormal because a drawing has an up. The basis PlaneFromNormal derives is deterministic but its in-plane orientation is explicitly UNSPECIFIED and free to change between versions of this package, which is fine for measuring an area and useless for a facade elevation, where it would rotate the page by an arbitrary amount.
ok is false for a non-finite dir, or one with no horizontal component: a view straight up or down is a plan, and leaves no world direction to call up on the page.
func HorizontalPlane ¶ added in v0.2.0
HorizontalPlane returns the z = cutZ plane with U=+X, V=+Y, N=+Z — the plane Footprint cuts.
func PlaneFromNormal ¶ added in v0.2.0
PlaneFromNormal derives a right-handed orthonormal basis for the plane through origin with normal n. U is seeded from the world axis least parallel to n, so the choice is deterministic and never degenerate. Returns ok=false for a non-finite origin, or an n that is non-finite, shorter than 1e-12, or large enough that normalizing it overflows.
ok=true guarantees Valid reports true for the returned plane, so a caller that checks ok need not check Valid as well.
The resulting U (and therefore V) is deterministic for a given n but otherwise UNSPECIFIED: it is NOT guaranteed to match HorizontalPlane's frame for a +Z normal, or any other particular in-plane orientation. It may also change between versions of this package. A caller that needs a specific U/V orientation (e.g. to match HorizontalPlane, or an external convention) must build the Plane itself rather than rely on this function's choice.
func (Plane) Valid ¶ added in v0.2.0
Valid reports whether p's basis is finite, orthonormal to planeBasisEps, and right-handed (N == U x V). Everything downstream of the projection assumes all three. SectionOn and FootprintOn return no rings when a plane fails this check, so a caller that hand-builds a Plane can call Valid up front to tell a bad basis apart from a plane that genuinely missed the mesh.
type Scene ¶
Scene is the assembled proxy geometry for a whole IFC model: one Element per source element, plus per-element warnings gathered during the build.
func Build ¶
Build assembles proxy geometry for every element in r, rendering ALL elements regardless of Emit.
func (*Scene) DerivedQuantities ¶
func (s *Scene) DerivedQuantities() map[string]model.Quantities
DerivedQuantities computes tier-2 (geometry-derived, GROSS) quantities per GlobalID from the tessellated proxy meshes, for elements the semantic Qto tier could not fill. Values are meters / m² / m³ in the world frame (Z-up):
Height = world-AABB vertical (Z) extent Length = larger horizontal world-AABB extent Width = smaller horizontal world-AABB extent Area = horizontal world-AABB footprint (Length × Width) Volume = closed-mesh gross volume (omitted when the mesh is not a closed shell)
These are GROSS bounding estimates: openings are not subtracted (v1 walls are solid), and a rotated element's world AABB over-reports its plan dimensions. The model's quantity_source tag marks them "geometry" (see Result.ApplyDerivedQuantities) so downstream never mistakes a bounding estimate for an authored, net Qto value. Elements with no mesh are omitted (they stay quantity_source="none" — never a fabricated 0.0).
func (*Scene) ElevationOn ¶ added in v0.7.0
ElevationOn projects the scene onto p and returns the elevation drawn from the +p.N side. Mirrors NetAreas: it needs f and r for the same reason, since an element's openings live in IfcRelVoidsElement rather than in its mesh. Scene.Elevations is the entry point for a building's several facades: this one classifies the scene on every call, and that classification does not depend on the plane.
Deterministic: identical input yields an identical view.
An element whose outline the boundary walk cannot close is OMITTED rather than approximated — see Element.SilhouetteOn. The same element is reported untrusted by NetAreas, so the drawing and the quantity agree about what neither could measure.
func (*Scene) Elevations ¶ added in v0.7.0
Elevations projects the scene onto each of planes and returns one view per plane, in the same order. Identical to calling Scene.ElevationOn for each, except that the scene is classified AT MOST ONCE, on the first valid plane, and shared across the rest: BuildFacings rasterizes an occupancy grid over every element per distinct mid-height band, and its result does not depend on the plane. A building has four facades, so asking one at a time repeats that work four times over.
An invalid plane yields its zero view in that position rather than being dropped, so the result stays index-aligned with planes, and — matching Scene.ElevationOn — never triggers BuildFacings on its own.
Deterministic: identical input yields identical views.
func (*Scene) ElevationsWith ¶ added in v0.9.0
func (s *Scene) ElevationsWith(f *step.File, r *model.Result, planes []Plane, facings map[string]Facing) []ElevationView
ElevationsWith is Scene.Elevations over a classification the caller already holds, so the two never pay for it twice.
BuildFacings dominates the cost of drawing a set of elevations — on a ~1,900-element model it is roughly 15s of a 17s call — and its result is worth more than the drawing alone: FaceArea binned by Azimuth is the sound way to total a facade, and summing the sheets is still not, even now that a host lands on one sheet rather than two (#28). A sheet outline is a PROJECTION — foreshortened by the cosine between the host's normal and the sheet — while FaceArea is the face itself; and a host at a true 45 degrees is deliberately drawn on both its sheets, so per-sheet totals would count it twice. A caller wanting both the drawings and the quantities would otherwise classify the same scene twice.
facings must have been built from THIS scene's elements — pass BuildFacings(s.Elements) to get exactly what Scene.Elevations computes. An element absent from the map is unclassified, and a nil or empty map is taken at face value rather than as a request to build one: silently doing the 15s build here would defeat the only reason to call this instead of Scene.Elevations.
Deterministic: identical input yields identical views.
func (*Scene) NetAreas ¶
NetAreas returns per-host (keyed by GlobalID) reconciliation for every element that has IfcRelVoidsElement openings. Hosts with NO voids are ABSENT from the map. For each voided host it measures the gross silhouette on the host's winning projection axis, then deducts the UNION of the openings' footprints measured on that SAME axis (so wall and openings share a plane and bias cancels). Net is emitted only when every opening passes every trust gate (see below); an untrusted host carries gross + a reason and an absent (nil) Net.
Gross and the deduction are BOTH unions from the same engine on the same plane, so Net is the exact net area of that projection — and equals the area of the host's Element.SilhouetteOn outline minus its openings, which is what makes the drawing and the number the same fact.
Openings that overlap in the host plane are handled, not refused: the deduction is a union, so each covered square metre is deducted exactly once however many voids claim it.
EXACTNESS ENVELOPE: an opening's footprint is its SILHOUETTE on the host's winning plane — every triangle of the void's solid projected there and unioned — so an arch, an L-shaped void or any other non-rectangular profile is measured at its true projected area rather than at a bounding box around it. Gross is the host's silhouette on that same plane, so gross and deduction share one projection AND one measure, and Net is the exact net area OF THAT PROJECTION.
What that does NOT mean: for a host whose face is tilted relative to the winning axis, the projection foreshortens gross and deduction by the same factor, so Net stays internally consistent but under-states the true 3D face area. Consumers needing the on-face area of a tilted host must correct for the obliquity themselves.
The only quantum in the path is the 1e-5 m endpoint weld the union boundary inherits from the ring stitcher; the coverage classification itself uses exact predicates and no tolerance.
SIDE EFFECT: this appends the aggregated orphan-fill warning to s.Warnings. Intended to be called ONCE per Scene — calling it repeatedly duplicates that warning and re-does the work.
INVARIANT (load-bearing): the host mesh in s.Elements must contain NO opening (IfcRelVoidsElement) geometry — gross is then the SOLID elevational area and OpeningDeduction is the ONLY netting applied. This holds today because clip.go's clipMeshByDifference subtracts ONLY an IfcHalfSpaceSolid second operand (plane/half-space cuts for roof-lines and miter joins); it never bakes a solid void into the host mesh. If clipMeshByDifference is ever extended to subtract a SOLID second operand, a voided host would be netted TWICE — once in its mesh, once here (double-subtraction). The un-voided invariant is guarded by TestNetAreas_RectWindow, which asserts Gross == the full solid wall area.
func (*Scene) WriteGLB ¶
WriteGLB emits one binary glTF. A single root node applies a Z-up->Y-up rotation (Rx(-90 deg)); every element is a child node named by its GlobalId, with node.Matrix = element Placement (world, meters, Z-up) and a mesh of the element-local verts/tris. All meshes share one binary buffer.