Skip to content

POC feat(): write an IFC conversion as several models (splits) - #337

Draft
ShaMan123 wants to merge 23 commits into
ThatOpen:mainfrom
ShaMan123:poc/ifc-split-output
Draft

ShaMan123 wants to merge 23 commits into
ThatOpen:mainfrom
ShaMan123:poc/ifc-split-output

Conversation

@ShaMan123

@ShaMan123 ShaMan123 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

/private/tmp/claude-501/-Users-shaman123-DEV-engine-fragment/f6e4263e-da2d-4e66-ae01-e48ccaf947b7/scratchpad/pr-splits.md

@ShaMan123 ShaMan123 mentioned this pull request Oct 12, 2026
2 tasks done
ShaMan123 and others added 23 commits October 12, 2026 08:29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…layer

- IfcByteSource: sync random access over bytes or a Blob (FileReaderSync
  with an LRU page cache); feeds web-ifc's load callback and the resolver.
- IfcEntityResolver reads through any IfcByteSource, captures the header,
  and gains an uncached parse() for sweeps.
- IfcResolverLineApi: the tape-reading subset of IfcAPI over the resolver,
  pinned against IfcAPI line by line on every fixture.
- ProcessData.file/source: web-ifc opens through the reader; the property
  pass reads through the resolver instead of a second web-ifc instance.
- Geometry is written to the builder as it arrives instead of held as JS
  arrays until the end.
- Fix embind handle leaks (mesh.geometries, id vectors, swept-disk probe),
  an unused web-ifc re-Init after Dispose, quadratic unshift loops and an
  indexOf-per-child spatial walk.
- Finer progress (within a category) and explicit phases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extraction (web-ifc meshes, hashing, shell building) depends on one element
alone; assembly (dedup of geometry and local transforms, id allocation)
replays the single-pass decisions in order. Shells travel as typed arrays.
The grid reader reads through IfcLineApi with the coordination matrix passed
in, so it no longer needs an open web-ifc model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IfcProjector writes, for a batch of elements, a standalone IFC holding only
what their geometry reads: down-references plus the four reverse edges
web-ifc's IfcCache follows (voids, inherited through aggregation; styled
items; material associations, cut down to the batch; material definitions)
and the project's units. Each batch opens in a web-ifc of its own, in this
thread or in workers (serveIfcGeometryWorker), and is assembled in order.

The origin is reproduced exactly: a probe batch reports which element set
it, and every later batch streams that element first. Pinning the matrix
with SetGeometryTransformation instead rounds a few transforms differently.

The extractor skips building geometry the assembler already holds, which
the split had lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IfcStatementScanner's byte loop becomes IfcStatementCursor, which reports
statements positionally, interns type names by a hash taken while
scanning, skips through statement bodies, and keeps its state in locals.
IfcEntityResolver.fromSource indexes through it directly: ~50 MB/s through
the stream becomes ~250 MB/s.

Also: parallel mode in the StreamingImport demo, and batches sized so every
executor gets several.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the .frag dump/compare out of the parity CLI into src/testing so
tests can use it, and adds replicate.ts, which builds large synthetic IFC
files from a real one for testing past the sizes real fixtures reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The property pass splits into prepare (every entity's attributes and every
relation, keyed by id) and finish (the layout, items with geometry first).
With geometry in workers, prepare runs on the coordinating thread
meanwhile, yielding every few milliseconds so batch results keep flowing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- IfcChunkedBytesSource holds a file as several buffers, so files past the
  2 GB a single Blob read allows can still be read into memory.
- IfcBlobSource pages default to 4 MB (each FileReaderSync call is a round
  trip to the browser process) with a 128 MB cache.
- IfcLineIndexBuilder takes a size hint, grows by 1.5x, and finalizes
  without copying when its arrays are mostly full.
- Demo: largest web-ifc heap across workers, elapsed time while converting,
  page and resident-budget parameters, bench screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Relations are an edge list in typed columns, grouped by a stable radix
  sort when read, instead of an object and arrays per entity.
- Items with geometry are typed columns filled on arrival, with material
  indices assigned then, instead of an object graph kept to the end.
- The shell dedup key is digested to 64 bits: it crosses from workers and
  is held once per geometry.
