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):
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.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
@@ -100,6 +102,32 @@ class CadTool(WorkSpaceTool):
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_X")
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:
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -187,7 +215,10 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_Q(self):
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:
bpy.ops.bim.edit_arbitrary_profile()
@@ -210,4 +241,9 @@ class CadHotkey(bpy.types.Operator):
def is_profile(self):
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,
profile.ChangeCardinalPoint,
profile.ChangeProfileDepth,
profile.DisableEditingExtrusionAxis,
profile.EditExtrusionAxis,
profile.EnableEditingExtrusionAxis,
profile.ExtendProfile,
profile.RecalculateProfile,
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 copy
import math
import bmesh
import mathutils.geometry
import ifcopenshell
@@ -29,10 +28,10 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.type
import blenderbim.core.geometry
from blenderbim.bim.ifc import IfcStore
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.model.decorator import ProfileDecorator
def element_listener(element, obj):
@@ -48,12 +47,12 @@ def mode_callback(obj, data):
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
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")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
return
if obj.mode == "EDIT":
IfcStore.edited_objs.add(obj)
tool.Ifc.edit(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
@@ -97,8 +96,8 @@ class DumbProfileGenerator:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self):
self.file = IfcStore.get_file()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
self.profile_set = material
@@ -328,7 +327,7 @@ class DumbProfileJoiner:
body = copy.deepcopy(axis1)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
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
body[1] = end
self.recreate_profile(element1, profile1, axis, body)
@@ -500,9 +499,7 @@ class DumbProfileJoiner:
# Openings should move with the host overall ...
# ... 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]:
percent = tool.Cad.edge_percent(
self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1))))
)
percent = tool.Cad.edge_percent(self.body[0], (previous_origin, (previous_matrix @ Vector((0, 0, 1)))))
is_z_offset_increased = True if percent < 0 else False
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()
DumbProfileRecalculator().recalculate(objs)
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
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import gpu
import blf
import bgl
import bpy
import math
import json
import bmesh
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import mathutils.geometry
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees, sin, cos, radians
from math import radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader
from blenderbim.bim.module.model.decorator import ProfileDecorator
def calculate_quantities(usecase_path, ifc_file, settings):
@@ -123,8 +114,8 @@ class DumbSlabGenerator:
self.relating_type = relating_type
def generate(self):
self.file = IfcStore.get_file()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
self.file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
thicknesses = []
for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"):
@@ -268,7 +259,7 @@ class DumbSlabPlaner:
def change_thickness(self, element, thickness):
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:
return
@@ -573,7 +564,7 @@ class DisableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
return context.selected_objects
def _execute(self, context):
DecorationsHandler.uninstall()
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="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)
bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context)
ProfileDecorator.install(context)
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
return {"FINISHED"}
@@ -633,7 +624,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
DecorationsHandler.uninstall()
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
@@ -657,7 +648,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context)
ProfileDecorator.install(context)
bpy.ops.object.mode_set(mode="EDIT")
return
@@ -749,245 +740,3 @@ class SetArcIndex(bpy.types.Operator):
group.add(selected_vertices, 1, "REPLACE")
bpy.ops.object.mode_set(mode="EDIT")
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": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
("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": "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": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
("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": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("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": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("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):
@@ -253,6 +254,10 @@ class BimToolUI:
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
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_T")
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
@@ -607,6 +612,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
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):
if not bpy.context.selected_objects:
return
@@ -72,4 +72,4 @@ class ProfileData:
@classmethod
def is_editing_arbitrary_profile(cls):
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.tool as tool
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
@@ -138,7 +138,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(context)
ProfileDecorator.install(context)
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):
obj = context.active_object
if obj and obj.data and obj.data.BIMMeshProperties.is_profile:
DecorationsHandler.uninstall()
if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
bpy.data.objects.remove(obj)
@@ -166,7 +166,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
DecorationsHandler.uninstall()
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
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])
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
DecorationsHandler.install(context)
ProfileDecorator.install(context)
bpy.ops.object.mode_set(mode="EDIT")
return
+1 -1
View File
@@ -421,7 +421,7 @@ class BIMMeshProperties(PropertyGroup):
is_native: BoolProperty(name="Is Native", default=False)
is_swept_solid: BoolProperty(name="Is Swept Solid")
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_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
material_checksum: StringProperty(name="Material Checksum", default="[]")
+44 -5
View File
@@ -140,6 +140,38 @@ class Model(blenderbim.core.tool.Model):
else:
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
def import_profile(cls, profile, obj=None, position=None):
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.from_pydata(cls.vertices, cls.edges, [])
mesh.BIMMeshProperties.is_profile = True
mesh.BIMMeshProperties.subshape_type = "PROFILE"
if obj is None:
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
elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False
is_closed = False
if curve.Segments:
for segment in curve.Segments:
if len(segment[0]) == 3: # IfcArcIndex
@@ -213,15 +246,21 @@ class Model(blenderbim.core.tool.Model):
if is_arc:
cls.arcs[-1].append(len(cls.vertices) - 1)
is_arc = False
if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]:
is_closed = True
else:
for local_point in curve.Points.CoordList:
global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d()
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))])
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
if cls.vertices[offset] == cls.vertices[-1]:
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"):
center = cls.convert_unit_to_si(
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()