mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 02:23:34 +00:00
Bonsai: fix SHAPELY fill mode for linked projects in drawings (#7061)
Printing a drawing with SHAPELY "Fill Mode" produced no .surface fill paths for any element from a Linked Project. Two compounding bugs in CreateDrawing.generate_linework's fill pass: - Object resolution: it resolved Blender objects with tool.Ifc.get_object, which only maps entities of the active session file. Linked-model elements live in a separate ifcopenshell.file and are merged into GlobalId-keyed "chunk" objects, so get_object returned None and raycast_objs never included any linked geometry. - Matrix: linked geometry sits inside a collection instanced by an empty; Blender does not fold that instancing transform into the children's matrix_world, so raycasting with the raw matrix always missed (the ray is in host/world space, the geometry at the linked file's untransformed local coordinates). The reverse resolution of a raycast hit to an element via get_entity likewise only worked for the main file. Track the link per file; for linked elements resolve the object via tool.Project.Link.get_obj_by_guid and raycast against the effective matrix link_matrix @ obj.matrix_world (raycast_objs is now obj -> effective matrix); resolve a hit on a linked chunk via get_guid_by_face_index + get_element_by_guid. The main-file path is guarded by link is None / is_linked_element and is unchanged. Verified live in headless Blender with a host file linking three georeferenced buildings (distinct IfcMapConversion transforms): SHAPELY fill on the plan drawing produced 0 .surface paths before and 7 after, distributed across all three buildings' positions. Ordinary non-SHAPELY linked linework is unaffected. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -934,8 +934,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
# Map ifc_path → (ifc_file, link_matrix); main file has no link_matrix (None)
|
||||
files: dict[str, tuple[ifcopenshell.file, Optional[Matrix]]] = {bim_props.ifc_file: (tool.Ifc.get(), None)}
|
||||
# Map ifc_path → (ifc_file, link_matrix, link); main file has no link_matrix/link (None)
|
||||
files: dict[str, tuple[ifcopenshell.file, Optional[Matrix], Optional["Link"]]] = {
|
||||
bim_props.ifc_file: (tool.Ifc.get(), None, None)
|
||||
}
|
||||
|
||||
props = tool.Project.get_project_props()
|
||||
for link in props.get_loaded_links_for_drawings():
|
||||
@@ -943,7 +945,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
link_matrix = tool.Project.calculate_link_matrix(link)
|
||||
except Exception:
|
||||
link_matrix = None
|
||||
files[link.filepath] = (self.get_linked_file(link), link_matrix)
|
||||
files[link.filepath] = (self.get_linked_file(link), link_matrix, link)
|
||||
|
||||
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
|
||||
self.setup_serialiser(target_view)
|
||||
@@ -954,10 +956,21 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# Accumulated across every file in the loop below (main model plus any
|
||||
# linked models) so the SHAPELY fill pass after the loop covers all of
|
||||
# them, not just whichever file happened to be processed last.
|
||||
raycast_objs = set()
|
||||
#
|
||||
# Maps each raycastable object to its *effective* world matrix. Objects
|
||||
# from a linked model live inside a collection instanced by an empty
|
||||
# (see tool.Project.calculate_link_matrix / LoadLink.link_blend), and
|
||||
# Blender does not propagate that instancing transform onto the
|
||||
# children's own .matrix_world. Raycasting linked objects with their
|
||||
# raw .matrix_world therefore always misses (the ray, which is in the
|
||||
# correctly-positioned host/world space, never meets geometry still
|
||||
# sitting at the linked file's own un-transformed local coordinates),
|
||||
# so surface fills silently never render for linked models. Combining
|
||||
# the link matrix with the object's own matrix here fixes that.
|
||||
raycast_objs: dict[bpy.types.Object, Matrix] = {}
|
||||
elements_with_faces = set()
|
||||
|
||||
for ifc_path, (ifc, link_matrix) in files.items():
|
||||
for ifc_path, (ifc, link_matrix, link) in files.items():
|
||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||
# TODO: hash paths are never used
|
||||
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
|
||||
@@ -970,10 +983,21 @@ class CreateDrawing(bpy.types.Operator):
|
||||
for element in drawing_elements.copy():
|
||||
if element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(element)
|
||||
# tool.Ifc.get_object() only resolves objects that belong to the
|
||||
# active session file (tool.Ifc.get()). Elements from a linked
|
||||
# model live in their own standalone ifcopenshell.file (see
|
||||
# get_linked_file() above) and are merged into "chunk" objects
|
||||
# keyed by GlobalId (see tool.Project.Link), so they need to be
|
||||
# looked up through that separate mechanism instead.
|
||||
obj = (
|
||||
tool.Ifc.get_object(element)
|
||||
if link is None
|
||||
else tool.Project.Link.get_obj_by_guid(link, element.GlobalId)
|
||||
)
|
||||
if obj and obj.type == "MESH" and len(obj.data.polygons):
|
||||
elements_with_faces.add(element.GlobalId)
|
||||
raycast_objs.add(obj)
|
||||
effective_matrix = obj.matrix_world if link_matrix is None else link_matrix @ obj.matrix_world
|
||||
raycast_objs[obj] = effective_matrix
|
||||
|
||||
# Get all representation contexts to see what we're dealing with.
|
||||
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
|
||||
@@ -1087,7 +1111,17 @@ class CreateDrawing(bpy.types.Operator):
|
||||
raycast_results = self.cast_rays_and_get_best_object(raycast_objs, centroid3d, camera_dir)
|
||||
raycast_element = None
|
||||
if raycast_obj := raycast_results[0]:
|
||||
raycast_element = tool.Ifc.get_entity(raycast_obj)
|
||||
if tool.Project.Link.is_linked_element(raycast_obj):
|
||||
# A linked model's geometry is merged into "chunk"
|
||||
# objects covering multiple elements (see
|
||||
# tool.Project.Link), so the hit face needs to be
|
||||
# resolved to a GlobalId first.
|
||||
face_index = raycast_results[2]
|
||||
guid = tool.Project.Link.get_guid_by_face_index(raycast_obj, face_index)
|
||||
if guid:
|
||||
raycast_element = self.get_element_by_guid(guid)
|
||||
else:
|
||||
raycast_element = tool.Ifc.get_entity(raycast_obj)
|
||||
|
||||
if raycast_element:
|
||||
path = etree.Element("path")
|
||||
@@ -1632,7 +1666,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
return tuple(map(float, arr))
|
||||
|
||||
def cast_rays_and_get_best_object(
|
||||
self, objs_to_raycast: list[bpy.types.Object], ray_origin, ray_direction
|
||||
self, objs_to_raycast: dict[bpy.types.Object, Matrix], ray_origin, ray_direction
|
||||
) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]:
|
||||
# This could be optimised even further with 2D box culling
|
||||
best_length_squared = 1.0
|
||||
@@ -1640,8 +1674,12 @@ class CreateDrawing(bpy.types.Operator):
|
||||
best_hit = None
|
||||
best_face_index = None
|
||||
|
||||
for obj in objs_to_raycast:
|
||||
matrix_inv = obj.matrix_world.inverted()
|
||||
for obj, matrix_world in objs_to_raycast.items():
|
||||
# NB: matrix_world here is the object's *effective* world matrix
|
||||
# (see the raycast_objs comment in generate_linework), which for
|
||||
# objects belonging to a linked model already has the link's
|
||||
# transform folded in. It's deliberately not obj.matrix_world.
|
||||
matrix_inv = matrix_world.inverted()
|
||||
ray_origin_obj = matrix_inv @ ray_origin
|
||||
ray_direction_obj = ray_direction.to_4d()
|
||||
ray_direction_obj[3] = 0.0
|
||||
@@ -1650,7 +1688,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
|
||||
|
||||
if success:
|
||||
hit = obj.matrix_world @ location
|
||||
hit = matrix_world @ location
|
||||
length_squared = (hit - ray_origin).length_squared
|
||||
if best_obj is None or length_squared < best_length_squared:
|
||||
best_length_squared = length_squared
|
||||
|
||||
Reference in New Issue
Block a user