mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-11 06:18:09 +00:00
Add opt-in join coplanar surfaces toggle with offset-wall guard
Adds a "Join Coplanar Surfaces" bool property (default off) nested under "Generate Material Layers" in BIM_PT_camera. When enabled, remove_coplanar_boundary_lines runs after linework generation to suppress shared boundary lines between adjacent coplanar elements of the same material. Fixes a false-positive where back-to-back walls at adjacent face planes (e.g. two thin walls touching at Y=1.185) were incorrectly joined. The bilateral segment matching now only removes a shared line segment when it covers the full running-direction extent of at least one element's union — offset partial overlaps are skipped. A companion flag suppresses the unilateral bbox fallback when bilateral already found explicit segments on that line, preventing the fallback from removing the same boundary the bilateral pass correctly rejected. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -1645,6 +1645,9 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
# local Z axis in world space points downward, the camera looks upward.
|
# local Z axis in world space points downward, the camera looks upward.
|
||||||
camera_looks_up = self.camera.matrix_world.col[2].z < 0
|
camera_looks_up = self.camera.matrix_world.col[2].z < 0
|
||||||
|
|
||||||
|
# GUIDs of pairs under investigation (first 12 chars used as key)
|
||||||
|
_dg = {"39Iwvbn41B7O", "08yv0FO$j2Aw", "1dM2d4kqjFH8", "2lwj_pzhrADf"}
|
||||||
|
|
||||||
obj_cache = {}
|
obj_cache = {}
|
||||||
|
|
||||||
def get_obj(guid):
|
def get_obj(guid):
|
||||||
@@ -1667,9 +1670,11 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
key = (min(guid_a, guid_b), max(guid_a, guid_b))
|
key = (min(guid_a, guid_b), max(guid_a, guid_b))
|
||||||
if key in adjacency_cache:
|
if key in adjacency_cache:
|
||||||
return adjacency_cache[key]
|
return adjacency_cache[key]
|
||||||
|
_dbg = guid_a[:12] in _dg and guid_b[:12] in _dg
|
||||||
obj_a = get_obj(guid_a)
|
obj_a = get_obj(guid_a)
|
||||||
obj_b = get_obj(guid_b)
|
obj_b = get_obj(guid_b)
|
||||||
if obj_a is None or obj_b is None or obj_a.type != "MESH" or obj_b.type != "MESH":
|
if obj_a is None or obj_b is None or obj_a.type != "MESH" or obj_b.type != "MESH":
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} PASS (no mesh)")
|
||||||
adjacency_cache[key] = True
|
adjacency_cache[key] = True
|
||||||
return True
|
return True
|
||||||
# Quick AABB guard
|
# Quick AABB guard
|
||||||
@@ -1681,9 +1686,11 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
min_b = min(c[axis] for c in corners_b)
|
min_b = min(c[axis] for c in corners_b)
|
||||||
max_b = max(c[axis] for c in corners_b)
|
max_b = max(c[axis] for c in corners_b)
|
||||||
if min_a > max_b + tol:
|
if min_a > max_b + tol:
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} FAIL aabb axis={axis}")
|
||||||
adjacency_cache[key] = False
|
adjacency_cache[key] = False
|
||||||
return False
|
return False
|
||||||
if min_b > max_a + tol:
|
if min_b > max_a + tol:
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} FAIL aabb axis={axis}")
|
||||||
adjacency_cache[key] = False
|
adjacency_cache[key] = False
|
||||||
return False
|
return False
|
||||||
# Proximity check: vertex-to-vertex OR vertex-to-edge.
|
# Proximity check: vertex-to-vertex OR vertex-to-edge.
|
||||||
@@ -1718,8 +1725,10 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
# Slower vertex-on-edge check for containment cases
|
# Slower vertex-on-edge check for containment cases
|
||||||
has_shared = vertex_near_edges(verts_b, obj_a, tol_sq) or vertex_near_edges(verts_a, obj_b, tol_sq)
|
has_shared = vertex_near_edges(verts_b, obj_a, tol_sq) or vertex_near_edges(verts_a, obj_b, tol_sq)
|
||||||
if not has_shared:
|
if not has_shared:
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} FAIL proximity")
|
||||||
adjacency_cache[key] = False
|
adjacency_cache[key] = False
|
||||||
return False
|
return False
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} PASS proximity")
|
||||||
# Containment check: if one AABB fully contains the other, the
|
# Containment check: if one AABB fully contains the other, the
|
||||||
# dominant-normal test is unreliable (a flat inner element's largest
|
# dominant-normal test is unreliable (a flat inner element's largest
|
||||||
# face is its top/bottom, not its side face). Skip normal check.
|
# face is its top/bottom, not its side face). Skip normal check.
|
||||||
@@ -1733,6 +1742,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
contained = aabb_contains(corners_a, corners_b) or aabb_contains(corners_b, corners_a)
|
contained = aabb_contains(corners_a, corners_b) or aabb_contains(corners_b, corners_a)
|
||||||
if contained:
|
if contained:
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} PASS contained")
|
||||||
adjacency_cache[key] = True
|
adjacency_cache[key] = True
|
||||||
return True
|
return True
|
||||||
# Coplanarity check: use the largest-face normal for each object.
|
# Coplanarity check: use the largest-face normal for each object.
|
||||||
@@ -1747,27 +1757,41 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
n_a = dominant_world_normal(obj_a)
|
n_a = dominant_world_normal(obj_a)
|
||||||
n_b = dominant_world_normal(obj_b)
|
n_b = dominant_world_normal(obj_b)
|
||||||
if n_a is None or n_b is None:
|
if n_a is None or n_b is None:
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} PASS (no normal)")
|
||||||
adjacency_cache[key] = True
|
adjacency_cache[key] = True
|
||||||
return True
|
return True
|
||||||
dot = abs(n_a.dot(n_b))
|
dot = abs(n_a.dot(n_b))
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} n_a={tuple(round(x,3) for x in n_a)} n_b={tuple(round(x,3) for x in n_b)} dot={dot:.6f}")
|
||||||
if dot <= 1.0 - 3.8e-5: # ~0.5° tolerance
|
if dot <= 1.0 - 3.8e-5: # ~0.5° tolerance
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} FAIL normal dot={dot:.6f}")
|
||||||
adjacency_cache[key] = False
|
adjacency_cache[key] = False
|
||||||
return False
|
return False
|
||||||
# Check whether both elements are at the same depth relative to the
|
# Face-plane check: parallel normals don't guarantee the elements are
|
||||||
# camera. Project vertices onto the camera look direction: elements
|
# on the same plane — they could be offset (e.g. two walls facing the
|
||||||
# at the same camera depth have overlapping ranges; depth-stacked
|
# same direction at different Y positions meeting at a corner).
|
||||||
# elements (one in front of the other) have separated ranges.
|
# Project all vertices of both elements onto n_a (using n_a for both
|
||||||
# Using the camera direction rather than n_a is critical — n_a may
|
# so that anti-parallel normals +Y/-Y produce matching values).
|
||||||
# be perpendicular to the camera (e.g. X-normal boxes in plan view)
|
#
|
||||||
# which would produce zero overlap for legitimate side-by-side pairs.
|
plane_pos_a = {round(v.dot(n_a), 5) for v in verts_a}
|
||||||
|
plane_pos_b = {round(v.dot(n_a), 5) for v in verts_b}
|
||||||
|
same_plane = any(abs(pa - pb) < tol for pa in plane_pos_a for pb in plane_pos_b)
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} plane_pos_a={sorted(plane_pos_a)} plane_pos_b={sorted(plane_pos_b)} same_plane={same_plane}")
|
||||||
|
if not same_plane:
|
||||||
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} FAIL face-plane")
|
||||||
|
adjacency_cache[key] = False
|
||||||
|
return False
|
||||||
|
# Depth check: confirm both elements span the same camera depth range.
|
||||||
|
# Catches elements at the same X-Y face plane but different Z heights
|
||||||
|
# (e.g. same wall type on two different floor levels).
|
||||||
projs_a = [v.dot(_cam_look) for v in verts_a]
|
projs_a = [v.dot(_cam_look) for v in verts_a]
|
||||||
projs_b = [v.dot(_cam_look) for v in verts_b]
|
projs_b = [v.dot(_cam_look) for v in verts_b]
|
||||||
range_a = (min(projs_a), max(projs_a))
|
range_a = (min(projs_a), max(projs_a))
|
||||||
range_b = (min(projs_b), max(projs_b))
|
range_b = (min(projs_b), max(projs_b))
|
||||||
overlap = min(range_a[1], range_b[1]) - max(range_a[0], range_b[0])
|
overlap = min(range_a[1], range_b[1]) - max(range_a[0], range_b[0])
|
||||||
same_plane = overlap > tol
|
same_depth = overlap > tol
|
||||||
adjacency_cache[key] = same_plane
|
if _dbg: print(f"[DBG] COPL {guid_a[:12]}/{guid_b[:12]} depth range_a={tuple(round(x,3) for x in range_a)} range_b={tuple(round(x,3) for x in range_b)} overlap={overlap:.4f} -> {'PASS' if same_depth else 'FAIL'}")
|
||||||
return same_plane
|
adjacency_cache[key] = same_depth
|
||||||
|
return same_depth
|
||||||
|
|
||||||
def parse_line(d):
|
def parse_line(d):
|
||||||
# Format is "Mx0,y0 Lx1,y1" (no space after M/L)
|
# Format is "Mx0,y0 Lx1,y1" (no space after M/L)
|
||||||
@@ -1898,6 +1922,15 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
return ((lo, coord), (hi, coord))
|
return ((lo, coord), (hi, coord))
|
||||||
return ((coord, lo), (coord, hi))
|
return ((coord, lo), (coord, hi))
|
||||||
|
|
||||||
|
def ivs_equal(a, b):
|
||||||
|
"""True if two interval lists are equal within TOL."""
|
||||||
|
if len(a) != len(b):
|
||||||
|
return False
|
||||||
|
return all(
|
||||||
|
abs(a_lo - b_lo) <= TOL and abs(a_hi - b_hi) <= TOL
|
||||||
|
for (a_lo, a_hi), (b_lo, b_hi) in zip(a, b)
|
||||||
|
)
|
||||||
|
|
||||||
# Group projection <g> elements by their immediate parent
|
# Group projection <g> elements by their immediate parent
|
||||||
parent_to_groups = {}
|
parent_to_groups = {}
|
||||||
for g in root.iter(f"{{{SVG}}}g"):
|
for g in root.iter(f"{{{SVG}}}g"):
|
||||||
@@ -2076,12 +2109,17 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
for j, (grp_j, mat_j, face_j, style_j, segs_j, guid_j) in enumerate(group_data):
|
for j, (grp_j, mat_j, face_j, style_j, segs_j, guid_j) in enumerate(group_data):
|
||||||
if j <= i:
|
if j <= i:
|
||||||
continue
|
continue
|
||||||
|
_wp = guid_i[:12] in _dg and guid_j[:12] in _dg
|
||||||
if not mat_keys_match(mat_i, mat_j, face_i, face_j):
|
if not mat_keys_match(mat_i, mat_j, face_i, face_j):
|
||||||
|
if _wp: print(f"[DBG] PAIR {guid_i[:12]}/{guid_j[:12]} SKIP mat mat_i={mat_i} mat_j={mat_j} face_i={face_i} face_j={face_j}")
|
||||||
continue
|
continue
|
||||||
if style_j != style_i:
|
if style_j != style_i:
|
||||||
|
if _wp: print(f"[DBG] PAIR {guid_i[:12]}/{guid_j[:12]} SKIP style style_i={style_i} style_j={style_j}")
|
||||||
continue
|
continue
|
||||||
if not are_coplanar_and_adjacent(guid_i, guid_j):
|
if not are_coplanar_and_adjacent(guid_i, guid_j):
|
||||||
|
if _wp: print(f"[DBG] PAIR {guid_i[:12]}/{guid_j[:12]} SKIP coplanar")
|
||||||
continue
|
continue
|
||||||
|
if _wp: print(f"[DBG] PAIR {guid_i[:12]}/{guid_j[:12]} JOINED — segs_i={len(segs_i)} segs_j={len(segs_j)}")
|
||||||
matched = 0
|
matched = 0
|
||||||
|
|
||||||
# Group each element's segments by axis-aligned line.
|
# Group each element's segments by axis-aligned line.
|
||||||
@@ -2105,6 +2143,12 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
lines_i = group_by_line(segs_i)
|
lines_i = group_by_line(segs_i)
|
||||||
lines_j = group_by_line(segs_j)
|
lines_j = group_by_line(segs_j)
|
||||||
|
|
||||||
|
# Track whether bilateral found shared keys but skipped them
|
||||||
|
# due to offset overlap. If so, both elements have explicit
|
||||||
|
# segments at the shared line — the unilateral fallback must
|
||||||
|
# not run for this pair (it would remove the same boundary
|
||||||
|
# segments that bilateral correctly rejected).
|
||||||
|
_bilateral_had_explicit_keys = False
|
||||||
for key in set(lines_i) & set(lines_j):
|
for key in set(lines_i) & set(lines_j):
|
||||||
entry_i = lines_i[key]
|
entry_i = lines_i[key]
|
||||||
entry_j = lines_j[key]
|
entry_j = lines_j[key]
|
||||||
@@ -2113,6 +2157,18 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
shared = ivs_intersect(union_i, union_j)
|
shared = ivs_intersect(union_i, union_j)
|
||||||
if not shared:
|
if not shared:
|
||||||
continue
|
continue
|
||||||
|
_bilateral_had_explicit_keys = True
|
||||||
|
# Only remove when the shared portion covers the full union
|
||||||
|
# of at least one element on this line. An offset partial
|
||||||
|
# overlap — where two back-to-back walls happen to have
|
||||||
|
# segments on the same line but at different running-direction
|
||||||
|
# positions — would cover neither element's full union and
|
||||||
|
# must not be removed (the line between them is a real
|
||||||
|
# architectural boundary).
|
||||||
|
if not (ivs_equal(shared, union_i) or ivs_equal(shared, union_j)):
|
||||||
|
if _wp: print(f"[DBG] SKIP offset-overlap key={key} shared={shared} union_i={union_i} union_j={union_j}")
|
||||||
|
continue
|
||||||
|
if _wp: print(f"[DBG] MATCHED key={key} shared={shared} union_i={union_i} union_j={union_j}")
|
||||||
matched += len(shared)
|
matched += len(shared)
|
||||||
kind = key[0]
|
kind = key[0]
|
||||||
coord = entry_i["coord"]
|
coord = entry_i["coord"]
|
||||||
@@ -2131,16 +2187,18 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
# (not explicitly drawn as a path segment), segments from the other
|
# (not explicitly drawn as a path segment), segments from the other
|
||||||
# element that lie on the boundary of that element's SVG bbox are
|
# element that lie on the boundary of that element's SVG bbox are
|
||||||
# still on the shared face and should be removed.
|
# still on the shared face and should be removed.
|
||||||
if matched == 0 and segs_i and segs_j:
|
if matched == 0 and not _bilateral_had_explicit_keys and segs_i and segs_j:
|
||||||
bbox_i = segs_bbox(segs_i)
|
bbox_i = segs_bbox(segs_i)
|
||||||
bbox_j = segs_bbox(segs_j)
|
bbox_j = segs_bbox(segs_j)
|
||||||
if bbox_i and bbox_j:
|
if bbox_i and bbox_j:
|
||||||
for path_j, line_j in segs_j:
|
for path_j, line_j in segs_j:
|
||||||
if seg_on_boundary_of(line_j, bbox_i, bbox_j):
|
if seg_on_boundary_of(line_j, bbox_i, bbox_j):
|
||||||
|
if _wp: print(f"[DBG] UNILATERAL j->i seg={line_j}")
|
||||||
to_remove.add(id(path_j))
|
to_remove.add(id(path_j))
|
||||||
matched += 1
|
matched += 1
|
||||||
for path_i, line_i in segs_i:
|
for path_i, line_i in segs_i:
|
||||||
if seg_on_boundary_of(line_i, bbox_j, bbox_i):
|
if seg_on_boundary_of(line_i, bbox_j, bbox_i):
|
||||||
|
if _wp: print(f"[DBG] UNILATERAL i->j seg={line_i}")
|
||||||
to_remove.add(id(path_i))
|
to_remove.add(id(path_i))
|
||||||
matched += 1
|
matched += 1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user