| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Elements with an empty material-key tuple (e.g. IfcDoor, which has no IfcMaterialLayerSetUsage) satisfied the prefix-match check trivially, since `long_[:0] == ()` is true regardless of `long_`'s contents. This made unmaterialised elements match every other element's material key, so remove_coplanar_boundary_lines could dissolve door/window outlines into an adjacent wall's boundary. Guard against empty keys before the prefix/suffix comparisons. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_camera_face_layer_id computed a "camera-facing layer" value for every LayerSetDirection (AXIS1/AXIS2/AXIS3 alike), but mat_keys_match's contract only intends that fallback for AXIS3 slabs/roofs — for AXIS1/AXIS2 walls the full cross-section is always visible in plan, so sharing only a facing layer must not count as a material match. Since this function's only caller feeds mat_keys_match's face_a/face_b, two walls with genuinely different materials (e.g. different exterior siding) could reach and pass the face_a == face_b fallback and have their boundary wrongly removed. Return None for anything but AXIS3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… recreation Both toggles were plain BoolProperty fields on BIMCameraProperties with no EPset_Drawing storage or update callback, unlike sibling properties (has_linework, linework_mode, fill_mode, cut_mode) that already follow this pattern. Camera "Block representation" recreation (triggered from CreateDrawing when update_representation() detects a stale matrix/raster) calls import_camera_props(), which resets any property not explicitly restored from the pset back to its Python default — silently turning join_coplanar_surfaces back off (and generate_material_layers back on) with no feedback to the user. Wire both properties to get_update_layer_callback(), and restore them from GenerateMaterialLayers/JoinCoplanarSurfaces in import_camera_props, matching the existing has_linework/linework_mode/fill_mode/cut_mode pattern. Verified live: the toggle now survives a forced representation recreation in headless Blender. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
remove_coplanar_boundary_lines only removed a segment when both
elements in a pair drew an explicit path at the exact same SVG line
key (bilateral matching). For a "contained" pair — a small same-material
element (e.g. an infill/repair "plug") fully embedded inside a larger
one (e.g. a long existing wall) — this leaves lines behind in two ways:
- An inner element's edge that is strictly interior to the outer's
face (not touching the outer's own bbox boundary) has no matching
key to pair against at all, since the outer never draws anything
at that interior coordinate other than its own true silhouette.
- Contained elements are frequently modelled as filling an actual
notch/void cut into the outer element, so the outer's own
silhouette legitimately traces around that void too — but since
each element's cut geometry is generated independently, the two
copies of that boundary commonly land a fraction of a mm apart,
just outside bilateral's exact-key tolerance, so neither side's
copy is ever recognised as a duplicate of the other.
Both cases left real, visible internal lines splitting up what should
render as one seamless area (reported as a long EXISTING wall with
several NEW same-material "plugs" still showing outlines around each
plug).
Add a geometry-based purge pass for confirmed contained pairs that
doesn't require an exact key match: it removes an inner segment that
is strictly interior to the outer's bbox, and an outer segment that
lies on/within the inner's bbox footprint (but not on the outer's own
true perimeter), in both cases only when no other differently
-materialed element also has a segment there (protecting a genuine
boundary against a third neighbour, e.g. a door cut into the plug
area).
Verified against the real-world repro model linked from PR IfcOpenShell#7908: the
reported "voids and infills" wall in the NORTH SECTION drawing now
renders with the plug outlines fully resolved, and every other
regression case in the same test drawing (materials, styles, other
containment cases) renders pixel-identical to the prior baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up to ea378a3's contained-pair purge, which itself left several concrete cases wrong when re-tested against coplanar join.ifc's PLAN_VIEW and NORTH SECTION drawings: - Line-key deference guard: the contained-pair purge decided whether to delete a segment using only a bounding-box test against the other element's overall bbox, so it would delete a real external edge that simply happened to run through that bbox range (e.g. two colinear, end-to-end perimeter segments on either side of a contained element) even when bilateral matching, just above it, had already correctly left that exact line alone. Both the inner- and outer-segment purge loops now skip a candidate whenever the other element independently draws its own real edge on the same line key, deferring to bilateral's own decision. - Clip-to-overlap for the outer-segment loop: a long containing element's one continuous real edge (e.g. a full-height wall corner) that merely passes near/through a small contained element's footprint was removed wholesale instead of only the truly-overlapping portion, destroying the part that sticks out beyond the contained element. Now intersects the segment's own interval with the contained element's extent and only removes/re-adds the overlap, preserving the remainder. - Bilateral-remnant interior filter: for contained pairs, a partial bilateral match's leftover remainder was re-added unconditionally even when it was itself still strictly interior to the other element's bbox (not a real perimeter edge, just an artifact of the partial match). - Outer-continuation guard on the inner-segment purge loop: purging an inner segment purely because its coordinate falls inside the outer's bbox range doesn't distinguish a genuine interior seam from the inner element's own real perimeter edge that coincidentally falls in that range (e.g. a material-layer/fascia edge two independently generated, adjacent panels each draw at slightly different positions). Now also requires a same-orientation outer segment beyond the candidate line, overlapping its interval, as positive confirmation that outer material genuinely continues past it. - Fixed-point iteration: the per-parent-group pass computed removals once from a single static parse of each element's segments, so a remainder segment created mid-pass (e.g. after a partial bilateral match against one neighbour) never got a chance to also be evaluated against a different neighbour that should account for the rest of it. The pass now repeats (bounded, 10 rounds) until a round makes no further changes. - Camera-aligned dominant normal: dominant_world_normal() picked whichever single polygon had the largest raw area, which fails for thin, elongated elements (e.g. a single masonry course modelled as its own sliver) whose top/bottom cap outweighs its front face by area — the wrong axis entirely for a section/elevation view. Now picks the polygon whose world normal is most aligned with the camera's own view direction, which is the face that actually matters for whichever projection is being drawn (naturally still picks the top/bottom face for plan views, where that's correct). Verified headless (Blender 4.5.3 debug build, bonsai_test profile) against coplanar join.ifc: PLAN_VIEW's two roof-panel pairs now render as a single seamless rectangle with symmetric material-layer detail on both sides; NORTH SECTION's 15 reported wall elements drop from 61 to 46 remaining path segments (several fully resolved to zero). Broader before/after path-set comparison across all four drawings in the model shows only small, legitimate corrections elsewhere, no unexplained regressions. Some interior lines remain in NORTH SECTION's wall assembly, traced to further instances of the same multi-neighbour partial-coverage pattern; not yet fully resolved. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…allback The previous commit's fixed-point iteration re-derives segments and reprocesses each parent group across multiple rounds, letting a remainder created mid-pass get a further chance to be matched against a different neighbour. That correctly fixed several NORTH SECTION wall cases, but introduced a regression: 2x0$XNlFH6FxlvS7eYJttx (a wall adjacent to 1eW6OACmXD1hWJLDeB6erW, which has a hole cut all the way through to their shared boundary) lost the entire left edge of that hole — the only remaining copy of that boundary line, since 1eW6 has no material left there at all to draw its own copy. Root cause: in round 0, bilateral correctly matches the overlapping portions of this pair's shared edge and leaves the right remainder in place (the hole's exposed edge) — this fully drains 1eW6's presence at that line key (nothing left on its side at all). In round 1, group_data is re-derived from the mutated SVG; since 1eW6 has zero segments at that key now, bilateral has nothing to compare this round, so the cruder unilateral fallback fires instead — and its bbox-boundary heuristic, blind to the fact that 1eW6's side was already deliberately and fully drained, wrongly treats the remainder as an unmatched implicit edge and deletes it. Fix: track every (guid, line_key) whose entire presence at that line was consumed to nothing by a bilateral match in an earlier round. The unilateral fallback now skips a removal whenever the bbox reference it would rely on belongs to a guid+key that was already fully drained — that match already gave its final, precise answer for this line; a later round must not re-litigate it with a cruder heuristic. This is deliberately narrower than blocking unilateral for the whole pair once any bilateral relationship existed between two elements (tried first, but that also blocked a legitimate wall-panel cleanup elsewhere) — draining is tracked per line key, not per pair, so it only suppresses unilateral for the specific line that was actually resolved, leaving every other key and pair unaffected. Verified headless (Blender 4.5.3 debug build, bonsai_test profile) against coplanar join.ifc: 2x0$'s hole edge is restored in both NORTH SECTION and SOUTH SECTION, matching the pre-regression baseline exactly. NORTH SECTION's wall-panel fix from the previous commit is preserved (15-GUID remaining path count 48, versus 47 with the regression still present) — one small residual segment remains on a different element, part of the same disclosed multi-neighbour partial-coverage tail, not a new issue. Broader before/after comparison across all four drawings shows only this restoration plus the same class of sub-micron coordinate noise seen in every previous round. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
remove_coplanar_boundary_lines' bilateral matching computed each pair's
remainder ("this element's line minus what this one partner matched")
immediately and independently. When an element shares the same line key with
more than one same-material neighbour in the same round (e.g. a wall panel
flanked by two others, each covering a different part of its edge), each
pair's remainder calculation was blind to what any other pair contributed
against that same element+line — so each remainder still included the
portion the other pair correctly claimed. Both partial remainders got added
back as separate paths, and the next round's group_by_line/ivs_union merged
them back into something that looked like almost the entire original,
untouched edge, even though the two neighbours together had already fully
accounted for it.
Concretely: 0gbhn0X9z7BBLNF58AuXfq shares one line key with both
0ZA0gbiYP4yeaGvSMlNATl (matching the left portion) and
2gKm6D45r4xgNcZthbYpFn (matching the middle portion) in the same round. Each
match's independently-computed remainder for 0gbhn included the portion the
other one had just claimed, and both got re-added, reconstructing 0gbhn's
edge almost in full for the next round to deal with (which the round-based
iteration and unilateral fallback couldn't cleanly resolve, since neither
guid_i is a fully "drained" claimant on its own).
Fix: group_by_line is now computed once per round (lines_by_guid), shared
across every pair instead of being recomputed per pair. For non-contained
matches (same_surface / plain adjacent), a pair no longer computes and
re-adds its own remainder immediately — it just accumulates its matched
interval into a per-(guid, line_key) union across every partner this round.
After the full (i, j) double loop finishes, a finalisation pass computes
each affected element's true remainder once, as its original extent minus
the combined union of every partner's match, and adds back only that (or
marks the key drained if nothing is left). Contained-pair remainder handling
is completely untouched — this only changes the non-contained path, which is
where multi-neighbour splits actually occur.
Verified headless (Blender 4.5.3 debug build, bonsai_test profile) against
coplanar join.ifc: NORTH SECTION's 15-GUID remaining path count drops from
48 to 43. 0gbhn's residual is gone, and 0MmPs4lGP0zgpUVE$tStUb (sharing the
same underlying pattern) resolves fully as a side effect — neither appears
in an overall-bbox interior-line scan any more. The "hole on edge" pair
(1eW6OACmXD1hWJLDeB6erW / 2x0$XNlFH6FxlvS7eYJttx) and PLAN_VIEW's
contained-pair fixes are unaffected (identical segment counts before and
after). Broader before/after path-set comparison across all four drawings
shows only this fix's intended removals plus the same class of sub-micron
coordinate noise seen in every previous round.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… dots IfcConvert's own linework export leaves a small number of near-zero-length path segments in the raw, untouched output for elements with complex voids and infills — almost certainly floating-point noise from the underlying BRep boolean/cut operations, present in the very first raw dump captured for these elements, long before any of this branch's changes. These render as visible dots rather than lines, mixed in among the genuine boundary lines remove_coplanar_boundary_lines is meant to clean up. Measured on the real repro model (3_TyYp2_X03PtH$QzJHWjh): 9 segments at length < 0.0001 (true points) and 5 more at length ~0.0135-0.0145; the smallest confirmed genuine line on the same element is length ~2.83 — a 100x+ margin either side of the chosen 0.1 threshold. Adds a cleanup pass, gated the same way as the rest of this feature (only runs for multi-element projection groups this function already processes), that strips any remaining path whose parsed length falls below MIN_VISIBLE_LENGTH after the existing round-based matching converges. Verified headless (Blender 4.5.3 debug build, bonsai_test profile) against coplanar join.ifc: confirmed via total-line-coverage comparison (summing the real-length line union before/after) that this never removes anything with genuine length — identical total coverage across all four drawings in the model, only the confirmed dot artifacts disappear. Also confirmed this before/after methodology is sound by checking that Blender's own run-to-run floating-point noise (re-running identical, unmodified code twice) never changes total coverage either — only which of two near-identical duplicate segments happens to survive. A companion fix for the contained-pair "multi-neighbour" duplicate-removal bug (the other half of the plan this was drafted alongside) was found, via the same total-coverage check, to cause real data loss in a different drawing (a wall edge visible from another camera angle lost ~80 units of real coverage) — not included here, reverted pending a redesign. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
This is the human feedback. I'm not sure that these complex cases are resolvable purely using the information available at the time this code executes. The AI has basically reached an impasse where trying to take a step forward will cause a regression. It may be that this is a developmental cul-de-sac, with no choice but to go back to the start and reassess the how and the where of how this is done. |
Sorry, something went wrong.
|
@sboddy congratulations, this is a real breakthrough on the case that had both you and theoryshaw stuck. I read through the contained-pair and multi-neighbour work and the approach is sound on all four fronts: the MIN_VISIBLE_LENGTH filter catching the near-zero-length float-noise segments from the BRep cuts (the dots that read as lingering lines), hoisting group_by_line to a round-wide per-guid reference, the _consumed accumulator so an element's remainder is finalised against the union of ALL its neighbours' claims that round rather than pair by pair (the actual 3-way-junction fix), and the interior-seam suppression for the contained plug, which is the crux of the masonry-infill case. On your open no-material same-style question, I traced the current matching logic to give you a definite answer rather than a guess. As the branch stands, style is a refinement on top of material, not a substitute for it: mat_keys_match returns False for any element with an empty material tuple (your own empty-tuple guard), and the style check is only reached after a material match already succeeded. So two material-less elements never merge today regardless of shared style, and style only ever makes a match stricter, never looser. That is arguably the safer default: merging purely on a shared style would over-merge unrelated material-less elements that happen to carry the same default style (two different doors, say). If you did want material-less-but-same-style to merge, that would be a deliberate opt-in where the style key becomes an alternative material key, with that over-merge risk to weigh. So I would not treat the current non-merge as a bug to chase. This was a code-level read on my side this round rather than a fresh Blender pass. Whenever you feel it is settled, happy to run the before-and-after regression on your coplanar join.ifc plus Highland_Haven in headless Blender and post the path-count deltas alongside your screenshots, same as we did on the earlier tip. |
Sorry, something went wrong.
Sorry, something went wrong.
Sorry, something went wrong.
|
@theoryshaw Yeah, the edge hole is a definite failure. The horizontal lines are all layerset related. The X is one where I wasn't sure what the correct approach was. Merge or not merge. Now I know equal styles should be merged, it should be relatively easy to fix that one. I'll have a tinker. |
Sorry, something went wrong.
|
@theoryshaw I'm so dumb!!! Those horizontal lines are the edges of planes, not the dividing line between materials in a layerset. Is there supposed to be a dividing line between different materials too? Because if so there should be more lines than you have marked in red, which I assume is indicating the missing edge for the plane change. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Branch: fix-3742-coplanar-bonsai-projection_2_sjb
Fork: https://github.com/sboddy/IfcOpenShell/tree/fix-3742-coplanar-bonsai-projection_2_sjb
Base: built on top of theoryshaw's fix-3742-coplanar-bonsai-projection_2 (upstream PR #7908), which itself fixes #3742.
Status: commits pushed to my fork; opening as a draft PR against
IfcOpenShell/IfcOpenShell's fix-3742-coplanar-bonsai-projection_2 branch
(confirmed via gh pr view 7908 that this branch lives directly in the
upstream repo itself, not a separate fork).
Summary
Follow-up to #7908. That PR's remove_coplanar_boundary_lines removes duplicate
SVG boundary lines between adjacent, same-material, coplanar IFC elements, but
left one class of case explicitly unresolved (documented in the branch's own
knowledge-transfer notes as the "Known Unresolved Case"): a small same-material
element fully contained inside a larger one — e.g. a masonry infill/repair
"plug" embedded in a long existing wall — still shows visible internal boundary
lines around the plug.
This was reported concretely against theoryshaw's own test model
(coplanar join.ifc, linked from
PR #7908 comment):
a long Status=EXISTING wall (3_TyYp2_X03PtH$QzJHWjh, material "Rubble
Stone" / type WAL600-Solid-Ext) containing several Status=NEW plug walls of
the identical material/type. In the NORTH SECTION drawing ("A COMPLICATED
WALL WITH COMPLICATED VOIDS AND INFILLS" test case), the plugs' outlines
remained fully visible instead of merging seamlessly into the wall.
This branch adds eight commits on top of 5a1723f524 (the PR's tip at the
time of this work):
— the main fix, addressing the reported bug directly.
— a follow-up round fixing several further cases found by re-testing
commit 1's own fix against the same repro model (see below).
— a targeted fix for a regression commit 5 itself introduced (see below).
— resolves the residual NORTH SECTION lines left open after commit 6
(see below).
— a separate, unrelated bug surfaced by re-testing commit 7 (see below).
Commits 2–4 fix separate, real correctness bugs found while investigating the
main issue (independently corroborated by another reviewer's comment on the
PR thread) — they are not required for the main fix but sit in the same code
path and were bundled in at the user's request.
Root cause of the main bug
remove_coplanar_boundary_lines (in CreateDrawing, operator.py) only
removes an SVG line segment when both elements in a pair draw an explicit
<path> at the exact same bucketed line key (bilateral matching). For a
"contained" pair (one element's 3D AABB fully nested inside the other's, both
same material) this leaves lines behind two ways:
(doesn't touch the outer's own bbox boundary) has no matching key at all
— the outer never draws anything at that interior coordinate beyond its own
true silhouette, so bilateral matching finds nothing to pair against.
into the outer element, so the outer's own silhouette legitimately traces
around that void too — but because each element's cut geometry is generated
independently, the two copies of that boundary commonly land a fraction of
a millimetre apart, just outside bilateral's exact-key tolerance, so
neither side's copy is ever recognised as a duplicate of the other.
Both failure modes leave real, visible internal lines splitting up what should
render as one seamless area — exactly the "voids and infills" symptom
reported.
The fix
A new geometry-based purge pass, gated on confirmed contained_a/
contained_b pairs, that doesn't require an exact line-key match:
bbox (not touching its edge) — it can only be a boundary against this outer
element.
footprint, but excludes anything also sitting on the outer's own true
perimeter (protects the case where the inner touches/spans flush to one
side of the outer — that line is a genuine external boundary of the merged
shape and must stay).
also has a segment at that same position — that's a genuine boundary
against a third neighbour (e.g. a door cut into the plug area), not an
internal seam between the contained pair.
The three bundled fixes
b, because the prefix-match check long_[:0] == () is trivially true
regardless of long_. Elements with no material at all (e.g. IfcDoor)
matched every other element's material key, so a door's outline could be
wrongly dissolved into an adjacent wall's boundary.
"camera-facing layer" value for every LayerSetDirection
(AXIS1/AXIS2/AXIS3), but mat_keys_match's own docstring says that fallback
is only valid for AXIS3 slabs/roofs — for AXIS1/AXIS2 walls the full
cross-section is always visible in plan. Since this function's only caller
feeds mat_keys_match's face_a/face_b, two walls with genuinely
different materials (e.g. different exterior siding) could reach and pass
the face_a == face_b fallback and have their boundary wrongly removed.
were plain BoolProperty fields with no EPset_Drawing storage or update
callback (unlike sibling properties has_linework/linework_mode/
fill_mode/cut_mode, which already follow this pattern). Camera "Block
representation" recreation — triggered from CreateDrawing whenever
update_representation() detects a stale matrix/raster — calls
import_camera_props(), which resets any property not explicitly restored
from the pset back to its Python default, silently turning
join_coplanar_surfaces back off with no feedback to the user.
Verification
Ran headless (Blender 4.5.3 debug build, bonsai_test profile) against the
real repro model linked from the PR thread:
enabled generate_material_layers/join_coplanar_surfaces, ran
bim.create_drawing.
PR code only, same environment) — the reported plug outlines are fully
resolved, confirmed both in the raw SVG path data (target elements' groups
now empty/reduced to legitimate boundary segments only) and visually
(rendered PNG crops, pixel-diffed).
(small-angle coplanarity, different layer makeups, holes on edges,
"coplanar donut", extrusion/tessellation material-match cases, different
material/style combinations, no-material-assignment cases) renders
pixel-identical to the baseline.
recreation mid-session and verified join_coplanar_surfaces survives it.
Test artifacts (repro IFC/blend, before/after SVGs, rendered PNG comparisons,
verification scripts) are kept alongside this file in
kt_repo/ and verify/.
Follow-up round: commit 5
Re-testing commit 1's contained-pair purge against the same coplanar join.ifc model (PLAN_VIEW and NORTH SECTION drawings) surfaced several
further concrete cases where it still removed the wrong line, or failed to
remove one it should have:
upper perimeter line; a second pair
(2NR8CMK21CwOCIzz8vKnRv/0XMcotQ9nDsxGg_VJcDKKd) that should merge into
one rectangle instead had its own upper line wrongly deleted and a
spurious leftover vertical line down the middle; and once those were fixed,
a follow-up asymmetry appeared where one side of a shared roof edge kept
its own material-layer/fascia detail line while the other side's matching
line had been purged.
numerous interior lines, including several that had disappeared after an
earlier fix in this round and then reappeared once a later fix in the same
round was added — a sign the underlying algorithm, not any single
guard, needed to change.
Root causes (all in remove_coplanar_boundary_lines):
test against the other element's overall bbox, which can't tell a genuine
interior seam apart from a real external edge that simply happens to run
through that bbox range (e.g. two colinear, end-to-end perimeter segments
either side of a contained element, or one continuous outer wall corner
that merely passes near/through a small contained plug's footprint without
being confined to it).
an inner element's own real perimeter edge that happens to coincidentally
fall inside the outer's bbox range — e.g. two independently generated,
adjacent roof panels each drawing their own material-layer/fascia edge at
a slightly different position.
each element's segments — so a remainder segment created mid-pass (e.g.
after a partial match against one neighbour) never got a further chance
to be checked against a different neighbour that should have accounted
for the rest of it. This explains both the "still there" and the
"reappeared" symptoms: whether a given edge happened to be fully consumed
in one shot depended on incidental processing order, not on the
pairwise logic being right or wrong.
raw area to decide an element's facing direction. For a thin, elongated
element (e.g. a single masonry course modelled as its own sliver), the
top/bottom cap has more area than the front face, so two genuinely
coplanar, touching wall elements were wrongly judged non-coplanar
entirely — the normals being compared didn't even point along the same
axis.
Fixes:
whenever the other element independently draws its own real edge on the
same line key, deferring to bilateral matching's own (already correct)
decision for it, instead of re-deciding with the coarser bbox test.
element's extent (reusing the existing interval-intersection helpers)
instead of removing a candidate segment wholesale, preserving whatever
part of it sticks out beyond the contained element as a real edge.
filtered the same way before being re-added — if it's still strictly
interior to the other element's bbox, it's dropped rather than re-drawn.
same-orientation outer segment beyond the candidate line, overlapping its
interval — that outer material genuinely continues past it, rather than
purging on bbox-range membership alone.
rounds): after applying a round's removals/additions, it re-derives the
segment list from the now-mutated SVG and runs again, until a round makes
no further changes.
most aligned with the camera's own view direction, rather than the one
with the largest raw area — the face that actually matters for whichever
projection is being drawn (this still naturally picks the top/bottom face
for plan views, where that's the correct choice).
Verification (commit 5)
Same headless setup (Blender 4.5.3 debug build, bonsai_test profile)
against the same coplanar join.ifc model:
rectangle with no spurious middle line, and both sides show matching
material-layer/fascia detail (previously asymmetric).
count dropped from 61 to 46 (several elements fully resolved to zero
interior lines); some interior lines remain, traced to further instances
of the same multi-neighbour partial-coverage pattern (see Open items).
model (PLAN_VIEW, NORTH SECTION, SOUTH SECTION,
REFLECTED_PLAN_VIEW) shows only small, legitimate-looking corrections of
the same kind elsewhere, no unexplained regressions.
Follow-up round: commit 6
Commit 5's fixed-point iteration (re-deriving segments and reprocessing each
parent group across multiple rounds) fixed several NORTH SECTION wall cases,
but introduced a regression of its own, reported against the model's "HOLE
ON EDGE" test case: 1eW6OACmXD1hWJLDeB6erW (a wall with a hole cut all the
way through to its right edge) and 2x0$XNlFH6FxlvS7eYJttx (the adjacent
wall sharing that edge, with no hole). The entire left edge of 2x0$ — the
only remaining copy of that hole's right-hand boundary, since 1eW6 has no
material left on its own side to draw it — had been deleted, leaving that
side of the hole unlined.
Root cause: in round 0, bilateral matching correctly matches the overlapping
portions of this pair's shared edge and leaves the non-overlapping remainder
in place (the hole's exposed edge) — this fully drains 1eW6's presence at
that line key (nothing left on its side at all). In round 1, the segment
list is re-derived from the mutated SVG; since 1eW6 now has zero segments
at that key, bilateral has nothing to compare this round, so the cruder
unilateral fallback fires instead — and its bbox-boundary heuristic, blind
to the fact that 1eW6's side was already deliberately and fully drained,
wrongly treats the remainder as an unmatched implicit edge and deletes it.
This failure mode only exists because the pass now runs more than once —
with a single pass, round 1 (and this re-examination) never happened.
Fix: track every (guid, line_key) whose entire presence at that line was
consumed to nothing by a bilateral match in an earlier round. The unilateral
fallback now skips a removal whenever the bbox reference it would rely on
belongs to a guid+key that was already fully drained — that match already
gave its final, precise answer for this line; a later round must not
re-litigate it with a cruder heuristic. An initial attempt blocked
unilateral for an entire pair once they'd shown any bilateral relationship
at all, which fixed the hole case but was too broad — it also blocked a
legitimate wall-panel cleanup elsewhere in NORTH SECTION that genuinely
needed a later round's unilateral pass. Tracking drained keys instead of
related pairs is narrower and correct: it only suppresses unilateral for
the specific line that was actually resolved, leaving every other key and
pair (including the wall-panel case) unaffected.
Verification (commit 6)
Same headless setup against the same coplanar join.ifc model:
matching the pre-regression baseline exactly.
remaining path count is 48 (versus 47 with the regression still present) —
one small residual segment remains on a different element
(0gbhn0X9z7BBLNF58AuXfq), part of the same disclosed multi-neighbour
partial-coverage tail from commit 5, not a new issue.
restoration plus the same class of sub-micron coordinate noise seen in
every previous round.
Follow-up round: commit 7
Commit 6's own "Open items" note named the next issue precisely: NORTH
SECTION's wall assembly still showed a small number of residual interior
lines (e.g. one segment on 0gbhn0X9z7BBLNF58AuXfq), and had already
identified the root cause without yet fixing it.
Root cause: bilateral matching computed each pair's remainder ("this
element's line minus what this one partner matched") immediately and
independently, inside the per-pair loop. When an element shares the same
line key with more than one same-material neighbour in the same round —
0gbhn shares its bottom edge with both 0ZA0gbiYP4yeaGvSMlNATl (matching
the left portion) and 2gKm6D45r4xgNcZthbYpFn (matching the middle
portion) — each pair's remainder calculation was blind to what the other
pair contributed against that same element+line: 0gbhn's remainder from
the 0ZA0g match still included the portion 2gKm6 was about to claim, and
vice versa. Both partial remainders got added back as separate paths, and
the next round's group_by_line/ivs_union merged them back into something
that looked like almost the entire original, untouched edge — even though
the two neighbours together had already fully accounted for it. Neither the
round-based iteration (commit 5) nor the drained-keys guard (commit 6) fixes
this, since it isn't about a segment failing to find a second-round match
or being wrongly re-examined — it's that the first-round remainder itself
was computed wrong whenever two partners split the same line.
Fix: group_by_line is now computed once per round (lines_by_guid),
shared across every pair instead of recomputed per pair. For non-contained
matches (same_surface / plain adjacent), a pair no longer computes and
re-adds its own remainder immediately — it accumulates its matched interval
into a per-(guid, line_key) union across every partner this round. After
the full (i, j) double loop finishes, a finalisation pass computes each
affected element's true remainder once — its original extent minus the
combined union of every partner's match — and adds back only that (or
marks the key drained if nothing is left). Contained-pair remainder handling
is completely untouched; multi-neighbour splits only occur on the
non-contained path.
Verification (commit 7)
Same headless setup against the same coplanar join.ifc model:
residual is gone, and 0MmPs4lGP0zgpUVE$tStUb (sharing the same underlying
pattern) resolves fully as a side effect — neither appears in an
overall-bbox interior-line scan any more.
and PLAN_VIEW's contained-pair fixes are unaffected (identical segment
counts before and after).
only this fix's intended removals plus the same class of sub-micron
coordinate noise seen in every previous round.
Follow-up round: commit 8
Re-testing commit 7 against NORTH SECTION's wall assembly turned up two
distinct problems, reported precisely by GlobalId plus line start-coordinate.
Only one of the two is fixed here.
Fixed: degenerate near-zero-length paths render as unwanted dots
IfcConvert's own linework export leaves a small number of near-zero-length
path segments in the raw, untouched output for elements with complex voids
and infills (confirmed present in 3_TyYp2's very first raw dump, long
before any of this branch's changes) — almost certainly floating-point noise
from the underlying BRep boolean/cut operations. These render as visible
dots rather than lines. Measured on 3_TyYp2: 9 segments at length < 0.0001
(true points), 5 more at length ≈0.0135–0.0145; the smallest confirmed
genuine line on the same element is length ≈2.83 — a 100x+ margin either
side. Fix: strip any remaining path below a MIN_VISIBLE_LENGTH = 0.1
threshold, applied to every projection group this function already
processes, after the existing round-based matching converges.
Verified by comparing total line coverage (the union of every real-length
segment) before/after, rather than just path counts or path-string diffs —
identical in every one of the four drawings in the model, confirming nothing
with genuine length is ever removed. This methodology was itself checked for
soundness: re-running identical, unmodified code twice (to measure Blender's
own run-to-run floating-point noise) also produces identical total coverage,
so the technique reliably distinguishes "real content changed" from
"harmless coordinate jitter."
Found, not fixed: contained-pair multi-neighbour bug causes real data loss
The other reported issue — several NORTH SECTION lines following the exact
same "two different neighbours independently compute their own remainder"
pattern commit 7 fixed, but occurring in the contained-pair code path
(which commit 7 deliberately left untouched, reasoned to be "usually 1:1").
3_TyYp2 is the counter-example: it's the contained_b outer for four
different inner plugs, and the identical blind-spot bug exists in two
contained-pair mechanisms that each compute an "outer" remainder per-pair,
independently (the bilateral match's own remainder, and the interior-purge
pass's outer-segment clip-to-overlap). Traced precisely: 1$k4ji92 and
3taVdKrmD45ehPoNLdDmdt together account for the entirety of one of
3_TyYp2's vertical lines, but each pair's independent remainder
computation left an incomplete piece behind, exactly reproducing two of the
reported unwanted lines.
Extending commit 7's same accumulate-then-finalise pattern to the outer side
of contained-pair interactions did resolve those two specific lines — but
checking total wall-edge coverage in REFLECTED_PLAN_VIEW (where
3_TyYp2 is visible from a different camera angle, overlapping a different
combination of contained plugs) exposed real data loss: a continuous
360.44-unit wall-top edge shrank to 246.5 units, an 80+ unit gap. Diagnosed as
the finalisation pass running every round and recomputing "original extent"
from the current round's segment state — which, in a later round, is already
the shrunk remainder from an earlier round, not the true pristine original
— so a further match against that already-reduced piece keeps eroding it,
compounding across up to 10 rounds.
A follow-up attempt replaced the per-round remainder computation with a
persisted-across-rounds pristine original (captured once, at round 0, before
any pair processing) and a persisted, ever-growing accumulator of every
partner's matched interval, so the remainder is always computed against the
same true original rather than a prior round's already-shrunk one. This
correctly fixed the round-erosion arithmetic: reverifying with the same
coverage check, the two originally-targeted NORTH SECTION lines
(418.17856 205.35803 and 418.17856 196.76428 on 3_TyYp2) now resolve to
exactly zero remainder, as expected.
However, the same coverage check caught a second, deeper problem in
REFLECTED_PLAN_VIEW that this arithmetic fix doesn't and can't address:
bilateral matching's core assumption — that any line drawn by both sides of a
same-material pair is necessarily a duplicate internal seam, safe to remove
from both — is wrong whenever that shared line actually coincides with the
combined shape's true external perimeter. Traced precisely: contained
plug 3taVdKrmD45ehPoNLdDmdt reaches flush to containing wall
3_TyYp2_X03PtH$QzJHWjh's own exterior top face; both independently draw a
copy of that same real, visible exterior edge at line key ('h', 2714) in
REFLECTED_PLAN_VIEW (a plan/roof-facing camera where this edge is genuinely
part of the silhouette, not an interior seam at all). Bilateral matching
removes both copies as if deduplicating an interior seam, deleting 113.9
units of real, visible boundary rather than reducing two copies to one.
Distinguishing this case from a genuine interior seam using only 2D SVG
line-key/interval data isn't possible — both look identical at that level;
it needs 3D reasoning about whether the material genuinely continues past
the shared line (in the same sense already used for the inner-segment
purge's _outer_confirms_continuation guard) applied to bilateral matching
itself. This is a pre-existing gap in bilateral matching's own core
assumption, predating this branch entirely (present in the original #7908
design) — it was previously masked here only by chance, because the old,
buggy round-accumulation logic happened to leave an accidental redundant
copy in that exact spot instead of cleanly deleting both.
Given the depth of the fix this now needs, the round-erosion redesign was
reverted rather than committed (confirmed via git checkout -- back to
commit 8's tip, re-verified against the test profile). No new commit is
added for this round; the contained-pair multi-neighbour bug remains open,
now with a materially clearer, precisely-located root cause.
A third, distinct edge case was also found while investigating the above:
colinear-but-adjacent (non-overlapping) segments between a contained
inner and its outer, which represent a shared void/notch boundary split
between the two — bilateral correctly leaves both alone (matching its
existing, deliberate handling of offset-wall corners), but the interior-purge
pass's line-key-deference guard (from an earlier round) then also defers to
that decision, so neither side is ever merged. Distinguishing "genuine
offset-wall corner, must stay split" from "split void boundary, should merge"
using only line-key/interval data is nontrivial and needs its own dedicated
design.
One reported line (310.06823 199.15698 on 3_TyYp2) was confirmed to be an
original, untouched raw segment with no matching partner in any pair — not
caused by either bug above; likely needs a same-surface/adjacent partner
elsewhere not yet identified.
Open items / not addressed here
investigation, not exhaustively tested beyond the repro model above.
ungated/pre-existing-crash are untouched by this branch.
distinct causes have been identified and precisely characterised (see
"Follow-up round: commit 8" above) but not yet fixed:
redesign (persisted pristine-original + ever-growing accumulator,
rather than each round's own restated "original") correctly fixed the
round-erosion arithmetic — verified against the two originally-targeted
NORTH SECTION lines — but exposed a deeper, pre-existing gap: bilateral
matching assumes any line both sides of a same-material pair draw is a
safe-to-remove duplicate internal seam, which is wrong when that line
is actually the combined shape's true external perimeter (confirmed
concretely: 3taVdKrmD45ehPoNLdDmdt flush with 3_TyYp2's exterior
face, 113.9 units of real boundary deleted at REFLECTED_PLAN_VIEW line
key ('h', 2714)). This predates the whole branch (present in the
original * Fix #3742: Remove coplanar boundary lines between adjacent same-mat… #7908 design) and needs 3D material-continuity reasoning, not
just 2D SVG line-key/interval data, to resolve safely. The redesign was
reverted rather than committed.
inner and outer, currently left unmerged by design (correctly
protecting a different, legitimate case — offset-wall corners) with no
way yet to distinguish the two.
at all (310.06823 199.15698 on 3_TyYp2), needing its own
investigation.