Skip to content

⚠ DO NOT REMOVE

Scope: the basic shapes of the Duplex rebuilt from its 2D set (walls with their openings cut, floor slabs, roof, door/window fills from the LOD400 component library), checked back against the source model. Separate from SPEC.md (another agent appends there; merge later). Spec before code. Read the log after every run; exit code is not evidence. Every value traces to a DXF entity; the source DB is the answer key only; a value the drawings do not carry is a GAP, never a default.

DXF2BIM basic shapes: walls, floor slabs, roof (with openings and fills)

Status: SPEC 2026-10-16 (written before code). Goal (user): "make this a prelim of the basic shapes which has to be accurate first as fundamental of most 2Ds.. ie floor slabs, walls, roof", then "with their openings.. mesh mapping from their LOD400 component library". Full circle: drawings exported from a model we hold, rebuilt, checked against that model. Windows/doors as TYPES are out of scope here except where a bound type is needed to place a fill.

1. Inputs and answer key

  • Drawings: bim-compiler/2D_Layout/output/DXF/DX_{FLOOR_GF,FLOOR_FF,FRONT,REAR,LEFT,RIGHT,ROOF}_20260428_0636.dxf (units mm). Marked copies from tests/dxf2bim/mark_dx_set.js (out/marked/), plus the shell markings of §5.3 written by the new rosetta script into out/marked_shell/ (copies; originals untouched).
  • Library (read-only): bim-compiler/deploy/component_library.db (ad_opening_family, ad_geometry_map, component_geometries).
  • Answer key only: 2D_Layout/input/DX_extracted.db (elements_rtree bboxes, base_geometries meshes + element_transforms centres, rel_fills_host). Never read by any module; read only by the witness script.
  • Code: new module deploy/dev/dxf2shell.js (pure). Minimal edits: dxf2bim.js (§2.1, §2.2), dxf2mesh.js (§2.1 mesh of a polygon wall), dxf2marks.js (one key, §5.3). Witness tests/dxf2bim/rosetta_dx_shell.js (log rosetta_dx_shell.log), unit tests tests/dxf2bim/test_shell.js (log test_shell.log). OBJ tests/dxf2bim/out/duplex_basic_shapes.obj.

1.1 Evidence measured before writing (scripts in the session scratchpad, 2026-10-16)

  • Plans have NO slab, stair, railing or void layer: FLOOR_GF/FLOOR_FF layers are A-WALL-FULL/PRTN/PATT, A-DOOR, A-GLAZ, A-FURN, A-GRID, annotation, title block.
  • Level 2 has 8 closed A-WALL-FULL outlines with 45° mitred corners (handles 68, 6E, 78, 81, 8B, 94, 10A, 110): 4 trapezoid-like long pieces and 4 corner triangles (e.g. 110 = (0,0),(417,0),(417,-417)). Source walls 1010-1013 are the 4 Level-2 exterior walls they belong to.
  • Level-2 wall 1035 misses 20 mm: piece C2 (2506..2526 x -10094..-9970 mm) is a pier 20 mm along the wall and 124 mm across; wall.min_thickness 0.05 drops it on its SHORT side before stub orientation (§46 of SPEC.md) can turn it.
  • R1b sums source opening widths; Level-2 windows are stacked three per plan box (e.g. 81/82/83 share 0.417..1.236), so the sum triple-counts one plan interval.
  • Source structural slabs: Level 1 127mm Slab on Grade 1103/1104, z -0.127..0, two L-shaped outlines inside the exterior walls (x 0.417..8.383, y -17.383..-0.417) separated by the party-wall zigzag; Level 2 Wood Joist with Subflooring 1105/1106, z 2.795..3.1, x 0.417..4.125 and 4.675..8.383 (split by party wall 990) with stair notches 0.417..1.471 x -9.725..-6.234 and 7.359..8.383 x -11.566..-8.075; exterior porch slabs 1122/1123 outside the building; 12 thin Finish Floor slabs (13-19 mm). Roof: IfcSlab 1121 Basic Roof:Live Roof over Wood Joist Flat Roof, storey Unknown, z 6.0..6.457, outline x 0.417..8.383, y -17.383..-0.417 with two holes 0.861..1.891 x -11.206..-10.124 and 6.894..7.924 x -7.676..-6.594. Skylights 98/99 (IfcWindow, storey Roof) bbox 0.79..1.962 x -11.277..-10.053 and 6.823..7.995 x -7.747..-6.523, z 6.457..6.635.
  • Drawing evidence for slabs/roof: ROOF plan A-ROOF closed polyline 0..8800 x -17800..0 (outer face of the building), two A-GLAZ closed rectangles equal to the skylight bboxes (790..1962 x -11277..-10053; 6823..7995 x -7747..-6523), text BUMBUNG RATA / FLAT ROOF. Elevations: A-ELEV-WALL rects split at y 0 / 3100 / 6000 / 6609, interior wall ends seen at 0..2795 (FRONT 6200..6324; LEFT -10370..-10218 and -10218..-9725; REAR 2476..2600; RIGHT -8075..-7582, -7582..-7430); level markers (A-ANNO-LEVL line + text) GRD. LEVEL -0.150, GRD. FLOOR LEVEL +0.000, SILL LEVEL +3.200, HEAD LEVEL +5.600, BEAM/CEILING LEVEL +6.000, ROOF LEVEL +6.500, dimension 500mm between 6000 and 6500. No first-floor level text, no section drawing in the set (plans carry A-ANNO-SECT markers for sections that are not in the set).
  • Library: no table links an ad_opening_family id to a geometry. ad_geometry_map holds, for building_type Ifc2x3_Duplex, per-instance EXTRACTED meshes keyed by Revit type name (element_ref, e.g. M_Fixed:4835mm x 2420mm:4835mm x 2420mm), centred, orientation baked (local bbox long side along X or along Y). Windows 56 vertices/108 triangles, doors 40/68 (glass door 56/100). Family descriptions name the type: DUPLEX_LIVING_WINDOW = "M_Fixed 4835×2420mm ...", US_DBL_DOOR_1250 = "US double door 1250x2010mm". Each size pair occurs in exactly one Duplex element_ref per family token (checked by SQL).

1.2 Tolerances (stated before any run)

Quantity Tolerance Why
plan coordinates, wall pieces vs source walls (R1a) 1 mm drawing coordinates are integer mm
wall recall R1b-u 5 mm as SPEC.md D-2
any z (wall top/base, slab top/bottom, roof, opening sill/head, fill z) 10 mm as SPEC.md R5/R6
slab/roof outline edge vs source outline extreme 1 mm integer mm
slab/roof footprint area not explained (see §4.4) 0.01 m² float rounding only
fill placement centre (x, y, z) vs source door/window bbox centre 10 mm as R5
roof hole centre vs source hole centre 10 mm
void sampling offset above head / below sill 20 mm inside the thinnest infill strip (66 mm between stacked windows)
elev_tol (interval equality in elevations) 3 mm SANDBOX, as rosetta_dx_heights

2. Walls

2.1 F9 polygon footprint (Level-2 mitres)

Supersedes SPEC.md D-1 for this goal (user 2026-10-16: Level-2 mitred walls must be rebuilt); D-1 forbade a bounding box, and F9 does not use one. - F9 config.wall.polygon_footprint: true (opt-in; absent = behaviour unchanged, GAP:wall_footprint_not_rect). In footprint mode a closed outline that is neither a rectangle (F1) nor rectilinear (F6), after dedupe and removal of collinear vertices (within snap_tol), and that is a SIMPLE polygon (no two non-adjacent edges cross) with at least 3 vertices, is emitted as ONE wall: outline = the vertices (metres, CCW), polygon: true, rect = its bounding box, axis along the bbox long side through the bbox centre (F2), thickness = bbox short side, square when equal. F4 (duplicate) and F5 (min_thickness on the bbox short side) apply. Self-crossing → GAP:wall_footprint_not_rect as before. - Polygon pieces take no part in the §46 stub pass (neither voters nor subjects). - Mesh (dxf2mesh.meshFromOps): a wall with outline is a prism of the outline from level+base to level+base+height (existing ear-clip prism). Openings hosted by a polygon wall are not cut there: GAP:opening_in_polygon_wall (the shell path of §6 handles openings in drawn gaps; nothing is silently cut). Tests (test_shell.js): WP1 mitred trapezoid (0,0),(4675,0),(4675,417),(417,417) mm → one polygon wall, outline equals input in m, rect (0,0,4.675,0.417), thickness 0.417 · proves: mitres dropped or bbox-filled · WP2 flag absent → GAP:wall_footprint_not_rect · proves: hidden behaviour change · WP3 self-crossing (bow-tie) → gap · proves: invalid outline accepted · WP4 prism volume = shoelace area x height, watertight, outward · proves: polygon mesh wrong · WP5 rectangle and rectilinear outlines unchanged (same ops as without the flag) · proves: F9 hijacks F1/F6.

2.2 F5b min_thickness after orientation

  • F5b When §46 stub orientation is active (opening.gap_max given), wall.min_thickness is checked on the thickness AFTER orientation: a piece below it on its short side is kept as a candidate (not a voter); if a vote turns it so the long side is its thickness, it is emitted; otherwise GAP:wall_below_min_thickness exactly as before. Without gap_max nothing changes (F5). Tests: WT1 pier 20 x 124 mm between a wall piece and a gap (same strip, gap 0.762 m ≤ gap_max) → emitted, thickness 0.124, horizontal · proves: real piers dropped as "thin strips" · WT2 thin strip 800 x 19 mm with no voter → still GAP:wall_below_min_thickness · proves: min_thickness disabled · WT3 thin candidate does not vote for another piece · proves: dropped strips steering orientation.

2.3 Heights (existing §28 H1-H6) and W-H3 slab-above convention

  • Elevation heights per SPEC.md §28 (unchanged). The plan @LEVEL markings (0, 3.1) were set FROM_SOURCE_MODEL by mark_dx_set.js; here each is accepted only if an elevation wall rect of the set has z0 equal to it within elev_tol (check LV, drawing-corroborated), else the run stops.
  • W-H3 (convention, labelled, own height_source): a plan wall piece left GAP:no_elevation_height by H4 takes base 0, height = U - level, height_source: 'slab_above', where U = the underside z of the slab directly above this storey (§4.2 for a floor, §5.2 roof bottom for the top storey). No U → the gap stays. Stated convention (Revit-style "top constraint = underside of floor above"), not a reading of that wall; reported separately from elevation heights. Tests: WH1 unseen piece with U given → base 0, height U-level, source slab_above · proves: convention silently used without label or with the wrong storey · WH2 no U → gap kept, wall dropped · proves: default invented · WH3 a piece with an elevation height is untouched · proves: convention overrides a drawn value.

2.4 Rosetta wall checks (in rosetta_dx.js and rosetta_dx_shell.js)

  • R1b-u (restates SPEC.md D-2, same 5 mm): covered length = length of the UNION of our pieces' intervals (bbox, along the source wall's long axis) inside the source wall rect; expected uncovered = length of the UNION of the source openings' intervals (door interval shrunk by 0.076 at each end, D-2). Union instead of sum because stacked windows share one plan interval and mitre triangles overlap the long pieces' bboxes. For pieces that never overlap (Level 1) union = sum.
  • rosetta_dx.js config adds wall.polygon_footprint: true and opening.gap_min 0.3 / gap_max 5 (SANDBOX, same values as rosetta_dx_types.js). Expected: R1b-u Level 1 17/17, Level 2 17/17; R1a still 0 stray (polygon pieces' bboxes inside a source wall); R1c 0 unexplained.
  • R6-shell (rosetta_dx_shell.js): every piece with height_source: elevation has the z range of the source wall containing it (10 mm), asserted; slab_above pieces: count equal / not equal reported, each mismatch listed (INFO, a convention is not asserted).

3. Shared: rectilinear shell solid

  • SH-M prismRect(outline, holes, z0, z1): outline and holes rectilinear (every edge axis-aligned within 1e-9 after rounding to 6 dp), holes inside the outline and disjoint. Grid = all distinct x and y of outline and holes; a cell is solid when its centre is inside the outline and inside no hole. Top and bottom = two triangles per solid cell; sides = one quad (two triangles) per cell edge between a solid cell and a non-solid cell or the outside, oriented outward. Result is one closed, outward mesh (each directed edge once, reverse once). Non-rectilinear input → GAP:shell_not_rectilinear (no mesh). Deterministic: cells in (y, x) order. Tests: SM1 rectangle 2x3, z 0..0.5 → volume 3.0, watertight · SM2 rectangle with a 1x1 hole → volume (6-1) x 0.5, watertight, the hole has inner side faces · SM3 L-shape (area 12) → 12 x thickness · SM4 diagonal outline → gap, no mesh · proves (all): a slab mesh that is open, inside out, fills holes or invents area.

4. Floor slabs

4.1 S-O outline (convention on drawn walls)

  • S-O Only for a storey whose footprint walls form a rectangular envelope E (bbox of all emitted wall pieces of the plan, rect or polygon). For each face F of E: the exterior pieces of F are pieces whose rect touches F's plane (snap_tol) and whose long axis is parallel to F (squares and polygon corner triangles excluded). Their inner edges (the rect side opposite F) must all be equal within snap_tol → inner line of F; zero pieces → GAP:slab_no_exterior_wall (face named); unequal → GAP:slab_inner_face_conflict (values listed). Outline = rectangle of the four inner lines. Convention, stated: "the slab spans between the inner faces of the exterior walls" (the source model's convention, slab-between-walls); NOT read from a slab entity (none exists in the set). outline_source: 'exterior_wall_inner_faces', members = handles used.
  • Not drawn, therefore not built: splits of the slab under party walls, stair voids, exterior porch slabs, finish-floor layers. The pipeline cannot see them; the witness lists the area they account for (§4.4).

4.2 S-Z top and underside (from elevation-derived wall heights)

  • S-Z1 top T = level + base of the storey's exterior pieces with height_source: 'elevation'; all must agree within elev_tol, else GAP:slab_top_conflict; none → GAP:slab_no_top.
  • S-Z2 underside U = the common top level_below + base + height of the pieces of the storey BELOW that have height_source: 'elevation' and end strictly below T (by more than elev_tol). All must agree within elev_tol, else GAP:slab_underside_conflict (values listed); none (no storey below in the set, or no lower piece ends below T) → GAP:slab_no_underside. Convention, stated: a lower wall seen in an elevation that stops below the next floor stops at that floor's underside.
  • S-Z3 thickness = T - U > 0, else GAP:slab_bad_thickness. No thickness → the slab op is emitted with thickness: null and NOT meshed (GAP:slab_no_underside carried). Never a default thickness. Expected before the run: Level 1 T = 0 (GF exterior rects start at 0), U: no storey below in the set → GAP:slab_no_underside (the foundation is seen only as elevation rects -1250..0, which end AT T, not below; and is not a plan). Level 2 T = 3.1, U = 2.795 (Level-1 interior walls seen in FRONT/REAR/LEFT/RIGHT stop at 2795) → 0.305 m. Tests: SZ1 hand: lower pieces end at 2.795 (two) and 3.1 (one, ends at T: excluded) → U 2.795 · SZ2 lower tops 2.795 and 2.6 → conflict gap, no slab thickness · SZ3 no lower storey → slab_no_underside, op has thickness null, not meshed · SZ4 T disagreement → slab_top_conflict · SO1 4-face envelope → inner rectangle (hand) · SO2 inner edges differ on one face → conflict gap · SO3 face without an along-face piece → gap · proves (all): invented thickness, a lower wall that reaches the floor treated as an underside, or a slab outline guessed where walls disagree.

4.3 Slab op and mesh

DXF_SLAB params: outline, holes: [], top, bottom, thickness, outline_source, z_source ({top: handles, underside: handles}), storey. Meshed with SH-M from bottom to top. GUID = hash(run ids of the storey | 'slab').

4.4 Witness (rosetta_dx_shell.js), per storey

Exact area classification on a compressed grid (all x, y of our outline, the source slabs' projected top triangles, the source walls' rtree rects, the source stair/railing rects of the storey band): every cell centre is classified once. - SL-a outline: each edge of our outline equals the corresponding extreme of the union of the storey's structural source slabs (not "Finish Floor", not porch slabs lying outside E) within 1 mm. Asserted. - SL-b precision: area in ours and not in a source structural slab = (under a source wall of this or the next storey: convention slab-under-wall) + (inside a source IfcStairFlight/IfcRailing rect: stair void not drawn) + OTHER. Asserted OTHER ≤ 0.01 m². - SL-c recall: area in source structural slabs inside E and not in ours = OTHER ≤ 0.01 m² (asserted); slabs outside E (porch) reported as not drawn. - SL-d z: our top and bottom vs the source structural slab z range of that storey (10 mm), asserted where we have a thickness; Level 1 reported as GAP (expected slab_no_underside).

5. Roof

5.1 R-O outline

  • R-O1 roof plan check: the ROOF plan (layer type roof in its layer map) must hold exactly one closed outline; it must equal the OUTER envelope E of the top storey's walls within snap_tol (consistency: the roof covers that storey). Else GAP:roof_outline_mismatch (both rects listed), no roof.
  • R-O2 slab outline = S-O applied to the top storey's pieces (the roof slab spans between the inner faces of the top storey's exterior walls, the same convention as floors). outline_source: 'top_storey_inner_faces', plus the A-ROOF handle.

5.2 R-Z bottom and top

  • R-Z1 bottom B = common top (level + base + height) of the top storey's exterior pieces with height_source: 'elevation' (all agree within elev_tol, else GAP:roof_bottom_conflict; none → GAP:roof_no_bottom).
  • R-Z2 top = the @ROOF marking (§5.3) of the marked elevations: every elevation that carries it must agree within elev_tol (else GAP:roof_top_conflict), and each value must coincide with a drawn horizontal line of type levelline in that elevation (via the @Z calibration, within elev_tol), else GAP:roof_top_not_drawn. No @ROOF anywhere → GAP:roof_no_top, roof not meshed.
  • Expected before the run: B 6.0 (Level-2 exterior rects end at 6000), top 6.5 (ROOF LEVEL +6.500 line). The source roof top is 6.457: the witness is EXPECTED TO FAIL on the roof top by 43 mm (the drawing's roof level marker disagrees with the model; reported as a finding, not tuned away).

5.3 Marking @ROOF (dxf2marks.js, one new key)

  • @ROOF=<metres> (ELEV view, single value, number; malformed/conflict per §24 rules): "the top of the roof structure is at this absolute height". The marker author takes it from the drawing's roof level line; the reader never parses level text (SPEC.md V5). For the Duplex the marking step of rosetta_dx_shell.js reads the A-ANNO-LEVL text ROOF LEVEL +6.500 in each elevation and writes @ROOF=6.5 into a COPY (out/marked_shell/); it refuses (no marker) when an elevation has zero or more than one such text. Test MK-R (test_shell.js): parse @ROOF=6.5 → desc.roof 6.5; @ROOF=abc → MARK:malformed; two different values → MARK:conflict · proves: a roof height read from a bad or contradictory marking.

5.4 R-H roof openings

  • R-H Each closed polyline of type glazing in the ROOF plan that lies inside the roof outline is a roof hole equal to that rectangle (the drawn skylight unit). Not rectangular → GAP:roof_hole_not_rect; outside or crossing the outline → GAP:roof_hole_outside. Stated limit: the drawing shows the skylight unit, not the structural opening; the hole is the unit outline.
  • Witness RH: hole centre vs source roof-slab hole centre ≤ 10 mm (asserted), our hole contains the source hole (asserted), the size excess per side reported.

5.5 Witness (roof)

RF-a outline edges vs source roof slab 1121 extremes (1 mm, asserted) · RF-b area classification as §4.4 (holes compared as RH) · RF-c bottom vs source 6.0 (10 mm, asserted) · RF-d top vs source 6.457 (10 mm, asserted; expected FAIL, see §5.2). Not built: parapet walls (source storey Roof, 6.0..6.609: seen only in elevations, no plan), foundation walls (seen only in elevations, no plan), skylight fills (§7.6).

6. Openings cut into walls (drawn gaps bridged)

The Duplex plans split every wall at its openings (drawn gaps). A wall with a window is therefore missing the wall below the sill and above the head. The cut is made by building what the elevation shows around the void, never by guessing it. - OP-1 Strip: for an opening O (door with a height, or window with sill/height) its host piece H (host_wall) gives the strip: H's cross range (rect side across the axis) and axis. Pieces of the same storey with the same orientation and the same cross range (within snap_tol) are the strip's pieces, sorted along the axis. - OP-2 Gap: O's span centre c along the axis (window: bbox centre; door: midpoint of hinge and the swing end point nearer the host axis, SPEC.md M-c) must lie strictly between two consecutive strip pieces A (before) and B (after): G = [A.end, B.start]. c inside a piece → GAP:opening_inside_piece (needs a true cut; none expected); no piece on one side → GAP:opening_gap_unbounded. - OP-3 Height: A and B must have the same level+base and top within elev_tol (else GAP:gap_height_conflict) and a height (else GAP:no_height). Every opening of G must lie inside [zb, zt] (else GAP:opening_outside_wall). - OP-4 Infill: G x strip x ([zb, zt] minus the union of the z intervals of all openings whose centre is in G) → one box per remaining interval (sill, between stacked windows, lintel), class IfcWall, infill: true, infill_of: [A.handle, B.handle], openings: [handles]. The void is G x opening z range: its width is the DRAWN gap, not the arc radius. - OP-5 Gaps without an opening (an interior door with no height, an export-defect door arc, see SPEC.md §21) stay full-height slots and are listed NOTE:gap_without_opening with the gap interval. Tests: OP1 window 1.0 wide, sill 0.9, height 1.2 in a 1.0 gap between two 3 m pieces → 2 infill boxes (0..0.9, 2.1..3.0), total volume = gap x t x (3-1.2) · OP2 three stacked windows → 4 boxes (sill, two mullion strips, lintel), hand z · OP3 door (sill 0) → lintel only · OP4 A and B with different tops → gap_height_conflict, no infill · OP5 opening centre inside a piece → opening_inside_piece · OP6 gap with no opening → NOTE, no infill · OP7 order independence (20 permutations of ops) · proves: voids not cut, wall above/below a window missing, wall built across a door, invented heights, order dependence. Witness OV (rosetta_dx_shell.js), for each source IfcDoor/IfcWindow hosted (rel_fills_host) in an IfcWall of Level 1/2: sample our wall solids (pieces, polygon prisms, infill) at the opening's plan centre: (1) z = centre, z0+20 mm, z1-20 mm → in NO wall solid (void cut); (2) z1+20 mm when the host source wall top is more than 40 mm above z1 → INSIDE a wall solid (wall above present); (3) z0-20 mm when z0 is more than 40 mm above the host wall base → INSIDE (wall below present). Status per opening: ok / explained (a logged gap at that opening: door without height, door_below_min_width) / FAIL. Asserted: FAIL = 0; counts reported.

7. Fills from the LOD400 component library (no fallback)

User directive (~/bim-compiler/prompts/RESUME_MODELLER_LOD400_REAL_GEOMETRY.md §LOD400-ENVELOPE): a bound product without real geometry is a HARD FAIL plus a GAP; never a box. - GEO-1 Product → type name. From the bound ad_opening_family row: size key = first NNN x NNN pair of its description (digits only; same pairKey as rosetta_dx_types R-T2); family token = the description text before that pair, trimmed. Candidates = distinct ad_geometry_map.element_ref of the same ifc_class whose size key equals it; if some candidates' family part (text before the first :) equals the token, keep only those. Exactly one → that type; zero → GAP:no_lod400_geometry; several → GAP:lod400_ambiguous. FROM_LIBRARY_DESCRIPTION (as SPEC.md §40 WALLMAP). - GEO-2 Geometry: rows of that element_ref with their component_geometries mesh and component_definitions local bbox. Keep the rows whose local long horizontal side lies along the opening's axis (window/door axis X: local X extent ≥ local Y extent). None → GAP:lod400_orientation. Several hashes → lowest geometry_hash (string order); handing/mirroring is NOT decided (stated). A mesh with ≤ 12 triangles is an envelope → GAP:lod400_envelope. - GEO-3 Placement: translate the local mesh so its bbox centre lands at (cx, cy, cz): window cx, cy = plan bbox centre, door cx, cy = centre of the matched drawn gap G on the strip centreline (OP-2), cz = level + sill + height/2 (door sill 0). No rotation, no scaling (the mesh keeps its authored size). Size delta (mesh bbox vs measured width/height) reported; above bind.size_tol → GAP:fill_size_mismatch (fill still placed: the product is the bound one). - GEO-4 Hard fail: after all fills, if any GAP:no_lod400_geometry | lod400_ambiguous | lod400_orientation | lod400_envelope exists, placeFills THROWS an Error listing every one (the GAPs are attached to the error). An opening that is not bound (no type) gets no fill and GAP:fill_unbound (not a hard fail: no product was chosen, so no product geometry is missing). - Binding used: the rosetta_dx_types chain (prefer DUPLEX_NOMINAL, SANDBOX sets by id prefix; 819x759 windows labelled from the SOURCE model with z=, as its E7 check: round trip, stated). Doors bind only with an elevation height (4 of 14). Tests: GF1 description with token + size → the one element_ref · GF2 size only (US row) → the one element_ref of that size · GF3 no match → throws, error lists GAP:no_lod400_geometry · GF4 two element_refs of the size, no token → throws lod400_ambiguous · GF5 orientation: Y-long opening picks the Y-long mesh; none → throws lod400_orientation · GF6 12-triangle mesh → throws lod400_envelope · GF7 placement: mesh bbox centre lands on the opening centre, z from sill (hand) · GF8 unbound opening → fill_unbound, no throw, no geometry · proves: a box or a wrong product placed, a missing geometry silently skipped, wrong position. Witness FL (rosetta_dx_shell.js): for each placed fill, the source IfcDoor/IfcWindow of that storey whose bbox centre is within 10 mm (x, y, z) and whose type size key equals the fill's element_ref key: precision (every fill matches one, one-to-one) and recall (source doors/windows of Level 1/2 with a fill / total, unfilled ones listed with their gap) reported; asserted: 0 unmatched fills, 0 wrong product. Control FLC: the same scorer with fills moved 50 mm along x must report them unmatched.

8. Assembly, OBJ, determinism

  • Elements: wall pieces (rect boxes, polygon prisms), infill boxes, slabs with a thickness, roof, fills. Classes IfcWall, IfcSlab (floor), IfcRoof (roof slab), IfcDoor, IfcWindow. tests/dxf2bim/out/duplex_basic_shapes.obj via dxf2mesh.toObj.
  • Logged: element count by class, triangle count, SHA-256 of the OBJ text; the whole pipeline run twice, hashes equal (MESH-det). Every wall/slab/roof element closed and outward (signed volume > 0, each edge used twice); library fills are reported as-is (their mesh is the library's; closure counted, not asserted).

9. Mutation controls (each check is broken on purpose; the named check must FAIL; then restored)

M-W1 F9 off → R1b-u Level 2 FAIL · M-W2 F5b off → R1b-u Level 2 (1035) FAIL · M-W3 R1b sum instead of union → R1b-u Level 2 FAIL · M-S1 underside taken from all lower pieces (no "ends below T" filter) → SZ1 / SL-d FAIL · M-S2 inner faces replaced by outer envelope → SO1 / SL-a FAIL · M-R1 roof top from @ROOF ignored → RF-d gap path / MK-R · M-O1 infill not built → OV FAIL · M-O2 void filled (infill over the full height) → OV FAIL · M-F1 orientation rule off → GF5 / FL FAIL · M-F2 missing geometry silently skipped (no throw) → GF3 FAIL · M-F3 z from level only (sill ignored) → GF7 / FL FAIL. Results recorded in §10.

9b. Amendments made while coding (2026-10-16, before the final run; each with its test)

  • W-H3 demoted (coordinator, user rule "a wall without a drawn height = GAP"): the MAIN model (variant A) meshes only walls with an elevation height; pieces no elevation shows stay GAP:no_elevation_height. W-H3 runs only in variant B (out/duplex_basic_shapes_convention.obj), reported as INFO, never asserted.
  • OP-1 for windows: axis and strip come from the window's own drawn box, not from host_wall (dxf2bim W8 picks a square corner piece on distance ties, so the strip was wrong). Test OP8.
  • OP-1b doors: the strip is the one whose axis line passes through the hinge AND along which one swing end point lies (closed leaf in the wall); exactly one such strip, else GAP:door_host_ambiguous | door_no_strip. Nearest-piece hosting failed at a T-junction (Level-2 door 164 was hosted by partition BC instead of wall 1035). Test DS2.
  • §6.1 door swing (coordinator): doorSwing reports opens_to (side of the wall the arc's mid-angle points to) and hinge_jamb (hinge at the -/+ end of the void along the wall axis), read from the arc only. Test DS1. Truth: NOT available in the answer key (element_transforms = centre only; door meshes carry no swing), so swing is reported, not claimed.
  • Witness DW (coordinator): ALL 14 source doors and 22 windows of Level 1/2 vs the plan: door = plan arc with hinge in the source bbox (20 mm), width = source bbox long side (2 mm), strip pieces inside the source host wall (rel_fills_host); window = plan box (10 mm) + elevation sill/head (10 mm), strip pieces at its box ends inside the source host wall. A miss must be explained by a logged gap.
  • RF-e (added after mutation M-SH survived RF-b/RH, which use the hole rings, not the mesh): roof mesh volume = classified roof area x thickness (1e-4 m3).
  • FL also compares the placed fill's bbox with the source bbox (10 mm, all six faces), after mutation M-F1 (orientation rule off) survived a centre-only check.
  • SL-d asserted for Level 2 (storey below in the set) instead of only reported, after mutation M-S1 turned it into a silent GAP.
  • dxf2bim run extent uses footprint rectangles (a reoriented pier's axis is not its extent): with stub orientation active, Level 1's extent shrank to y -0.2085 and assembly raised a false WARN:storey_extent_mismatch.
  • Unit tests added beyond §2-§7: RP1 (roof plan outline/holes), RZ1-RZ2 (roof z rules), OP8, DS1, DS2.

10. Results (2026-10-16)

Code: deploy/dev/dxf2shell.js (new); edits: dxf2bim.js (F9 polyOutline, F5b thin candidates, stub pass excludes polygons, extent from rects), dxf2mesh.js (polygon wall prism, GAP:opening_in_polygon_wall), dxf2marks.js (@ROOF). Tests tests/dxf2bim/test_shell.js (44 checks, log test_shell.log), witness tests/dxf2bim/rosetta_dx_shell.js (log rosetta_dx_shell.log), rosetta_dx.js (flags + R1b-u union). Nothing committed.

10.1 Walls (drawings only: plan pieces + elevation heights; no height, slab or level value from config)

  • R1b-u recall: Level 1 17/17, Level 2 17/17 (was 12/17) in both rosetta_dx.js and rosetta_dx_shell.js; R1a 0 stray (29 and 37 plan pieces; Level 2 gains 8 polygon pieces and 2 F5b piers: C2 = wall 1035's 20 mm pier, 124 = a 0.6 mm sliver inside wall 1036).
  • LV: both plan levels (0, 3.1; markings FROM_SOURCE_MODEL) coincide with drawn elevation rect bottoms (18 / 16 rects).
  • R6-shell: every piece with an elevation height equals its source wall's z (L1 18/18, L2 20/20, 10 mm). Meshed (variant A): 18 of 29 and 20 of 37 pieces; the rest GAP:no_elevation_height (interior walls no exterior elevation shows).
  • Variant B (W-H3 convention, INFO): L1 10/11 equal (C0 0..2.795 vs source 1029 0..3.1), L2 15/17 equal (116, 119 at 6.0 vs source 1033/1034 5.695).
  • Openings cut (OV, sampling our solids at each source opening): L1 10 openings: 8 ok, 2 explained (interior doors without a drawn height); L2 26: 18 ok, 8 explained (6 doors without height, 2 export-defect arcs r 0.174); 0 FAIL. 40 infill boxes (12 + 28) around 18 voids; 0 opening_outside_wall (rosetta_dx's old mesh path still reports 14, it does not bridge).
  • DW: doors L1 6/6, L2 6/8 + 2 explained (§21 export defect); windows 4/4 and 18/18 (plan box, sill/head, host). Finding: dxf2bim W8 host_wall is a perpendicular corner piece for windows 76, 81, 82, 83, 88 (not used by the shell, not fixed).
  • Door swing read from arcs (12 doors), e.g. 62 opens to -x hinge at -y jamb, 74 opens to +y hinge at -x jamb; not verifiable against the source (no rotation/handing stored).

10.2 Floor slabs

  • Level 1: outline 0.417..8.383 x -17.383..-0.417 = source slab extremes (SL-a PASS); area: ours 135.151 m2, explained excess 5.465 m2 under source walls (party-wall gap between 1103/1104), 0 unexplained; source porch slabs 1122/1123 (50.84 m2) outside the building, not drawn. Thickness: GAP:slab_no_underside (no storey below in the set; foundation only as elevation rects ending AT 0): NOT meshed. Source 127 mm (-0.127..0) not reachable from these drawings.
  • Level 2: same outline (SL-a PASS); z 2.795..3.1 = source (SL-d PASS): top from the Level-2 exterior rects, underside from Level-1 interior walls seen ending at 2795 (81, 89, C3, C6, C9, CC). Area excess 9.331 m2 under party wall 990 and 7.257 m2 in the two stair voids (not drawn: no stair/void entity in the set), 0 unexplained.

10.3 Roof

  • Outline = Level-2 inner faces, equal to source roof 1121 (RF-a PASS); A-ROOF outline 59 equals the Level-2 envelope (R-O1). Holes: the two drawn skylight units, centres equal to the source holes (RH PASS), each 72 mm larger per side than the structural hole (0.323 m2 each, explained by the skylight unit outline). RF-e mesh volume 66.1396 = area x thickness.
  • Bottom 6.000001 (Level-2 exterior rects; source 6.0, RF-c PASS). Top 6.5 from @ROOF (each elevation's ROOF LEVEL +6.500 text, line on A-ANNO-LEVL verified): RF-d FAIL by 43 mm (source 6.457). Finding, kept as a failure: the drawing's roof level marker disagrees with the model; nothing else in the set states the roof top.

10.4 Fills (LOD400 library, no fallback)

26 fills placed (L1: 2 M_Single-Flush 1250, 2 M_Single-Glass 813, 2 M_Fixed 4835, 2 M_Fixed 750; L2: 4 M_Fixed 2800, 2 M_Fixed 750, 8 M_Fixed 819, 4 M_Casement 819), 0 hard fail, 0 size mismatch; FL: 26/26 match a source door/window of the same product (centre and bbox, 10 mm), 0 wrong; FLC control 26/26 unmatched when moved 50 mm. Not filled: the 10 doors without a drawn height (cannot bind, R-T4), and the 2 skylights (not attempted). Library meshes are per-instance EXTRACTED from this same Duplex IFC (circular for shape; the witness is placement/product).

10.5 Mesh

tests/dxf2bim/out/duplex_basic_shapes.obj: 106 elements (IfcWall 78 incl. 40 infill, IfcDoor 4, IfcWindow 22, IfcSlab 1, IfcRoof 1), 3,792 triangles, z 0..6.5, SHA-256 246ce6f0dd36404bdb33b664ab20dda1fd480ed69d7b37acc82a71b7f5208ecb, identical on a second full run; 80/80 wall/slab/roof elements closed and outward; 26/26 library fills closed (reported). Variant B: 134 elements, 70d3f6a81b9a0c02beeb81051479b2e917dfc365674e8681c4ceda22bfe4c4d7.

10.6 Mutations (broken on purpose, restored, re-run green)

M-W1 F9 off -> WP1, R1b-u L2 FAIL · M-W2 F5b off -> WT1, R1b-u L2 · M-W2b thin pieces vote -> WT3 · M-W3 R1b sum -> R1b-u L2 · M-S1 underside filter off -> SZ1, SL-d L2 · M-S2 outer envelope as outline -> SL-a L1/L2, RF-a · M-SH hole ignored by the mesher -> SM2, RF-e (RF-b/RH survived first; RF-e added) · M-R1 roof marking ignored -> RF-d, RF-e · M-O1 no infill -> OV L1/L2 · M-O2 void filled -> OV L1/L2 · M-O3 first stacked opening only -> OP2, OV L2 · M-D1 door strip first hit -> DS2 · M-D2 swing side flipped -> DS1 · M-F1 orientation rule off -> GF5, FL L2 (FL L1 survives: lowest hash is already the right orientation there) · M-F2 no hard fail -> GF3, GF4, GF6 · M-F3/M-F4 sill ignored -> GF7, FL L1/L2. During the first mutation pass three crashed instead of failing cleanly (null outline paths); guarded, re-run, now clean FAILs.

10.7 Regression (all re-run 2026-10-16 after the last change)

15 unit suites SUMMARY fail=0 (test_shell 44 new); rosetta_dx 15 PASS / 0 FAIL (was 14 / 1: Level-2 recall); rosetta_dx_elev 3/0, rosetta_dx_types 11/0, rosetta_dx_heights 5/0, verify_marks_dx 22/0 (unchanged); rosetta_dx_shell 33 PASS / 1 FAIL (RF-d, the 43 mm roof-level finding).

10.8 Not claimed

Level-1 slab thickness (not in the drawings); stair voids and party-wall slab splits (not drawn); porch slabs; finish floors; parapet and foundation walls (elevation-only, no plan); interior wall heights and interior door heights (no section/interior elevation: variant B is a convention); door swing correctness (no truth); door handing as left/right; skylight fills; 819x759 window types come from labels taken FROM the source model (round trip); library geometry for these products was extracted from this same IFC; markings for levels FROM_SOURCE_MODEL (corroborated by LV only); one building, exported by our own writer (not independent); SampleHouse not run.

11. Shared configuration, roof-plan role, run_id, element summary (2026-10-16, for the Modeller import, bim-ootb prompts/DXF2BIM_MODELLER_LANE.md §L3)

Why: the Modeller runs the same pipeline in the browser (lane L3). Its numbers must be the witness's numbers, not a copy-paste; its convert must be refused when a sheet has no role marking, and the ROOF plan had none; and its witness C1 needs the Node run's identity and elements. - CF-1 Shared config. deploy/dev/dxf2shell_config.js (UMD, Dxf2ShellConfig) holds MAP_PLAN/ELEV/LEVL/ROOF, CFG, the type-binding prefs (TYPES: size_tol 0.005, prefer DUPLEX_NOMINAL, SANDBOX sets by id prefix), FILL.size_tol 0.005 and the mark alignment tolerance 0.001: moved verbatim from rosetta_dx_shell.js, which now requires it. The Modeller copies the file verbatim. Proof of no change: the OBJ SHA-256 stays 246ce6f0… (§10.5). - CF-2 Roof-plan role marking. The vocabulary (§24 of SPEC.md) has @VIEW=PLAN|ELEV; a roof plan is a PLAN view that carries @ROOF and no @LEVEL (a stated reading of two existing keys, no new key). The marking step of rosetta_dx_shell.js (the marker, like §5.3) writes into a COPY out/marked_shell/DX_ROOF_shell.dxf: @SET = the plans' set, @VIEW=PLAN, @UNITS=mm from the header $INSUNITS 4 (DXF code table), @NAME=ROOF PLAN, @ROOF = the one value every marked elevation agrees on. It refuses (run stops) when the elevations disagree, the header code is not 4, or there is no A-ROOF outline. The pipeline reads the roof plan from that copy and takes its unit factor from the @UNITS marking (Dxf2Marks.UNITS) instead of the stated literal 0.001. describe still reports MARK:missing LEVEL,GRIDX(>=2),GRIDY(>=2) for it; a reader treats those as expected for the roof role only. - CF-3 run_id. Dxf2ShellConfig.runId(canon, hashHex, storeyRuns, sheetHashes) = SHA-256 of canon{pipeline, the Dxf2Bim.run run_id of each storey plan in level order, the sorted SHA-256 of every sheet text used (plans, marked elevations, marked roof plan), CFG, the four layer maps}. Identity of the drawing set + config; the library is NOT part of it (fills are a separate, reported input). Logged as check RUN-id (two full runs equal). - CF-4 Element summary. out/duplex_basic_shapes_elements.json = {run_id, storeys (name, level, run_id), elements: guid, class, storey, handle, layer, bbox [minx,miny,minz,maxx,maxy,maxz], centre, volume, infill flag + infill_of + openings for the §6 infill boxes, element_ref/geometry_hash/family_id for fills}. Check ELEM-dump: every infill box and every element present. Result (2026-10-16): OBJ 246ce6f0dd36404bdb33b664ab20dda1fd480ed69d7b37acc82a71b7f5208ecb unchanged; RUN-id PASS, ELEM-dump PASS (106 elements, 40 infill); RF-d still the one FAIL (§10.3 finding, unchanged).

12. Library subset export for the Modeller fills (2026-10-16, bim-ootb prompts/DXF2BIM_MODELLER_LANE.md §L3.2)

Why: the Modeller import (L3) places no door/window fill because no Modeller source holds the bound LOD400 rows and meshes. User rule: no DB or LFS on GitHub; prototyping stays local, deploy = OCI. - LS-1 Labels written. rosetta_dx_shell.js also writes out/duplex_basic_shapes_labels.json = {run_id, labels: {storey: [{class, x, y, z, text}]}}: exactly the labels it applied (from the source model, round trip, as before). No other change: OBJ SHA-256 stays 246ce6f0…. - LS-2 Export. scripts/export_library_subset.js --elements <summary.json> [--labels <labels.json>] [--lib <component_library.db>] --out <file.json>: bound = the fills' family_id + geometry_hash in the summary. Rows: all active ad_opening_family and ad_wall_type (the binder chooses among all size candidates); the ad_geometry_map door/window rows whose class and Dxf2Shell.pairKey equal a bound family's (the candidate set familyToRef reads), deduped element_ref|hash; their meshes decoded (vertices float32 LE, faces int32 LE, normals null when NULL — the encoding of GeometryIntegrityChecker.blobToFloats/blobToInts). Header: source DB SHA-256 + bytes, summary SHA-256 + run_id, row counts. No timestamp: same inputs → byte-identical. Refuses when a bound hash is outside the candidate set or a mesh row is absent. - LS-3 Not committed. The output is library data: it is written to a local, git-ignored path (the Modeller's modeller/dxf2bim/local/). Full-library / OCI loader: open, needs the user. Result (2026-10-16, on master 4bb4bf3c0): OBJ 246ce6f0… unchanged, run_id 501b2d4e…, RF-d still the one FAIL (§10.3). Export: 104,632 bytes, 68 families / 18 wall types / 20 map rows / 20 meshes (normals NULL 20/20) / 12 labels, source sha256 7e5df6e1…; two runs byte-identical. Decoded bbox check: 00b72db6… 0.467 × 1.402 × 2.086 m and 974f0644… 4.835 × 0.417 × 2.420 m equal the source door/window extents in DX_extracted.db. Consumer witnesses: bim-ootb witness_dxf_import_node.js P6/P7, witness_dxf_convert.js C1 FILLS 26/26.

13. The std set in the Modeller (2026-10-16, bim-ootb prompts/DXF2BIM_MODELLER_LANE.md §L3.3)

Why: the Modeller import runs the std set (the film's set) with copies of deploy/dev/; the std layer maps, the qualify settings and the door casing lived in test scripts, so a copy would have been a fork. - SC-1 Moved, verbatim. STD (MAP_PLAN/ELEV/LEVL/ROOF/SECT, EXCL_ELEV), QUALIFY (was DX_QUALIFY) from tests/dxf2bim/set_configs.js and CASING 0.076 from rosetta_dx_shell.js into deploy/dev/dxf2shell_config.js; set_configs.js re-exports them. Check: every set_configs export JSON-equal before/after; both rosetta logs and rosetta_qualify.log unchanged on master e3e9c70cd (OBJ dx 87e410d0…, std ca14e389…; std 36/0; std verdict qualifies, 0 blocking). - SC-2 std run_id. runId(..., maps) takes the maps the run used (omitted = DX maps, so the DX run_id 501b2d4e… is unchanged). The std run passes the STD maps and also hashes its SECTION sheet (the section states the slabs; it was outside the identity). std run_id = 0c538552…. - SC-3 std element summary. out/duplex_basic_shapes_elements_std.json + _labels_std.json (same format as §11 CF-4 / §12 LS-1); checks RUN-id + ELEM-dump now also run for the std set (158 elements, 51 infill).