diff --git a/.github/workflows/ci-py-only.yml b/.github/workflows/ci-py-only.yml index b8b46bdb87..cded3ca5ff 100644 --- a/.github/workflows/ci-py-only.yml +++ b/.github/workflows/ci-py-only.yml @@ -88,7 +88,7 @@ jobs: sudo /usr/bin/python -m pip install lark sudo /usr/bin/python -m pip install networkx sudo /usr/bin/python -m pip install tabulate - sudo /usr/bin/python -m pip install dateutil + sudo /usr/bin/python -m pip install python-dateutil - name: Test run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed39f3984f..36b0952fe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: sudo /usr/bin/python -m pip install networkx sudo /usr/bin/python -m pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip sudo /usr/bin/python -m pip install tabulate - sudo /usr/bin/python -m pip install dateutil + sudo /usr/bin/python -m pip install python-dateutil - name: Test run: | diff --git a/src/blenderbim/blenderbim/bim/module/drawing/data.py b/src/blenderbim/blenderbim/bim/module/drawing/data.py index 5ce3aeab2b..e861a57c2e 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/data.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/data.py @@ -115,6 +115,7 @@ class DecoratorData: # stores 1 type of data per object data = {} + # used by Ifc Annotations with ObjectType = "BATTING" @classmethod def get_batting_thickness(cls, obj): result = cls.data.get(obj.name, None) @@ -128,6 +129,7 @@ class DecoratorData: cls.data[obj.name] = thickness return thickness + # used by Ifc Annotations with ObjectType = "SECTION" @classmethod def get_section_markers_display_data(cls, obj): result = cls.data.get(obj.name, None) @@ -169,6 +171,7 @@ class DecoratorData: cls.data[obj.name] = display_data return display_data + # used by Ifc Annotations with ObjectType = "TEXT" @classmethod def get_ifc_text_data(cls, obj): """returns font size in mm for current ifc text object""" @@ -214,3 +217,22 @@ class DecoratorData: text_data = {"Literals": literals_data, "FontSize": font_size} cls.data[obj.name] = text_data return text_data + + # used by Ifc Annotations with ObjectType = "DIMENSION" + @classmethod + def get_dimension_style(cls, obj): + result = cls.data.get(obj.name, None) + if result is not None: + return result + + element = tool.Ifc.get_entity(obj) + if not element or not element.is_a("IfcAnnotation") or element.ObjectType != "DIMENSION": + return None + + dimension_style = "arrow" + classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes") + if classes and "oblique" in classes.lower().split(): + dimension_style = "oblique" + + cls.data[obj.name] = dimension_style + return dimension_style diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index ad1a1b8a86..1f1d85f396 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -442,7 +442,7 @@ class BaseDecorator: box_alignment_offset += Vector((0, h)) else: # middle / center - box_alignment_offset += Vector((0, h / 2)) + box_alignment_offset += Vector((0, h / 2)) if "left" in box_alignment: pass @@ -451,7 +451,7 @@ class BaseDecorator: else: # middle / center box_alignment_offset += Vector((w / 2, 0)) - + pos -= rotation_matrix.transposed() @ box_alignment_offset else: if center: @@ -506,6 +506,10 @@ class DimensionDecorator(BaseDecorator): DEF_GLSL = ( BaseDecorator.DEF_GLSL + """ + #define OBLIQUE_SYMBOL_ANGLE PI / 4.0 + #define OLBIQUE_SYMBOL_SIZE 10.0 + #define OBLIQUE_HEAD_VERTS 7 + #define ARROW_ANGLE PI / 12.0 #define ARROW_SIZE 16.0 """ @@ -514,10 +518,23 @@ class DimensionDecorator(BaseDecorator): GEOM_GLSL = """ uniform vec2 winsize; uniform float viewportDrawingScale; + uniform float use_oblique_style; layout(lines) in; layout(line_strip, max_vertices=MAX_POINTS) out; + void oblique_dimension_head(in vec4 dir, in float size, in float angle, out vec4 head[OBLIQUE_HEAD_VERTS]) { + float c = cos(angle), s = sin(angle); + vec4 ortho = vec4(-dir.y, dir.x, 0, 0); + head[0] = -dir * size*0.66; + head[1] = vec4(0); + head[2] = ortho * size; + head[3] = -ortho * size; + head[4] = vec4(0); + head[5] = vec4((mat2(c, +s, -s, c) * dir.xy) * size * 0.47, 0, 0); + head[6] = -head[5]; + } + void main() { vec4 clip2win = matCLIP2WIN(); vec4 win2clip = matWIN2CLIP(); @@ -529,49 +546,78 @@ class DimensionDecorator(BaseDecorator): vec4 p; - vec4 head[3]; - arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head); + if (use_oblique_style > 0) { + vec4 head[OBLIQUE_HEAD_VERTS]; + oblique_dimension_head(dir, viewportDrawingScale * OLBIQUE_SYMBOL_SIZE, OBLIQUE_SYMBOL_ANGLE, head); - // start edge arrow - gl_Position = p0; - EmitVertex(); - p = p0w + head[1]; - gl_Position = WIN2CLIP(p); - EmitVertex(); - p = p0w + head[2]; - gl_Position = WIN2CLIP(p); - EmitVertex(); - gl_Position = p0; - EmitVertex(); - EndPrimitive(); + // start edge arrow + for (int i = 0; i < OBLIQUE_HEAD_VERTS; i++) { + gl_Position = WIN2CLIP((p0w + head[i])); + EmitVertex(); + } + EndPrimitive(); - // end edge arrow - gl_Position = p1; - EmitVertex(); - p = p1w - head[1]; - gl_Position = WIN2CLIP(p); - EmitVertex(); - p = p1w - head[2]; - gl_Position = WIN2CLIP(p); - EmitVertex(); - gl_Position = p1; - EmitVertex(); - EndPrimitive(); + // end edge arrow + for (int i = 0; i < OBLIQUE_HEAD_VERTS; i++) { + gl_Position = WIN2CLIP((p1w - head[i])); + EmitVertex(); + } + EndPrimitive(); - // stem, with gaps for arrows - p = p0w + head[0]; - gl_Position = WIN2CLIP(p); - EmitVertex(); - p = p1w - head[0]; - gl_Position = WIN2CLIP(p); - EmitVertex(); - EndPrimitive(); + // stem + gl_Position = p0; + EmitVertex(); + gl_Position = p1; + EmitVertex(); + EndPrimitive(); + } else { + vec4 head[3]; + arrow_head(dir, viewportDrawingScale * ARROW_SIZE, ARROW_ANGLE, head); + + // start edge arrow + gl_Position = p0; + EmitVertex(); + p = p0w + head[1]; + gl_Position = WIN2CLIP(p); + EmitVertex(); + p = p0w + head[2]; + gl_Position = WIN2CLIP(p); + EmitVertex(); + gl_Position = p0; + EmitVertex(); + EndPrimitive(); + + // end edge arrow + gl_Position = p1; + EmitVertex(); + p = p1w - head[1]; + gl_Position = WIN2CLIP(p); + EmitVertex(); + p = p1w - head[2]; + gl_Position = WIN2CLIP(p); + EmitVertex(); + gl_Position = p1; + EmitVertex(); + EndPrimitive(); + + // stem, with gaps for arrows + p = p0w + head[0]; + gl_Position = WIN2CLIP(p); + EmitVertex(); + p = p1w - head[0]; + gl_Position = WIN2CLIP(p); + EmitVertex(); + EndPrimitive(); + } } """ def decorate(self, context, obj): verts, idxs, _ = self.get_path_geom(obj, topo=False) - self.draw_lines(context, obj, verts, idxs) + dimension_style = DecoratorData.get_dimension_style(obj) + self.draw_lines( + context, obj, verts, idxs, extra_float_kwargs={"use_oblique_style": float(dimension_style == "oblique")} + ) self.draw_labels(context, obj, verts, idxs) def draw_labels(self, context, obj, vertices, indices): @@ -806,7 +852,7 @@ class LeaderDecorator(BaseDecorator): props = obj.BIMTextProperties text_data = props.get_text_edited_data() if props.is_editing else DecoratorData.get_ifc_text_data(obj) - text = text_data["Literals"][0]['CurrentValue'] + text = text_data["Literals"][0]["CurrentValue"] self.draw_label(context, text, pos, dir, gap=0, center=False, vcenter=False) @@ -1332,18 +1378,22 @@ class PlanLevelDecorator(LevelDecorator): region = context.region region3d = context.region_data for verts in splines: - v0 = verts[0] - v1 = verts[1] - p0 = location_3d_to_region_2d(region, region3d, v0) - p1 = location_3d_to_region_2d(region, region3d, v1) + p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]] dir = p1 - p0 if dir.length < 1: continue + + if dir.x > 0: + box_alignment = "bottom-left" + else: + box_alignment = "bottom-right" + dir *= -1 + z = verts[-1].z / unit_scale z = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), z) z *= unit_scale text = "RL " + self.format_value(context, z) - self.draw_label(context, text, p0, dir, gap=8, center=False) + self.draw_label(context, text, p0, dir, gap=8, center=False, box_alignment=box_alignment) class SectionLevelDecorator(LevelDecorator): @@ -2094,7 +2144,7 @@ class TextDecorator(BaseDecorator): def matrix_only_rotation(m): return m.to_3x3().normalized().to_4x4() - text_dir = Vector((1,0)) + text_dir = Vector((1, 0)) text_dir_world = matrix_only_rotation(region3d.perspective_matrix.inverted()) @ text_dir.to_3d() text_dir_world_rotated = matrix_only_rotation(obj.matrix_world) @ text_dir_world text_dir = (matrix_only_rotation(region3d.perspective_matrix) @ text_dir_world_rotated).to_2d().normalized() diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index fd20499814..f151fe343e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -562,6 +562,7 @@ def disable_editing_extrusion_profile(context): element = tool.Ifc.get_entity(obj) body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + profile_mesh = obj.data blenderbim.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -571,6 +572,7 @@ def disable_editing_extrusion_profile(context): is_global=True, should_sync_changes_first=False, ) + tool.Geometry.delete_data(profile_mesh) return {"FINISHED"} @@ -663,6 +665,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) + profile_mesh = obj.data blenderbim.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -672,6 +675,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator): is_global=True, should_sync_changes_first=False, ) + bpy.data.meshes.remove(profile_mesh) footprint_context = ifcopenshell.util.representation.get_context( tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW" @@ -734,15 +738,20 @@ class SetArcIndex(bpy.types.Operator): return bool(obj) and obj.type == "MESH" def cancel_message(self, msg): - self.report({"WARNING"}, msg) + self.report({"ERROR"}, msg) return {"CANCELLED"} def execute(self, context): # NOTE: undo won't remove new verex group # because of jumping between modes obj = context.active_object + + bm = tool.Blender.get_bmesh_for_mesh(obj.data) + selected_vertices = [v.index for v in bm.verts if v.select] + if len(selected_vertices) != 3: + return self.cancel_message("Select 3 vertices.") + bpy.ops.object.mode_set(mode="OBJECT") - selected_vertices = [v.index for v in obj.data.vertices if v.select] in_group = any([bool(obj.data.vertices[v].groups) for v in selected_vertices]) for group in obj.vertex_groups: group.remove(selected_vertices) diff --git a/src/blenderbim/blenderbim/bim/module/profile/operator.py b/src/blenderbim/blenderbim/bim/module/profile/operator.py index b7f496a042..9b0f3ef397 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/operator.py +++ b/src/blenderbim/blenderbim/bim/module/profile/operator.py @@ -148,7 +148,9 @@ def disable_editing_arbitrary_profile(context): if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE": ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") + profile_mesh = obj.data bpy.data.objects.remove(obj) + bpy.data.meshes.remove(profile_mesh) # need to update profile manager ui # if this was called from decorator @@ -191,7 +193,10 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.object.mode_set(mode="EDIT") return + profile_mesh = obj.data bpy.data.objects.remove(obj) + bpy.data.meshes.remove(profile_mesh) + profile.ProfileType = old_profile.ProfileType profile.ProfileName = old_profile.ProfileName for inverse in tool.Ifc.get().get_inverse(old_profile): diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 3f74aef95b..640ccbb955 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -176,6 +176,11 @@ class Model(blenderbim.core.tool.Model): @classmethod def import_profile(cls, profile, obj=None, position=None): + """Creates new profile mesh and assigns it to `obj`, + if `obj` is `None` then new "Profile" object will be created. + + Need to make sure to remove temporary mesh/object after use to avoid orphan data. + """ cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if position is None: