Fix #2077. You can now edit profiles via their axis line.

This commit is contained in:
Dion Moult
2023-03-03 17:47:00 +11:00
parent 47192cc834
commit 08561b11c3
10 changed files with 511 additions and 289 deletions
@@ -48,7 +48,9 @@ class CadTool(WorkSpaceTool):
def draw_settings(context, layout, tool): def draw_settings(context, layout, tool):
obj = context.active_object obj = context.active_object
if obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile: if not obj or not obj.data:
return
if hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q") row.label(text="", icon="EVENT_Q")
@@ -100,6 +102,32 @@ class CadTool(WorkSpaceTool):
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_X") row.label(text="", icon="EVENT_X")
row.operator("bim.reset_vertex", text="Reset Vertex") row.operator("bim.reset_vertex", text="Reset Vertex")
elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
row.operator("bim.edit_extrusion_axis", text="Save Axis")
row.operator("bim.disable_editing_extrusion_axis", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_E")
row.operator("bim.cad_hotkey", text="Extend").hotkey = "S_E"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T")
row.operator("bim.cad_hotkey", text="Mitre").hotkey = "S_T"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_F")
row.operator("bim.cad_hotkey", text="Fillet").hotkey = "S_F"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
row.operator("bim.cad_hotkey", text="Offset").hotkey = "S_O"
else: else:
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
@@ -187,7 +215,10 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_Q(self): def hotkey_S_Q(self):
if tool.Ifc.get_entity(bpy.context.active_object): if tool.Ifc.get_entity(bpy.context.active_object):
bpy.ops.bim.edit_extrusion_profile() if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
bpy.ops.bim.edit_extrusion_profile()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
else: else:
bpy.ops.bim.edit_arbitrary_profile() bpy.ops.bim.edit_arbitrary_profile()
@@ -210,4 +241,9 @@ class CadHotkey(bpy.types.Operator):
def is_profile(self): def is_profile(self):
obj = bpy.context.active_object obj = bpy.context.active_object
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile return (
obj
and obj.data
and hasattr(obj.data, "BIMMeshProperties")
and obj.data.BIMMeshProperties.subshape_type == "PROFILE"
)
@@ -74,6 +74,9 @@ classes = (
opening.ShowOpenings, opening.ShowOpenings,
profile.ChangeCardinalPoint, profile.ChangeCardinalPoint,
profile.ChangeProfileDepth, profile.ChangeProfileDepth,
profile.DisableEditingExtrusionAxis,
profile.EditExtrusionAxis,
profile.EnableEditingExtrusionAxis,
profile.ExtendProfile, profile.ExtendProfile,
profile.RecalculateProfile, profile.RecalculateProfile,
profile.Rotate90, profile.Rotate90,
@@ -0,0 +1,269 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import gpu
import bgl
import bmesh
import blenderbim.tool as tool
from math import pi, degrees, sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
class ProfileDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def __call__(self, context):
obj = context.active_object
if obj.mode != "EDIT":
return
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
all_vertices = []
error_vertices = []
selected_vertices = []
unselected_vertices = []
special_vertices = []
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
special_edges = [] # edges that have a circle or an arc associated with them
arc_groups = []
circle_groups = []
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
arc_groups.append(i)
elif "IFCCIRCLE" in group.name:
circle_groups.append(i)
arcs = {}
circles = {}
bm = bmesh.from_edit_mesh(obj.data)
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
deform_layer = bm.verts.layers.deform.active
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
all_vertices.append(co)
if vertex.hide:
continue
is_arc = False
for group_index in arc_groups:
if group_index in vertex[deform_layer]:
is_arc = True
break
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle = False
for group_index in circle_groups:
if group_index in vertex[deform_layer]:
is_circle = True
break
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
if vertex.select:
selected_vertices.append(co)
else:
if len(vertex.link_edges) > 1 and is_circle:
error_vertices.append(co)
elif is_circle:
special_vertices.append(co)
elif len(vertex.link_edges) != 2:
error_vertices.append(co)
elif is_arc:
special_vertices.append(co)
else:
unselected_vertices.append(co)
for edge in bm.edges:
edge_indices = [v.index for v in edge.verts]
if edge.hide:
continue
if edge.select:
selected_edges.append(edge_indices)
else:
i1, i2 = edge.verts[0].index, edge.verts[1].index
if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None):
special_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
indices = [[v.index for v in e.verts] for e in bm.edges]
white = (1, 1, 1, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges)
self.shader.uniform_float("color", grey)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices})
self.shader.uniform_float("color", red)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices})
self.shader.uniform_float("color", blue)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
# Draw arcs
arc_centroids = []
arc_segments = []
for arc in arcs.values():
if len(arc) != 3:
continue
sorted_arc = [None, None, None]
for v1 in arc:
connections = 0
for link_edge in v1.link_edges:
v2 = link_edge.other_vert(v1)
if v2 in arc:
connections += 1
if connections == 2: # Midpoint
sorted_arc[1] = v1
else:
sorted_arc[2 if sorted_arc[2] is None else 0] = v1
points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc]
centroid = tool.Cad.get_center_of_arc(points)
if centroid:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in arc_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
# Draw circles
circle_centroids = []
circle_segments = []
for circle in circles.values():
if len(circle) != 2:
continue
p1 = obj.matrix_world @ circle[0].co
p2 = obj.matrix_world @ circle[1].co
radius = (p2 - p1).length / 2
centroid = p1.lerp(p2, 0.5)
circle_centroids.append(tuple(centroid))
segments = self.create_circle_segments(360, 20, radius)
matrix = obj.matrix_world.copy()
matrix.col[3] = centroid.to_4d()
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in circle_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
# https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py
# This function is taken from Sverchok, licensed under GPL v2-or-later.
# This is a combination of the make_verts and make_edges function.
def create_circle_segments(self, Angle, Vertices, Radius):
if Angle < 360:
theta = Angle / (Vertices - 1)
else:
theta = Angle / Vertices
listVertX = []
listVertY = []
for i in range(Vertices):
listVertX.append(Radius * cos(radians(theta * i)))
listVertY.append(Radius * sin(radians(theta * i)))
if Angle < 360 and self.mode_ == 0:
sigma = radians(Angle)
listVertX[-1] = Radius * cos(sigma)
listVertY[-1] = Radius * sin(sigma)
elif Angle < 360 and self.mode_ == 1:
listVertX.append(0.0)
listVertY.append(0.0)
points = list((x, y, 0) for x, y in zip(listVertX, listVertY))
listEdg = [(i, i + 1) for i in range(Vertices - 1)]
if Angle < 360 and self.mode_ == 1:
listEdg.append((0, Vertices))
listEdg.append((Vertices - 1, Vertices))
else:
listEdg.append((Vertices - 1, 0))
return points, listEdg
@@ -18,7 +18,6 @@
import bpy import bpy
import copy import copy
import math
import bmesh import bmesh
import mathutils.geometry import mathutils.geometry
import ifcopenshell import ifcopenshell
@@ -29,10 +28,10 @@ import blenderbim.bim.handler
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.core.type import blenderbim.core.type
import blenderbim.core.geometry import blenderbim.core.geometry
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees, inf from math import pi, degrees, inf
from mathutils import Vector, Matrix from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.decorator import ProfileDecorator
def element_listener(element, obj): def element_listener(element, obj):
@@ -48,12 +47,12 @@ def mode_callback(obj, data):
or not bpy.context.scene.BIMProjectProperties.is_authoring or not bpy.context.scene.BIMProjectProperties.is_authoring
): ):
return return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) product = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile": if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return return
if obj.mode == "EDIT": if obj.mode == "EDIT":
IfcStore.edited_objs.add(obj) tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data) bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data) bmesh.update_edit_mesh(obj.data)
@@ -97,8 +96,8 @@ class DumbProfileGenerator:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self): def generate(self):
self.file = IfcStore.get_file() self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type) material = ifcopenshell.util.element.get_material(self.relating_type)
if material and material.is_a("IfcMaterialProfileSet"): if material and material.is_a("IfcMaterialProfileSet"):
self.profile_set = material self.profile_set = material
@@ -328,7 +327,7 @@ class DumbProfileJoiner:
body = copy.deepcopy(axis1) body = copy.deepcopy(axis1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
si_length = unit_scale * length si_length = unit_scale * length
end = profile1.matrix_world @ Vector((si_length, 0, 0)) end = profile1.matrix_world @ Vector((0, 0, si_length))
axis[1] = end axis[1] = end
body[1] = end body[1] = end
self.recreate_profile(element1, profile1, axis, body) self.recreate_profile(element1, profile1, axis, body)
@@ -500,9 +499,7 @@ class DumbProfileJoiner:
# Openings should move with the host overall ... # Openings should move with the host overall ...
# ... except their position should stay the same along the local Z axis of the wall # ... except their position should stay the same along the local Z axis of the wall
for opening in [r.RelatedOpeningElement for r in element.HasOpenings]: for opening in [r.RelatedOpeningElement for r in element.HasOpenings]:
percent = tool.Cad.edge_percent( percent = tool.Cad.edge_percent(self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1)))))
self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1))))
)
is_z_offset_increased = True if percent < 0 else False is_z_offset_increased = True if percent < 0 else False
change_in_z = (self.body[0] - previous_origin).length / self.unit_scale change_in_z = (self.body[0] - previous_origin).length / self.unit_scale
@@ -913,3 +910,121 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.view_layer.update() bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(objs) DumbProfileRecalculator().recalculate(objs)
return {"FINISHED"} return {"FINISHED"}
class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_extrusion_axis"
bl_label = "Enable Editing Extrusion Axis"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj = context.active_object
element = tool.Ifc.get_entity(obj)
axis = ifcopenshell.util.representation.get_representation(element, "Model", "Axis", "GRAPH_VIEW")
if axis:
position = obj.matrix_world.copy()
tool.Model.import_axis(axis.Items[0], obj=obj)
else:
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(body)
if extrusion.Position:
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
position[0][3] *= self.unit_scale
position[1][3] *= self.unit_scale
position[2][3] *= self.unit_scale
else:
position = Matrix()
direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized()
tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context)
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
return {"FINISHED"}
class DisableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_extrusion_axis"
bl_label = "Disable Editing Extrusion Axis"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_extrusion_axis"
bl_label = "Edit Extrusion Axis"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
start = obj.matrix_world @ obj.data.vertices[0].co.copy()
end = obj.matrix_world @ obj.data.vertices[1].co.copy()
depth = (end - start).length
z_axis = (end - start).normalized()
y_axis = Vector((0, 0, 1))
x_axis = y_axis.cross(z_axis).normalized()
y_axis = z_axis.cross(x_axis).normalized()
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
matrix = Matrix(
(
[x_axis[0], y_axis[0], z_axis[0], start.x],
[x_axis[1], y_axis[1], z_axis[1], start.y],
[x_axis[2], y_axis[2], z_axis[2], start.z],
[0, 0, 0, 1],
)
)
obj.matrix_world = matrix
bpy.context.view_layer.update()
joiner = DumbProfileJoiner()
joiner.set_depth(obj, depth / self.unit_scale)
return {"FINISHED"}
@@ -16,30 +16,21 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>. # along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import blf
import bgl
import bpy import bpy
import math
import json import json
import bmesh import bmesh
import ifcopenshell import ifcopenshell
import ifcopenshell.util.type import ifcopenshell.util.type
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element import ifcopenshell.util.element
import mathutils.geometry
import blenderbim.bim.handler import blenderbim.bim.handler
import blenderbim.core.type import blenderbim.core.type
import blenderbim.core.geometry import blenderbim.core.geometry
import blenderbim.tool as tool import blenderbim.tool as tool
from bpy.types import SpaceView3D from math import radians
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees, sin, cos, radians
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.geometry.helper import Helper
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat from blenderbim.bim.module.model.decorator import ProfileDecorator
from gpu_extras.batch import batch_for_shader
def calculate_quantities(usecase_path, ifc_file, settings): def calculate_quantities(usecase_path, ifc_file, settings):
@@ -123,8 +114,8 @@ class DumbSlabGenerator:
self.relating_type = relating_type self.relating_type = relating_type
def generate(self): def generate(self):
self.file = IfcStore.get_file() self.file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
thicknesses = [] thicknesses = []
for rel in self.relating_type.HasAssociations: for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"): if rel.is_a("IfcRelAssociatesMaterial"):
@@ -268,7 +259,7 @@ class DumbSlabPlaner:
def change_thickness(self, element, thickness): def change_thickness(self, element, thickness):
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
obj = IfcStore.get_element(element.id()) obj = tool.Ifc.get_object(element)
if not obj: if not obj:
return return
@@ -573,7 +564,7 @@ class DisableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects return context.selected_objects
def _execute(self, context): def _execute(self, context):
DecorationsHandler.uninstall() ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object obj = context.active_object
@@ -620,7 +611,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position) tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context) ProfileDecorator.install(context)
if not bpy.app.background: if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
return {"FINISHED"} return {"FINISHED"}
@@ -633,7 +624,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
DecorationsHandler.uninstall() ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object obj = context.active_object
@@ -657,7 +648,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
self.layout.label(text="INVALID PROFILE") self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context) ProfileDecorator.install(context)
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
return return
@@ -749,245 +740,3 @@ class SetArcIndex(bpy.types.Operator):
group.add(selected_vertices, 1, "REPLACE") group.add(selected_vertices, 1, "REPLACE")
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"} return {"FINISHED"}
class DecorationsHandler:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def __call__(self, context):
obj = context.active_object
if obj.mode != "EDIT":
return
bgl.glLineWidth(2)
bgl.glPointSize(6)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
all_vertices = []
error_vertices = []
selected_vertices = []
unselected_vertices = []
special_vertices = []
special_vertex_indices = {}
selected_edges = []
unselected_edges = []
special_edges = [] # edges that have a circle or an arc associated with them
arc_groups = []
circle_groups = []
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
arc_groups.append(i)
elif "IFCCIRCLE" in group.name:
circle_groups.append(i)
arcs = {}
circles = {}
bm = bmesh.from_edit_mesh(obj.data)
# https://docs.blender.org/api/blender_python_api_2_63_8/bmesh.html#CustomDataAccess
# This is how we access vertex groups via bmesh, apparently, it's not very intuitive
deform_layer = bm.verts.layers.deform.active
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
all_vertices.append(co)
if vertex.hide:
continue
is_arc = False
for group_index in arc_groups:
if group_index in vertex[deform_layer]:
is_arc = True
break
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle = False
for group_index in circle_groups:
if group_index in vertex[deform_layer]:
is_circle = True
break
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
if vertex.select:
selected_vertices.append(co)
else:
if len(vertex.link_edges) > 1 and is_circle:
error_vertices.append(co)
elif is_circle:
special_vertices.append(co)
elif len(vertex.link_edges) != 2:
error_vertices.append(co)
elif is_arc:
special_vertices.append(co)
else:
unselected_vertices.append(co)
for edge in bm.edges:
edge_indices = [v.index for v in edge.verts]
if edge.hide:
continue
if edge.select:
selected_edges.append(edge_indices)
else:
i1, i2 = edge.verts[0].index, edge.verts[1].index
if i1 in special_vertex_indices and special_vertex_indices[i1] == special_vertex_indices.get(i2, None):
special_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
indices = [[v.index for v in e.verts] for e in bm.edges]
white = (1, 1, 1, 1)
green = (0.545, 0.863, 0, 1)
red = (1, 0.2, 0.322, 1)
blue = (0.157, 0.565, 1, 1)
grey = (0.2, 0.2, 0.2, 1)
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=unselected_edges)
self.shader.bind()
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=selected_edges)
self.shader.uniform_float("color", green)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "LINES", {"pos": all_vertices}, indices=special_edges)
self.shader.uniform_float("color", grey)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": unselected_vertices})
self.shader.uniform_float("color", white)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": error_vertices})
self.shader.uniform_float("color", red)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": special_vertices})
self.shader.uniform_float("color", blue)
batch.draw(self.shader)
batch = batch_for_shader(self.shader, "POINTS", {"pos": selected_vertices})
self.shader.uniform_float("color", green)
batch.draw(self.shader)
# Draw arcs
arc_centroids = []
arc_segments = []
for arc in arcs.values():
if len(arc) != 3:
continue
sorted_arc = [None, None, None]
for v1 in arc:
connections = 0
for link_edge in v1.link_edges:
v2 = link_edge.other_vert(v1)
if v2 in arc:
connections += 1
if connections == 2: # Midpoint
sorted_arc[1] = v1
else:
sorted_arc[2 if sorted_arc[2] is None else 0] = v1
points = [tuple(obj.matrix_world @ v.co) for v in sorted_arc]
centroid = tool.Cad.get_center_of_arc(points)
if centroid:
arc_centroids.append(tuple(centroid))
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
batch = batch_for_shader(self.shader, "POINTS", {"pos": arc_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in arc_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
# Draw circles
circle_centroids = []
circle_segments = []
for circle in circles.values():
if len(circle) != 2:
continue
p1 = obj.matrix_world @ circle[0].co
p2 = obj.matrix_world @ circle[1].co
radius = (p2 - p1).length / 2
centroid = p1.lerp(p2, 0.5)
circle_centroids.append(tuple(centroid))
segments = self.create_circle_segments(360, 20, radius)
matrix = obj.matrix_world.copy()
matrix.col[3] = centroid.to_4d()
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
circle_segments.append(segments)
batch = batch_for_shader(self.shader, "POINTS", {"pos": circle_centroids})
self.shader.uniform_float("color", (0.2, 0.2, 0.2, 1))
batch.draw(self.shader)
for verts, edges in circle_segments:
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
self.shader.uniform_float("color", (0.157, 0.565, 1, 1))
batch.draw(self.shader)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
# https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py
# This function is taken from Sverchok, licensed under GPL v2-or-later.
# This is a combination of the make_verts and make_edges function.
def create_circle_segments(self, Angle, Vertices, Radius):
if Angle < 360:
theta = Angle / (Vertices - 1)
else:
theta = Angle / Vertices
listVertX = []
listVertY = []
for i in range(Vertices):
listVertX.append(Radius * cos(radians(theta * i)))
listVertY.append(Radius * sin(radians(theta * i)))
if Angle < 360 and self.mode_ == 0:
sigma = radians(Angle)
listVertX[-1] = Radius * cos(sigma)
listVertY[-1] = Radius * sin(sigma)
elif Angle < 360 and self.mode_ == 1:
listVertX.append(0.0)
listVertY.append(0.0)
points = list((x, y, 0) for x, y in zip(listVertX, listVertY))
listEdg = [(i, i + 1) for i in range(Vertices - 1)]
if Angle < 360 and self.mode_ == 1:
listEdg.append((0, Vertices))
listEdg.append((Vertices - 1, Vertices))
else:
listEdg.append((Vertices - 1, 0))
return points, listEdg
@@ -83,17 +83,18 @@ class BimTool(WorkSpaceTool):
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}), ("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}), ("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("bim.hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}), ("bim.hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("bim.hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}), ("bim.hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}), ("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
("bim.hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}), ("bim.hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("bim.hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}), ("bim.hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}), ("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}), ("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}), ("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}), ("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}), ("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
) )
def draw_settings(context, layout, ws_tool): def draw_settings(context, layout, ws_tool):
@@ -253,6 +254,10 @@ class BimToolUI:
row.label(text="", icon="EVENT_E") row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E" row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E"
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_T") row.label(text="", icon="EVENT_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T" row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
@@ -607,6 +612,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return return
bpy.ops.bim.select_decomposition() bpy.ops.bim.select_decomposition()
def hotkey_A_E(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "PROFILE":
bpy.ops.bim.enable_editing_extrusion_axis()
def hotkey_A_O(self): def hotkey_A_O(self):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
return return
@@ -72,4 +72,4 @@ class ProfileData:
@classmethod @classmethod
def is_editing_arbitrary_profile(cls): def is_editing_arbitrary_profile(cls):
obj = bpy.context.active_object obj = bpy.context.active_object
return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile return obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "PROFILE"
@@ -21,7 +21,7 @@ import ifcopenshell.api
import blenderbim.bim.helper import blenderbim.bim.helper
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim.module.model.profile as model_profile import blenderbim.bim.module.model.profile as model_profile
from blenderbim.bim.module.model.slab import DecorationsHandler from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.profile.prop import generate_thumbnail_for_active_profile from blenderbim.bim.module.profile.prop import generate_thumbnail_for_active_profile
@@ -138,7 +138,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.scene.collection.objects.link(obj) bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context) ProfileDecorator.install(context)
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool") bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
@@ -149,8 +149,8 @@ class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
obj = context.active_object obj = context.active_object
if obj and obj.data and obj.data.BIMMeshProperties.is_profile: if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
DecorationsHandler.uninstall() ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
bpy.data.objects.remove(obj) bpy.data.objects.remove(obj)
@@ -166,7 +166,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object obj = context.active_object
DecorationsHandler.uninstall() ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
profile = tool.Model.export_profile(obj) profile = tool.Model.export_profile(obj)
@@ -176,7 +176,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
self.layout.label(text="INVALID PROFILE: " + indices[1]) self.layout.label(text="INVALID PROFILE: " + indices[1])
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context) ProfileDecorator.install(context)
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
return return
+1 -1
View File
@@ -421,7 +421,7 @@ class BIMMeshProperties(PropertyGroup):
is_native: BoolProperty(name="Is Native", default=False) is_native: BoolProperty(name="Is Native", default=False)
is_swept_solid: BoolProperty(name="Is Swept Solid") is_swept_solid: BoolProperty(name="Is Swept Solid")
is_parametric: BoolProperty(name="Is Parametric", default=False) is_parametric: BoolProperty(name="Is Parametric", default=False)
is_profile: BoolProperty(name="Is Profile", default=False) subshape_type: StringProperty(name="Subshape Type")
ifc_definition: StringProperty(name="IFC Definition") ifc_definition: StringProperty(name="IFC Definition")
ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
material_checksum: StringProperty(name="Material Checksum", default="[]") material_checksum: StringProperty(name="Material Checksum", default="[]")
+44 -5
View File
@@ -140,6 +140,38 @@ class Model(blenderbim.core.tool.Model):
else: else:
break break
@classmethod
def import_axis(cls, axis, obj=None, position=None):
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if position is None:
position = Matrix()
cls.vertices = []
cls.edges = []
cls.arcs = []
cls.circles = []
if isinstance(axis, list):
cls.vertices.extend([
position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(),
position @ Vector(cls.convert_unit_to_si(axis[1])).to_3d(),
])
cls.edges.append([0, 1])
else:
cls.import_curve(obj, position, axis)
mesh = bpy.data.meshes.new("Axis")
mesh.from_pydata(cls.vertices, cls.edges, [])
mesh.BIMMeshProperties.subshape_type = "AXIS"
if obj is None:
obj = bpy.data.objects.new("Axis", mesh)
else:
obj.data = mesh
return obj
@classmethod @classmethod
def import_profile(cls, profile, obj=None, position=None): def import_profile(cls, profile, obj=None, position=None):
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -162,7 +194,7 @@ class Model(blenderbim.core.tool.Model):
mesh = bpy.data.meshes.new("Profile") mesh = bpy.data.meshes.new("Profile")
mesh.from_pydata(cls.vertices, cls.edges, []) mesh.from_pydata(cls.vertices, cls.edges, [])
mesh.BIMMeshProperties.is_profile = True mesh.BIMMeshProperties.subshape_type = "PROFILE"
if obj is None: if obj is None:
obj = bpy.data.objects.new("Profile", mesh) obj = bpy.data.objects.new("Profile", mesh)
@@ -195,6 +227,7 @@ class Model(blenderbim.core.tool.Model):
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcIndexedPolyCurve"): elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False is_arc = False
is_closed = False
if curve.Segments: if curve.Segments:
for segment in curve.Segments: for segment in curve.Segments:
if len(segment[0]) == 3: # IfcArcIndex if len(segment[0]) == 3: # IfcArcIndex
@@ -213,15 +246,21 @@ class Model(blenderbim.core.tool.Model):
if is_arc: if is_arc:
cls.arcs[-1].append(len(cls.vertices) - 1) cls.arcs[-1].append(len(cls.vertices) - 1)
is_arc = False is_arc = False
if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]:
is_closed = True
else: else:
for local_point in curve.Points.CoordList: for local_point in curve.Points.CoordList:
global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d() global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d()
cls.vertices.append(global_point) cls.vertices.append(global_point)
# Curves without segments are cls closing
del cls.vertices[-1]
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices))]) if cls.vertices[offset] == cls.vertices[-1]:
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop is_closed = True
del cls.vertices[-1]
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)])
if is_closed:
cls.edges.append([len(cls.vertices) - 1, offset]) # Close the loop
elif curve.is_a("IfcCircle"): elif curve.is_a("IfcCircle"):
center = cls.convert_unit_to_si( center = cls.convert_unit_to_si(
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d() Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()