Use object local stacking axis for camera-face layer detection

Replace the camera_looks_up flag (plan vs RCP) and dominant-face-
normal heuristic with a unified local-axis projection for all
LayerSetDirections:
  AXIS3 (slabs/roofs) → object local Z (col[2])
  AXIS2 (walls)       → object local Y (col[1])
  AXIS1 (rare walls)  → object local X (col[0])

The camera look vector is projected onto the object's world-space
stacking axis; a negative dot product means the camera is on the
positive (exterior) side of the layer sequence, a positive dot
means it is on the negative (interior) side.

This correctly handles sloped AXIS3 slabs viewed by two plan-type
cameras from opposite sides — the tilted local Z produces different
dot products for each camera direction, where the old camera_looks_up
flag returned the same result for both.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Ryan Schultz
2026-04-07 13:12:27 -05:00
parent 29dc58fb61
commit 16d2ec64b4
@@ -1940,17 +1940,27 @@ class CreateDrawing(bpy.types.Operator):
mats = ifcopenshell.util.element.get_materials(element) mats = ifcopenshell.util.element.get_materials(element)
return tuple(sorted(m.id() for m in mats)) if mats else () return tuple(sorted(m.id() for m in mats)) if mats else ()
# Camera look direction (unit vector pointing from camera into scene)
_cam_look = -self.camera.matrix_world.col[2].to_3d().normalized()
def get_camera_face_layer_id(guid): def get_camera_face_layer_id(guid):
"""Return the material ID of the layer the camera sees for this element. """Return the material ID of the layer the camera sees for this element.
For IfcMaterialLayerSetUsage elements with AXIS3 (vertical stacking For IfcMaterialLayerSetUsage elements the camera-facing layer depends
slabs, roofs), the visible face depends on camera direction: on the LayerSetDirection and which side of the element the camera is on:
- Plan view (camera looks down): the top layer is visible.
- RCP (camera looks up): the bottom layer is visible. AXIS3 (slabs/roofs — vertical stacking):
DirectionSense POSITIVE means layers stack upward (Layer[0] = bottom, Plan view (camera looks down) → top layer visible.
Layer[-1] = top). NEGATIVE means layers stack downward (Layer[0] = top). RCP (camera looks up) → bottom layer visible.
Returns None for walls (non-AXIS3) or elements without layer usage —
these fall back to the generic boundary-ends check. AXIS1/AXIS2 (walls — horizontal stacking):
The dominant face normal is computed for the element. If the camera
look direction is opposite to the face normal the camera sees the
face-normal side (the "positive" face). Otherwise it sees the
"negative" face (the interior side of the layer sequence).
DirectionSense POSITIVE: Layer[0] is the negative/interior face,
Layer[-1] is the positive/exterior face. NEGATIVE reverses this.
""" """
element = self.get_element_by_guid(guid) element = self.get_element_by_guid(guid)
if element is None: if element is None:
@@ -1958,8 +1968,6 @@ class CreateDrawing(bpy.types.Operator):
material = ifcopenshell.util.element.get_material(element) material = ifcopenshell.util.element.get_material(element)
if material is None or not material.is_a("IfcMaterialLayerSetUsage"): if material is None or not material.is_a("IfcMaterialLayerSetUsage"):
return None return None
if material.LayerSetDirection != "AXIS3":
return None
layer_set = material.ForLayerSet layer_set = material.ForLayerSet
if layer_set is None: if layer_set is None:
return None return None
@@ -1967,13 +1975,32 @@ class CreateDrawing(bpy.types.Operator):
if not layers: if not layers:
return None return None
positive = material.DirectionSense == "POSITIVE" positive = material.DirectionSense == "POSITIVE"
# POSITIVE: Layer[0]=bottom, Layer[-1]=top layer_dir = material.LayerSetDirection
# NEGATIVE: Layer[0]=top, Layer[-1]=bottom
if camera_looks_up: # For all LayerSetDirections, determine which side of the element
face_layer = layers[0] if positive else layers[-1] # the camera is on by projecting the camera look vector onto the
else: # object's local stacking axis in world space:
face_layer = layers[-1] if positive else layers[0] # AXIS3 → stacking along local Z (col[2]) — slabs/roofs
return face_layer.Material.id() # AXIS2 → stacking along local Y (col[1]) — most walls
# AXIS1 → stacking along local X (col[0]) — rare walls
# DirectionSense POSITIVE: Layer[-1] is on the +axis side.
# dot < 0 → camera look is opposite to stacking axis
# → camera is on the +axis (positive/exterior) side.
# This correctly handles sloped slabs seen by two plan-view
# cameras from opposite sides, unlike the simpler camera_looks_up
# flag which cannot distinguish them.
col_idx = {"AXIS1": 0, "AXIS2": 1, "AXIS3": 2}.get(layer_dir)
if col_idx is not None:
obj = get_obj(guid)
if obj is None or obj.type != "MESH":
return None
stack_axis = obj.matrix_world.col[col_idx].to_3d().normalized()
dot = _cam_look.dot(stack_axis)
on_positive_side = dot < 0
face_layer = (layers[-1] if positive else layers[0]) if on_positive_side else (layers[0] if positive else layers[-1])
return face_layer.Material.id()
return None
def mat_keys_match(a, b, face_a=None, face_b=None): def mat_keys_match(a, b, face_a=None, face_b=None):
"""True if two ordered layer-material tuples represent compatible assemblies. """True if two ordered layer-material tuples represent compatible assemblies.