/** * In-browser IFC loader — parses an `.ifc` with web-ifc (WASM) and produces, * IN MEMORY, exactly the artifacts the offline converter writes to disk: a * glTF binary carrying per-element feature IDs, a 3D Tiles tileset pointing at * it, a property table, and a crease/boundary outline overlay. * * A LAZY chunk, on the same terms as `cityjson.ts`: nothing here is reachable * from the main bundle except through `loadIfc`'s dynamic import in * `index.ts`, and web-ifc's ~2 MB WASM is fetched only when a page actually * loads an IFC. * * This is a LOADER, not a converter — nothing is written out. Everything comes * back as blob URLs, so `pick-features`, `feature-styles`, the declarative * isolate/hide/ghost attributes and a PathLayer outline overlay all work * unchanged: they cannot tell an uploaded model from a prepared tileset. * * Why blob URLs rather than feeding geometry to a layer directly: the whole * 3D-Tiles path — tile traversal, `FeatureMeshLayer`'s per-primitive fan-out, * picking, the style textures — already consumes a tileset URL. Handing it one * reuses every bit of that instead of growing a second rendering path. * * web-ifc is MPL-2.0 and is NOT a package dependency. It is fetched from a CDN * by default; `configureIfc({wasmPath})` points it at a self-hosted copy. */ /** Point the loader at a self-hosted web-ifc build instead of the default CDN. */ export declare function configureIfc(options: { wasmPath?: string; }): void; /** One row of the property table — one IFC element. */ export interface IfcFeature { ifcClass: string; name: string; material: string; container: string; /** * The element's full spatial ancestry, outermost first, joined by * `SPATIAL_SEPARATOR` — e.g. `Default site / Building / 00 groundfloor`. * * `container` is only the element's IMMEDIATE spatial parent, which is a * flat list, not a hierarchy. This is what a model tree navigates. Empty * when the model has no spatial decomposition to walk. */ spatialPath: string; /** `IfcDoorType > Single-Flush 0915x2134` — the element's type object. */ typePath: string; /** The system, zone or group the element is assigned to, including parent systems. */ systemPath: string; /** `CCS > Ss > Ss_25_10_30` — the classification reference chain, walked via `ReferencedSource`. */ classificationPath: string; netVolume: number; } /** * Separator inside `spatialPath`. U+001F (UNIT SEPARATOR) exists for exactly * this and cannot occur in an IFC label, so splitting is unambiguous — a * readable delimiter like " / " would break on any storey actually named * "Level 1 / Mezzanine". */ export declare const SPATIAL_SEPARATOR = "\u001F"; export interface IfcModel { /** Blob URL of a 3D Tiles tileset — assign to a `Tile3DLayer`'s `tileset`. */ tilesetUrl: string; /** Blob URL of PathLayer rows tracing crease/boundary edges, or null when disabled. */ edgesUrl: string | null; /** The property table, indexed BY feature ID — the same rows picking resolves. */ features: IfcFeature[]; /** * The `loadOptions` a `Tile3DLayer` needs for this model — assign alongside * `tilesetUrl`. * * `isTileset: true` is REQUIRED, not a tuning knob. Tiles3DLoader decides * tileset-vs-tile with `context.url.indexOf('.json') !== -1`, and * `Response.url` strips the fragment, so a blob URL never satisfies it and * the tileset JSON gets parsed as a binary tile * ("3DTileLoader: unknown type {\"as"). Forcing it is safe: loaders.gl * overrides the flag per tile from the tile's own type * (`isTileset: this.type === 'json'`), so tile CONTENT still parses normally. */ loadOptions: Record; /** * Where the model says it is: its own IfcSite position when it declares one. * * Feed this to the layer's `site-origin` rather than baking it in — see * `readGeoreference` for why a declared position is so often an authoring * tool's default rather than a survey. */ lonLat: [number, number]; /** True when `lonLat` came from the model rather than the fallback. */ georeferenced: boolean; /** * The file's declared `IfcMapConversion.OrthogonalHeight`, RAW — undefined * when the file never states one (the common case; distinct from a * declared 0). No geoid/vertical-datum conversion is applied: absolute * elevation only means something once the layer sits on a basemap that * also has real elevation (`terrain`), and no correction turns a flat * Z=0 basemap into one — feed this to `site-origin`'s elevation component * only when terrain is active. */ orthogonalHeight?: number; /** Bearing for the layer's `site-heading`, in degrees clockwise from true north. */ heading: number; /** Multiplier for the layer's `site-scale`; 1 unless IfcMapConversion says otherwise. */ scale: number; /** Whether `heading` was read from the file or merely assumed — never conflate the two. */ headingSource: HeadingSource; /** Which of the file's georeferencing routes supplied `lonLat` — a survey-grade transform, a declared site, or neither. */ originSource: OriginSource; stats: { elements: number; primitives: number; polylines: number; bytes: number; }; /** Milliseconds per phase — for finding where a slow load actually goes. */ timings: Record; /** Tile-space bounds (Z-up metres) and the offset applied — for diagnosing placement. */ bounds: { lo: [number, number, number]; hi: [number, number, number]; centre: [number, number, number]; }; /** * The model-space origin subtracted from every vertex, in the FILE's own * coordinates. Feed it back as `LoadIfcOptions.origin` when loading another * discipline model of the same building so the two share a frame. */ origin: [number, number, number]; /** Frees the blob URLs. Call when swapping models — they are not garbage collected. */ revoke(): void; } export interface LoadIfcOptions { onProgress?(message: string): void; /** Used when the model declares no position of its own. */ fallbackLonLat?: [number, number]; /** Trace crease/boundary outlines (default true). Expensive on very large models. */ edges?: boolean; /** Minimum angle between adjacent faces for a crease edge, in degrees. */ creaseDegrees?: number; /** * Shared model-space origin, in the file's own coordinates (tile axes: * east, north, up). Pass the `origin` a previous `loadIfc` returned to keep * two discipline models CO-REGISTERED — without it each model is centred on * its own bounding box and they drift apart by the difference, which is * precisely what makes a clash pass return nothing. */ origin?: [number, number, number]; } /** Minimal shape of the bits of web-ifc's API this module touches. */ interface IfcApiLike { SetWasmPath(path: string, absolute?: boolean): void; Init(): Promise; OpenModel(data: Uint8Array, settings?: Record): number; CloseModel(modelID: number): void; GetLine(modelID: number, expressID: number, flatten?: boolean): Record; GetLineType(modelID: number, expressID: number): number; GetNameFromTypeCode(type: number): string; GetLineIDsWithType(modelID: number, type: number): { size(): number; get(i: number): number; }; LoadAllGeometry(modelID: number): { size(): number; get(i: number): StreamedMesh; }; GetGeometry(modelID: number, id: number): IfcGeometryLike; GetVertexArray(ptr: number, size: number): Float32Array; GetIndexArray(ptr: number, size: number): Uint32Array; } interface StreamedMesh { expressID: number; geometries: { size(): number; get(i: number): { geometryExpressID: number; color: { x: number; y: number; z: number; w: number; }; flatTransformation: ArrayLike; }; }; } interface IfcGeometryLike { GetVertexData(): number; GetVertexDataSize(): number; GetIndexData(): number; GetIndexDataSize(): number; delete(): void; } /** The pinned hash a URL must satisfy, or null when it is not the default CDN's (self-hosted → deliberately unverified, see `PINNED_SHA256`). */ export declare function pinnedWebIfcSha256(url: string): string | null; /** * `IfcCompoundPlaneAngleMeasure` -> decimal degrees. * * IFC2x3 predates `IfcMapConversion`, so a site's position lives on * `IfcSite.RefLatitude`/`RefLongitude` as (degrees, minutes, seconds, * millionths-of-a-second). The sign rides on the leading non-zero component * and applies to the whole measure — reading only the degrees puts a western * longitude in the wrong place by several arc-minutes. */ export declare function compoundAngleToDegrees(parts: unknown): number | null; export type HeadingSource = "map-conversion" | "true-north" | "assumed"; /** * The affine an `IfcMapConversion` (or its IFC2x3 / 4.3 equivalents) defines, * kept so it can be applied to a REAL MODEL POINT rather than collapsed into a * single coordinate. * * This is the difference between georeferencing a model and merely putting it * near the right place. The conversion anchors the model's ORIGIN — (0,0,0) in * its own coordinates — but a tileset is recentred on the geometry, and those * are not the same point. MiniBIM-3.1's origin sits 32.7 m from its own * centre, so un-projecting the eastings alone and dropping the model there * lands it 32.7 m out with a perfectly plausible-looking result. */ export interface MapConversionAffine { /** EPSG code of the target projected CRS, or null when it could not be read. */ code: number | null; eastings: number; northings: number; orthogonalHeight: number; /** Model +X in the map grid, as (cos, sin). */ xAxisAbscissa: number; xAxisOrdinate: number; scale: number; } /** * Model coordinates -> projected map coordinates. * * The spec's affine, applied in full: rotate by the grid axis, scale, then * offset by the eastings/northings. Straight out of IfcMapConversion's own * definition rather than an approximation of it. */ export declare function mapConversionApply(a: MapConversionAffine, x: number, y: number): [number, number]; /** * Assembles the affine a parsed `IfcMapConversion` line defines, from its * already-unwrapped numeric attributes. Pulled out from `readGeoreference` so * the one rule that matters is independently testable without booting web-ifc. * * `scale` is fixed at 1 — deliberately NOT the file's own `Scale` attribute. * Per buildingSMART's definition, `IfcMapConversion.Scale` exists to convert * a file's OWN declared length unit into the target CRS's when they differ * ("used when the length unit... [is] not identical with the length unit * established for this project... e.g. to convert feet into metres") — but * web-ifc already returns every vertex normalised to metres regardless of * what the file declares, so that conversion has already happened before any * coordinate reaches this function. Taking the file's Scale at face value * here — or worse, as a render-time `site-scale` — would apply it a second * time: `geographic-referencing-utm.ifc` declares millimetres with * Scale=1000 (mm -> m), and doing so rendered the whole model 1000x too * large, a building-sized object turned into a kilometre-wide monument. */ export declare function buildMapConversionAffine(params: { epsg: number | null; eastings: number; northings: number; orthogonalHeight: number; xAxisAbscissa: number; xAxisOrdinate: number; }): MapConversionAffine; /** * `OrthogonalHeight` corrected for the tileset's own recentring — the * vertical counterpart of un-projecting `mapConversionApply` AT `centre` * rather than at the file's raw origin (see that comment at the call site * below). The tileset's baked origin sits at the model's LOWEST vertex (or, * federated, whatever point the first model shared), not at the file's own * local Z=0 — which is what `OrthogonalHeight` is declared relative to. * `recentredLocalHeight` is `centre[2]`: the model's ORIGINAL, pre-recentre * local height at the point that becomes the tile origin. The vertex now * AT that origin was at that local height before recentring, so its * absolute elevation is `orthogonalHeight + recentredLocalHeight`, not * `orthogonalHeight` alone — exactly as the 32.7 m horizontal precedent * this mirrors (see `applying an IfcMapConversion` tests below) needed * `centre[0]`/`centre[1]` folded into the un-projection, not the model's * raw origin. */ export declare function correctOrthogonalHeightForRecentring(orthogonalHeight: number, recentredLocalHeight: number): number; /** Where `lonLat` came from — a survey-grade transform, a declared site, or nothing at all. */ export type OriginSource = "map-conversion" | "ifc-site" | "fallback"; /** * UTM easting/northing -> lon/lat on WGS84 (the standard inverse series). * * Needed because `IfcMapConversion` states position in a PROJECTED CRS, so a * properly georeferenced export — the case that actually matters on real * projects — cannot be placed without un-projecting it. UTM covers the * overwhelming majority of what `IfcProjectedCRS` names in practice * (EPSG:326xx north / 327xx south); anything else is declined rather than * approximated, because a wrong projection lands the model in a different * country while looking perfectly plausible. * * Verified against a known control point (Sydney Opera House, zone 56S) to * within 0.1 m. */ export declare function utmToLonLat(easting: number, northing: number, zone: number, south: boolean): [number, number]; /** `EPSG:32633` -> `{zone: 33, south: false}`. Anything that is not a WGS84 UTM zone returns null. */ export declare function parseUtmCrs(name: string): { zone: number; south: boolean; } | null; /** * Grid convergence at a point in a UTM zone: the bearing of GRID north * clockwise from TRUE north, in degrees — γ = atan(tan(λ−λ₀)·sinφ), the * standard spherical approximation (well under the ~1e-4° heading rounding in * `readGeoreference` anywhere inside a zone's extent). * * The analytic counterpart of crs.ts's numerically-derived `gridConvergence`, * with the SAME sign convention (that function unprojects a step of grid * north and takes its true bearing; verified against it in ifc.test.ts). It * exists for the same reason too: a model aligned to a UTM GRID is not * aligned to true north, and `site-heading` is a true bearing. UTM zones are * resolved analytically precisely so ANY zone works without proj4 or a * bundled def — so the correction has to be analytic as well, or an * EPSG:326xx/327xx file sits skewed (up to ~3° near a zone edge at high * latitude) while an EPSG:25832 file, whose convergence rides the proj4 * branch, does not. */ export declare function utmGridConvergence(lonDeg: number, latDeg: number, zone: number): number; /** * Un-projects an `IfcMapConversion` affine AT a real model point — the * tileset's own anchor, `centre` — rather than at the raw origin the * conversion names. Pulled out of `loadIfc` so the routing rule is * independently testable without booting web-ifc. * * The routing mirrors `readGeoreference` exactly, and that symmetry IS the * fix: WGS84 UTM codes go through the analytic `utmToLonLat` (any of the 120 * zones, either hemisphere, no proj4), everything else through the bundled * CRS table. An earlier version sent every code to `projectedToLonLat`, * which returns null for the UTM zones outside the bundled defs (e.g. * EPSG:32656) — so exactly the files whose position had been resolved * analytically had their re-anchor silently no-op, keeping the raw-origin * position and reintroducing the ~32.7 m offset class this correction exists * to remove. * * Returns null when neither route can un-project the code; the caller keeps * its current position but must stop claiming survey-grade placement for it. */ export declare function reanchorMapConversion(affine: MapConversionAffine, centreEast: number, centreNorth: number): Promise<[number, number] | null>; /** * Where the model says it is, which way it says north is, and at what scale. * * Three independent facts from three different places, every one optional: * * - POSITION — `IfcSite.RefLatitude`/`RefLongitude`. Read it, but do not trust * it: an authoring tool's DEFAULT project location is indistinguishable from * a surveyed one. Both prepared samples in this repo carry defaults (the * clinic sits on Revit's Boston, the duplex on a Chicago city centre point), * so the models land on occupied downtown blocks at an arbitrary rotation. * That is the whole reason `site-origin` exists as an override. * - HEADING — `IfcMapConversion.XAxisAbscissa`/`XAxisOrdinate` on a properly * georeferenced export, otherwise `IfcGeometricRepresentationContext. * TrueNorth`. Most IFC2x3 files carry neither, and then project north is * ASSUMED to be true north. `headingSource` says which of the three * happened, because "read from the file" and "assumed because the file was * silent" should never look the same to a caller. * - SCALE — `IfcMapConversion.Scale`. * * `IfcMapConversion.Eastings`/`Northings` are deliberately NOT used for * position. They are in a projected CRS identified only by name on * `IfcProjectedCRS`, and guessing a projection for it would put the model * somewhere confidently wrong instead of admittedly approximate. * * Exported for tests: `api` is structurally typed, so a hand-rolled fake * exercises the reading rules (convergence, unit handling, precedence) * without booting web-ifc's WASM. */ export declare function readGeoreference(api: IfcApiLike, modelID: number, fallbackLonLat: [number, number]): Promise<{ lonLat: [number, number]; georeferenced: boolean; heading: number; scale: number; headingSource: HeadingSource; originSource: OriginSource; affine: MapConversionAffine | null; /** True only when the file itself states `OrthogonalHeight` — absent defaults to 0 the same as a declared 0 would, so this is the one signal that tells the two apart. */ hasOrthogonalHeight: boolean; }>; /** * Below this element count, tiling overhead isn't worth it: the whole model * keeps today's plain single-tile treatment, verbatim (see `useGrid` in * `loadIfc`). Existing small fixtures (the clinic, duplex, bridge) stay on * this path and produce byte-identical output to before spatial tiling * existed at all — the clinic alone has 2,626 elements, so this has to clear * that with real headroom, not just be "a small number". See * `ifc.test.ts`'s "spatial tiling threshold" describe block, which asserts * this against the clinic's real committed element count directly — change * this constant without re-checking that and it is exactly the mistake that * test exists to catch. */ export declare const GRID_TILING_THRESHOLD = 5000; /** * A feature's cell, keyed off its FIRST placed geometry's own placement * origin (the matrix translation column) rather than a true centroid — cheap * (no vertex scan needed before bucketing starts) and adequate: it is only a * bucketing heuristic, not a visual clip, since each cell's own 3D-Tiles * bounding box is always computed from the vertices actually inside it. */ export declare function gridCellKey(east: number, north: number): string; /** * Parses IFC bytes into a renderable 3D Tiles model held entirely in memory. * * Assign `tilesetUrl` to a `Tile3DLayer`'s `tileset` and `edgesUrl` to a * companion `PathLayer`'s `data`; `features` is the same property table * picking resolves, for building legends and category lists. */ export declare function loadIfc(source: ArrayBuffer | Uint8Array | Blob, options?: LoadIfcOptions): Promise; export {};