- The projected reader's processing order is two typed arrays.
- Bench: sampled live heap of the import worker (--heap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README: how the pipeline works, what changed in the library, parity
  evidence, measurements against the previous importer on the test models
  and a 2.4 GB synthetic file, the workers/memory trade-off, and limits.
- IfcGeometryExtractor and its per-geometry methods take destructured
  objects.
- Bench reports the geometry workers' largest web-ifc heap.
- Examples index lists the StreamingImport demo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…progress

Batches of consecutive elements were cut by count, and exporters write
costly elements next to each other: one batch then held a run of CSG-heavy
members, or 150 instances of a 22k-face mapped brep, and bounded the whole
conversion. The projector now estimates each element's meshing cost as it
walks its closure (booleans, openings, geometry bytes, and a mapped
representation again per instance, since web-ifc re-meshes every instance),
batches are cut at a cost worth targetBatchMs, and finished batches
calibrate how many ms a unit of cost is. Slowest models: 18.7 -> 11.7 s,
6.7 -> 4.6 s, 10.9 -> 7.9 s; no change on the rest.

Also: batch durations in IfcImporter.stats, and the demo shows progress while
loading into the viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Estimating a representation map's cost with a walk of its own re-read much
of an instanced file, and planning a paged 2.4 GB file went from 31 s to
75 s. The map is now measured as part of the batch's walk, and the walk
keeps index positions on its stack, looking each statement up once.

README: how batches are planned and why not cluster, the corrected account
of what batching duplicates (web-ifc meshes shared geometry per element in
any mode), the output cap, and results at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heap claim

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One model in one file gives the viewer nothing to draw until all of it is
loaded, in one worker, properties included. `splits` writes a conversion as
several models instead, as cutting the IFC up with the splitter does, but
from one conversion: one origin, one set of local ids.

- Geometry splits hold the meshes and the local id and category of the item
  each belongs to, which is all drawing, picking and hiding read.
- The reference split holds the grids and alignments, which the viewer
  draws from their attributes.
- The data split holds everything else, for every item, elements included:
  attributes, relations, GUIDs, the spatial structure and the metadata.
  Property sets alone are 2-7% of a model; most of what rendering never
  reads belongs to the elements themselves.
- `process` resolves to a manifest that lists the splits and says, for each
  local id, which split has its geometry and which its attributes
  (`findItemSplits`).

Splits are planned with the batches: elements are counted into the current
split and the batch that would overfill it is cut short, so a split is a
run of consecutive batches. A split is also ended when its model passes
`maxBytes`. Each is assembled on its own, into a builder of its own, and
handed to `onSplit` as soon as its last batch is assembled; `onSplit` is
awaited, and assembly with it. The config follows the one discussed in
ThatOpen#180, without a `splitBy` for now.

Writing a model's meshes table moves out of IfcGeometryProcessor into
MeshesWriter, one per model written. Without `splits` the output is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Splits are put back together (`dumpSplits`) and compared with the single
model: on every fixture, cut by count and by size, with the manifest
checked against what each split holds and every split loaded in the
viewer's model.

Shells are compared as shapes rather than as lists of numbers. Geometry is
deduplicated by a key made of a mesh's rounded vertices and its size, not
its triangles, so one model stores two meshes with the same key once; when
a split boundary falls between them, each is stored as itself: points that
differ by what the rounding hides, sometimes in another order, or with a
flat face cut into loops another way. `compare` counts those when given a
tolerance and still fails on anything else. Grids and alignments, whose
local ids the importer makes up, can be compared by what they hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo writes the model as splits when "Several models" is ticked, and
loads each as it arrives: geometry into the scene, grids and alignments
drawn from the reference split, the data split into a thread group of its
own. Clicking an element reads its data from the data split, found through
the manifest. It reports when the first geometry is on screen and when
everything is loaded, and bench.mjs passes both on, with an item read back
from the data split.

convert.ts writes splits and a manifest with SPLIT_ITEMS, and parity.ts
takes a manifest in place of a .frag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a converted model is made of, what splits buy when loading (1.3-4.6x
faster viewer load, first geometry on screen in a sixth to a half of the
time), what they cost, how they compare with the single model on four real
models, and what is left: data is one split, and nothing in the viewer
loads a manifest yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ShaMan123
ShaMan123 force-pushed the poc/ifc-split-output branch from 519f262 to 570e24d Compare October 12, 2026 06:45

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant