diff --git a/.gitignore b/.gitignore index ee3454b468..247d590e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,7 @@ src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py # apple -.DS_Store \ No newline at end of file +.DS_Store + +# Brickschema +src/blenderbim/blenderbim/bim/schema/Brick.ttl \ No newline at end of file diff --git a/src/bcf/src/bcf/v2/topic.py b/src/bcf/src/bcf/v2/topic.py index 87cea5744a..c1188c9a06 100644 --- a/src/bcf/src/bcf/v2/topic.py +++ b/src/bcf/src/bcf/v2/topic.py @@ -275,6 +275,7 @@ class TopicHandler: """ new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) self.add_visinfo_handler(new_viewpoint) + return new_viewpoint def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None: """Add a viewpoint pointing at an XYZ point in space @@ -287,6 +288,7 @@ class TopicHandler: position, *guids, xml_handler=self._xml_handler ) self.add_visinfo_handler(vi_handler) + return vi_handler def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None: self.viewpoints[new_viewpoint.guid + ".bcfv"] = new_viewpoint diff --git a/src/blenderbim/blenderbim/bim/module/bcf/__init__.py b/src/blenderbim/blenderbim/bim/module/bcf/__init__.py index 37b5f4f271..5e6b55e072 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/__init__.py @@ -57,6 +57,7 @@ classes = ( operator.SelectBcfBimSnippetReference, operator.SelectBcfDocumentReference, operator.SelectBcfHeaderFile, + operator.UnloadBcfProject, operator.ViewBcfTopic, prop.BcfReferenceLink, prop.BcfLabel, diff --git a/src/blenderbim/blenderbim/bim/module/bcf/operator.py b/src/blenderbim/blenderbim/bim/module/bcf/operator.py index f41d70851b..6b44e3cecc 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/operator.py @@ -58,7 +58,6 @@ class LoadBcfProject(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) def execute(self, context): - context.scene.BCFProperties.is_loaded = False if self.filepath: bcfstore.BcfStore.bcfxml = bcf.bcfxml.load(self.filepath) bcfxml = bcfstore.BcfStore.get_bcfxml() @@ -72,6 +71,17 @@ class LoadBcfProject(bpy.types.Operator): return {"RUNNING_MODAL"} +class UnloadBcfProject(bpy.types.Operator): + bl_idname = "bim.unload_bcf_project" + bl_label = "Unload BCF Project" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + bcfstore.BcfStore.set(None) + context.scene.BCFProperties.is_loaded = False + return {"FINISHED"} + + class LoadBcfTopics(bpy.types.Operator): bl_idname = "bim.load_bcf_topics" bl_label = "Load BCF Topics" diff --git a/src/blenderbim/blenderbim/bim/module/bcf/ui.py b/src/blenderbim/blenderbim/bim/module/bcf/ui.py index 9c4c5f796b..821d028dbb 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/ui.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/ui.py @@ -39,14 +39,16 @@ class BIM_PT_bcf(Panel): scene = context.scene props = scene.BCFProperties - row = layout.row(align=True) - row.operator("bim.new_bcf_project", text="New Project") - row.operator("bim.load_bcf_project", text="Load Project") if not props.is_loaded: + row = layout.row(align=True) + row.operator("bim.new_bcf_project", text="New Project") + row.operator("bim.load_bcf_project", text="Load Project") return - row.operator("bim.save_bcf_project", text="Save Project") + row = layout.row(align=True) + row.operator("bim.save_bcf_project", icon="EXPORT", text="Save Project") + row.operator("bim.unload_bcf_project", text="", icon="CANCEL") row = layout.row() row.prop(props, "name") diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index 5e0afa989a..744e679dab 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -33,6 +33,9 @@ classes = ( operator.RewindBrickClass, operator.ViewBrickClass, operator.ViewBrickItem, + operator.UndoBrick, + operator.RedoBrick, + operator.SerializeBrick, prop.Brick, prop.BIMBrickProperties, ui.BIM_PT_brickschema, diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index 072f9b2f70..bb8ba06089 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -110,7 +110,8 @@ class BrickschemaData: return [] results = [] for alias, uri in BrickStore.graph.namespaces(): - results.append((uri, f"{alias}: {uri}", "")) + # results.append((uri, f"{alias}: {uri}", "")) + results.append((uri, f"{alias}", "")) return results @classmethod diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 33d877ebd1..72ef846c87 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -189,3 +189,24 @@ class RemoveBrick(bpy.types.Operator, Operator): library=tool.Ifc.get().by_id(int(props.libraries)) if props.libraries else None, brick_uri=props.bricks[props.active_brick_index].uri, ) + +class UndoBrick(bpy.types.Operator, Operator): + bl_idname = "bim.undo_brick" + bl_label = "Undo Brick" + + def _execute(self, context): + core.undo_brick(tool.Brick) + +class RedoBrick(bpy.types.Operator, Operator): + bl_idname = "bim.redo_brick" + bl_label = "Redo Brick" + + def _execute(self, context): + core.redo_brick(tool.Brick) + +class SerializeBrick(bpy.types.Operator, Operator): + bl_idname = "bim.serialize_brick" + bl_label = "Serialize Brick" + + def _execute(self, context): + core.serialize_brick(tool.Brick) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 568c5a4ab5..717ee3eb12 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -58,6 +58,13 @@ class BIM_PT_brickschema(Panel): row.operator("bim.add_brick_feed", text="", icon="PLUGIN") row.operator("bim.remove_brick", text="", icon="X") + row = self.layout.row(align=True) + row.operator("bim.undo_brick", icon="LOOP_BACK") + row.operator("bim.redo_brick", icon="LOOP_FORWARDS") + + row = self.layout.row(align=True) + row.operator("bim.serialize_brick") + self.layout.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index") for attribute in BrickschemaData.data["attributes"]: diff --git a/src/blenderbim/blenderbim/bim/module/clash/operator.py b/src/blenderbim/blenderbim/bim/module/clash/operator.py index f224d40859..933cc06b48 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/operator.py +++ b/src/blenderbim/blenderbim/bim/module/clash/operator.py @@ -23,7 +23,7 @@ import bmesh import logging import numpy as np import ifcopenshell -from mathutils import Matrix +from mathutils import Matrix, Vector from math import radians from blenderbim.bim.ifc import IfcStore @@ -222,6 +222,7 @@ class ExecuteIfcClash(bpy.types.Operator): _, extension = os.path.splitext(self.filepath) if extension != ".json": self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf") + settings = ifcclash.ClashSettings() settings.output = self.filepath settings.logger = logging.getLogger("Clash") @@ -230,12 +231,28 @@ class ExecuteIfcClash(bpy.types.Operator): if context.scene.BIMClashProperties.should_create_clash_snapshots: - def get_viewpoint_snapshot(viewpoint, mat): + def get_viewpoint_snapshot(viewpoint): camera = bpy.data.objects.get("IFC Clash Camera") if not camera: camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera")) context.scene.collection.objects.link(camera) - camera.matrix_world = Matrix(mat) + + bcf_camera = viewpoint.visualization_info.perspective_camera + p = bcf_camera.camera_view_point + z = bcf_camera.camera_direction + z = Vector([z.x, z.y, z.z]) * -1 + y = bcf_camera.camera_up_vector + y = Vector([y.x, y.y, y.z]) + x = y.cross(z) + + mat = Matrix([ + [x[0], y[0], z[0], p.x], + [x[1], y[1], z[1], p.y], + [x[2], y[2], z[2], p.z], + [0, 0, 0, 0], + ]) + + camera.matrix_world = mat context.scene.camera = camera camera.data.angle = radians(60) area = next(area for area in context.screen.areas if area.type == "VIEW_3D") @@ -246,7 +263,8 @@ class ExecuteIfcClash(bpy.types.Operator): context.scene.render.image_settings.file_format = "PNG" context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png") bpy.ops.render.opengl(write_still=True) - return context.scene.render.filepath + with open(context.scene.render.filepath, "rb") as f: + return ("snapshot.png", f.read()) clasher.get_viewpoint_snapshot = get_viewpoint_snapshot diff --git a/src/blenderbim/blenderbim/bim/module/geometry/data.py b/src/blenderbim/blenderbim/bim/module/geometry/data.py index 0c95c61d2f..8b1849b200 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/data.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/data.py @@ -111,10 +111,13 @@ class ConnectionsData: for rel in connected_to: if element.is_a("IfcDistributionPort"): related_element = rel.RelatedPort - related_element_connection_type = "" else: related_element = rel.RelatedElement + + if element.is_a("IfcRelConnectsPathElements"): related_element_connection_type = rel.RelatedConnectionType + else: + related_element_connection_type = "" results.append( { @@ -128,10 +131,13 @@ class ConnectionsData: for rel in connected_from: if element.is_a("IfcDistributionPort"): relating_element = rel.RelatingPort - relating_element_connection_type = "" else: relating_element = rel.RelatingElement + + if element.is_a("IfcRelConnectsPathElements"): relating_element_connection_type = rel.RelatingConnectionType + else: + relating_element_connection_type = "" results.append( { diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index f938ea1e49..f85037b07b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -292,8 +292,6 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): offset: bpy.props.IntProperty() def _execute(self, context): - from PIL import Image, ImageDraw - if bpy.app.background: return @@ -310,106 +308,11 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): offset = 0 queue = queue[offset : offset + 9] - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - while queue: # if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation element = queue.pop() - obj = tool.Ifc.get_object(element) - - if not obj: - continue # Nothing to process - elif AuthoringData.type_thumbnails.get(element.id(), None): - continue # Already processed - elif obj.preview and obj.preview.icon_id: - AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id - continue - - if obj.data: - obj.asset_generate_preview() - while not obj.preview: - pass - else: - size = 128 - img = Image.new("RGBA", (size, size)) - draw = ImageDraw.Draw(img) - - material = ifcopenshell.util.element.get_material(element) - if material and material.is_a("IfcMaterialProfileSet"): - profile = material.MaterialProfiles[0].Profile - tool.Profile.draw_image_for_ifc_profile(draw, profile, size) - - elif material and material.is_a("IfcMaterialLayerSet"): - thicknesses = [l.LayerThickness for l in material.MaterialLayers] - total_thickness = sum(thicknesses) - si_total_thickness = total_thickness * unit_scale - if si_total_thickness <= 0.051: - width = 10 - elif si_total_thickness <= 0.11: - width = 20 - elif si_total_thickness <= 0.21: - width = 30 - elif si_total_thickness <= 0.31: - width = 40 - else: - width = 50 - - height = 100 - - is_horizontal = False - if element.is_a("IfcSlabType"): - is_horizontal = True - - parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") - if parametric: - layer_set_direction = parametric.get("LayerSetDirection", None) - if layer_set_direction == "AXIS2": - is_horizontal = False - elif layer_set_direction == "AXIS3": - is_horizontal = True - - if is_horizontal: - width, height = height, width - - x_offset = (size / 2) - (width / 2) - y_offset = (size / 2) - (height / 2) - draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=5) - current_thickness = 0 - del thicknesses[-1] - for thickness in thicknesses: - current_thickness += thickness - if element.is_a("IfcSlabType"): - y = (current_thickness / total_thickness) * height - line = [x_offset, y_offset + y, x_offset + width, y_offset + y] - else: - x = (current_thickness / total_thickness) * width - line = [x_offset + x, y_offset, x_offset + x, y_offset + height] - draw.line(line, fill="white", width=2) - elif False: - # TODO: things like parametric duct segments - pass - elif not element.RepresentationMaps: - # Empties are represented by a generic thumbnail - width = height = 100 - x_offset = (size / 2) - (width / 2) - y_offset = (size / 2) - (height / 2) - draw.line([x_offset, y_offset, width + x_offset, height + y_offset], fill="white", width=2) - draw.line([x_offset, y_offset + height, width + x_offset, y_offset], fill="white", width=2) - draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=5) - else: - draw.line([0, 0, size, size], fill="red", width=2) - draw.line([0, size, size, 0], fill="red", width=2) - - pixels = [item for sublist in img.getdata() for item in sublist] - - obj.asset_generate_preview() - while not obj.preview: - pass - - obj.preview.image_size = size, size - obj.preview.image_pixels_float = pixels - - queue.append(element) + if tool.Model.update_thumbnail_for_element(element): + queue.append(element) return {"FINISHED"} @@ -472,7 +375,6 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator): obj.matrix_world = newmat - def generate_box(usecase_path, ifc_file, settings): box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW") if not box_context: diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 113b192da4..6e4b4ec16c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -145,12 +145,21 @@ class DumbProfileRegenerator: objs = [] if not profile: return + + element_types = set() for element in self.get_elements_using_profile(profile): obj = tool.Ifc.get_object(element) if obj: objs.append(obj) + if element.is_a("IfcElementType"): + element_types.add(element) + DumbProfileRecalculator().recalculate(objs) + # update related thumbnails + for element in self.get_element_types_using_profile(profile): + tool.Model.update_thumbnail_for_element(element, refresh=True) + def regenerate_from_profile(self, usecase_path, ifc_file, settings): self.file = ifc_file objs = [] @@ -165,9 +174,10 @@ class DumbProfileRegenerator: def get_elements_using_profile(self, profile): results = [] - for profile_set in [ + profile_sets = [ mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile") - ]: + ] + for profile_set in profile_sets: for inverse in self.file.get_inverse(profile_set): if not inverse.is_a("IfcMaterialProfileSetUsage"): continue @@ -181,6 +191,18 @@ class DumbProfileRegenerator: results.extend(rel.RelatedObjects) return results + def get_element_types_using_profile(self, profile): + results = [] + profile_sets = [ + mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile") + ] + for profile_set in profile_sets: + for inverse in self.file.get_inverse(profile_set): + if not inverse.is_a("IfcRelAssociatesMaterial"): + continue + results.extend(inverse.RelatedObjects) + return results + def regenerate_from_type(self, usecase_path, ifc_file, settings): obj = tool.Ifc.get_object(settings["related_object"]) if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 7c2730f957..a57c097e25 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -164,7 +164,7 @@ class BIM_PT_project(Panel): op.should_save_as = False op = row.operator("export_ifc.bim", icon="FILE_TICK", text="Save As") op.should_save_as = True - row.operator("bim.unload_project", text="", icon="X") + row.operator("bim.unload_project", text="", icon="CANCEL") def draw_create_project_ui(self, context): props = context.scene.BIMProperties diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index d4df98654e..cd7fafaed0 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -109,3 +109,14 @@ def remove_brick(ifc, brick, library=None, brick_uri=None): ifc.run("library.remove_reference", reference=reference) brick.remove_brick(brick_uri) brick.run_refresh_brick_viewer() + +def undo_brick(brick): + brick.undo_brick() + brick.run_refresh_brick_viewer() + +def redo_brick(brick): + brick.redo_brick() + brick.run_refresh_brick_viewer() + +def serialize_brick(brick, file_name="BlenderBIMSerializeTest.ttl"): + brick.serialize_brick(file_name) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 61e498d861..b4539c64aa 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -25,6 +25,7 @@ import blenderbim.tool as tool try: import brickschema + import brickschema.persistent import urllib.parse from rdflib import Literal, URIRef, Namespace from rdflib.namespace import RDF @@ -32,14 +33,20 @@ except: # See #1860 print("Warning: brickschema not available.") +# silence known rdflib_sqlalchemy TypeError warning +# see https://github.com/BrickSchema/Brick/issues/513#issuecomment-1558493675 +import logging +logger = logging.getLogger("rdflib") +logger.setLevel(logging.ERROR) class Brick(blenderbim.core.tool.Brick): @classmethod def add_brick(cls, namespace, brick_class): ns = Namespace(namespace) brick = ns[ifcopenshell.guid.expand(ifcopenshell.guid.new())] - BrickStore.graph.add((brick, RDF.type, URIRef(brick_class))) - BrickStore.graph.add((brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Unnamed"))) + with BrickStore.graph.new_changeset("PROJECT") as cs: + cs.add((brick, RDF.type, URIRef(brick_class))) + cs.add((brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Unnamed"))) return str(brick) @classmethod @@ -100,7 +107,7 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def clear_project(cls): - BrickStore.graph = None + BrickStore.purge() bpy.context.scene.BIMBrickProperties.active_brick_class == "" bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() @@ -249,24 +256,24 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def load_brick_file(cls, filepath): - if not BrickStore.schema: - BrickStore.schema = brickschema.Graph() + if not BrickStore.schema: # important check for running under test cases cwd = os.path.dirname(os.path.realpath(__file__)) - schema_path = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl") - BrickStore.schema.load_file(schema_path) - BrickStore.graph = brickschema.Graph().load_file(filepath) + BrickStore.schema + BrickStore.schema = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + with BrickStore.graph.new_changeset("SCHEMA") as cs: + cs.load_file(BrickStore.schema) + with BrickStore.graph.new_changeset("PROJECT") as cs: + cs.load_file(filepath) BrickStore.path = filepath @classmethod def new_brick_file(cls): - if not BrickStore.schema: - BrickStore.schema = brickschema.Graph() - #BrickStore.schema = brickschema.persistent.VersionedGraphCollection("sqlite://") + if not BrickStore.schema: # important check for running under test cases cwd = os.path.dirname(os.path.realpath(__file__)) - schema_path = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl") - BrickStore.schema.load_file(schema_path) - #BrickStore.schema.load_graph(schema_path) - BrickStore.graph = brickschema.Graph() + BrickStore.schema + BrickStore.schema = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl") + BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") + with BrickStore.graph.new_changeset("SCHEMA") as cs: + cs.load_file(BrickStore.schema) BrickStore.graph.bind("digitaltwin", Namespace("https://example.org/digitaltwin#")) BrickStore.graph.bind("brick", Namespace("https://brickschema.org/schema/Brick#")) BrickStore.graph.bind("rdfs", Namespace("http://www.w3.org/2000/01/rdf-schema#")) @@ -281,8 +288,10 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def remove_brick(cls, brick_uri): - for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): - BrickStore.graph.remove(triple) + if(BrickStore.graph.triples((URIRef(brick_uri), None, None))): + with BrickStore.graph.new_changeset("PROJECT") as cs: + for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): + cs.remove(triple) @classmethod def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None): @@ -308,14 +317,39 @@ class Brick(blenderbim.core.tool.Brick): def set_active_brick_class(cls, brick_class): bpy.context.scene.BIMBrickProperties.active_brick_class = brick_class + @classmethod + def undo_brick(cls): + if(len(BrickStore.graph.versions()) > 1): + BrickStore.graph.undo() + + @classmethod + def redo_brick(cls): + with BrickStore.graph.conn() as conn: + redo_record = conn.execute( + "SELECT * from redos " "ORDER BY timestamp ASC LIMIT 1" + ).fetchone() + if redo_record is not None: + BrickStore.graph.redo() + + @classmethod + def serialize_brick(cls, file_name): + #temporary file path, could either be user selected for "save as" or use the BrickStore.path for simply "save" + cwd = os.path.dirname(os.path.realpath(__file__)) + dest = os.path.join(cwd, "..", "bim", "schema", file_name) + BrickStore.get_project().serialize(destination=dest, format="turtle") class BrickStore: - schema = None - graph = None - path = None - + schema = None # this is now a os path + path = None # file path if the project was loaded in + graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project" + # "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities + @staticmethod def purge(): BrickStore.schema = None BrickStore.graph = None - BrickStore.path = None + BrickStore.path = None + + @classmethod + def get_project(cls): + return BrickStore.graph.graph_at(graph="PROJECT") \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index e695ebafbb..1bb487728d 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -202,7 +202,6 @@ class Loader(blenderbim.core.tool.Loader): ifc_path = Path(tool.Ifc.get_path()) image_url = ifc_path.parent / image_url - # import pdb; pdb.set_trace() if not image_url.exists(): print(f"WARNING. Couldn't find texture by path {image_url}, it will be skipped.") continue diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index c9d8249a4d..c958e47fbe 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -29,6 +29,7 @@ from mathutils import Matrix, Vector from blenderbim.bim import import_ifc from blenderbim.bim.module.geometry.helper import Helper import collections +from blenderbim.bim.module.model.data import AuthoringData class Model(blenderbim.core.tool.Model): @@ -630,3 +631,101 @@ class Model(blenderbim.core.tool.Model): is_global=True, should_sync_changes_first=False, ) + + @classmethod + def update_thumbnail_for_element(cls, element, refresh=False): + if bpy.app.background: + return + + from PIL import Image, ImageDraw + + obj = tool.Ifc.get_object(element) + if not obj: + return # Nothing to process + + if not refresh and element.id() in AuthoringData.type_thumbnails: + return # Already processed + + obj.asset_generate_preview() + while not obj.preview: + pass + + # if object has .data we can use default blender .asset_generate_preview() + if not obj.data: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + size = 128 + img = Image.new("RGBA", (size, size)) + draw = ImageDraw.Draw(img) + + material = ifcopenshell.util.element.get_material(element) + if material and material.is_a("IfcMaterialProfileSet"): + profile = material.MaterialProfiles[0].Profile + tool.Profile.draw_image_for_ifc_profile(draw, profile, size) + + elif material and material.is_a("IfcMaterialLayerSet"): + thicknesses = [l.LayerThickness for l in material.MaterialLayers] + total_thickness = sum(thicknesses) + si_total_thickness = total_thickness * unit_scale + if si_total_thickness <= 0.051: + width = 10 + elif si_total_thickness <= 0.11: + width = 20 + elif si_total_thickness <= 0.21: + width = 30 + elif si_total_thickness <= 0.31: + width = 40 + else: + width = 50 + + height = 100 + + is_horizontal = False + if element.is_a("IfcSlabType"): + is_horizontal = True + + parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") + if parametric: + layer_set_direction = parametric.get("LayerSetDirection", None) + if layer_set_direction == "AXIS2": + is_horizontal = False + elif layer_set_direction == "AXIS3": + is_horizontal = True + + if is_horizontal: + width, height = height, width + + x_offset = (size / 2) - (width / 2) + y_offset = (size / 2) - (height / 2) + draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=5) + current_thickness = 0 + del thicknesses[-1] + for thickness in thicknesses: + current_thickness += thickness + if element.is_a("IfcSlabType"): + y = (current_thickness / total_thickness) * height + line = [x_offset, y_offset + y, x_offset + width, y_offset + y] + else: + x = (current_thickness / total_thickness) * width + line = [x_offset + x, y_offset, x_offset + x, y_offset + height] + draw.line(line, fill="white", width=2) + elif False: + # TODO: things like parametric duct segments + pass + elif not element.RepresentationMaps: + # Empties are represented by a generic thumbnail + width = height = 100 + x_offset = (size / 2) - (width / 2) + y_offset = (size / 2) - (height / 2) + draw.line([x_offset, y_offset, width + x_offset, height + y_offset], fill="white", width=2) + draw.line([x_offset, y_offset + height, width + x_offset, y_offset], fill="white", width=2) + draw.rectangle([x_offset, y_offset, width + x_offset, height + y_offset], outline="white", width=5) + else: + draw.line([0, 0, size, size], fill="red", width=2) + draw.line([0, size, size, 0], fill="red", width=2) + + pixels = [item for sublist in img.getdata() for item in sublist] + + obj.preview.image_size = size, size + obj.preview.image_pixels_float = pixels + + AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 816c762c28..8fcc687c2d 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -75,7 +75,8 @@ def an_empty_blender_session(): # default project settings bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProjectProperties.template_file = '0' + bpy.context.scene.BIMProjectProperties.template_file = "0" + bpy.context.preferences.addons["blenderbim"].preferences.should_play_chaching_sound = False @given("an empty IFC project") diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 1fc2a9b962..14687ce694 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -307,10 +307,8 @@ class TestImportBrickItems(NewFile): class TestLoadBrickFile(NewFile): def test_run(self): # We stub the schema to make tests run faster - BrickStore.schema = brickschema.Graph() cwd = os.path.dirname(os.path.realpath(__file__)) - schema_path = os.path.join(cwd, "..", "files", "BrickStub.ttl") - BrickStore.schema.load_file(schema_path) + BrickStore.schema = os.path.join(cwd, "..", "files", "BrickStub.ttl") # This is the actual test cwd = os.path.dirname(os.path.realpath(__file__)) @@ -322,10 +320,8 @@ class TestLoadBrickFile(NewFile): class TestNewBrickFile(NewFile): def test_run(self): # We stub the schema to make tests run faster - BrickStore.schema = brickschema.Graph() cwd = os.path.dirname(os.path.realpath(__file__)) - schema_path = os.path.join(cwd, "..", "files", "BrickStub.ttl") - BrickStore.schema.load_file(schema_path) + BrickStore.schema = os.path.join(cwd, "..", "files", "BrickStub.ttl") # This is the actual test subject.new_brick_file() diff --git a/src/ifcclash/ifcclash/ifcclash.py b/src/ifcclash/ifcclash/ifcclash.py index 1513e89c36..1406f40aee 100644 --- a/src/ifcclash/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash/ifcclash.py @@ -122,14 +122,20 @@ class Clasher: for clash in clash_set["clashes"].values(): title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}' topic = bcfxml.add_topic(title, title, "IfcClash") - topic.add_viewpoint_from_point_and_guids( + viewpoint = topic.add_viewpoint_from_point_and_guids( np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"], ) + snapshot = self.get_viewpoint_snapshot(viewpoint) + if snapshot: + topic.markup.viewpoints[0].snapshot = snapshot[0] + viewpoint.snapshot = snapshot[1] suffix = f".{i}" if i else "" bcfxml.save_project(f"{self.settings.output}{suffix}") - def get_viewpoint_snapshot(self, viewpoint, mat): - return None # Possible to overload this function in a GUI application if used as a library + def get_viewpoint_snapshot(self, viewpoint): + # Possible to overload this function in a GUI application if used as a library. + # Should return a tuple of (filename, bytes). + return None def export_json(self): clash_sets = self.clash_sets.copy() diff --git a/src/ifcdiff/ifcdiff.py b/src/ifcdiff/ifcdiff.py index e372c7b18c..4c61782a0f 100755 --- a/src/ifcdiff/ifcdiff.py +++ b/src/ifcdiff/ifcdiff.py @@ -29,9 +29,10 @@ import multiprocessing import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.element +import ifcopenshell.util.selector import ifcopenshell.util.placement import ifcopenshell.util.classification -import ifcopenshell.util.selector +import ifcopenshell.util.representation from deepdiff import DeepDiff @@ -141,8 +142,9 @@ class IfcDiff: continue if should_check_geometry: # Option 1: check everything heuristically using the iterator (seems faster) - potential_old_changes.append(old) - potential_new_changes.append(new) + if ifcopenshell.util.representation.get_representation(new, "Model", "Body", "MODEL_VIEW"): + potential_old_changes.append(old) + potential_new_changes.append(new) # Option 2: check first using Python, then fallback to iterator (twice as slow) # diff = self.diff_element_basic_geometry(old, new) # if diff: