Add support for circles in profile editing

This commit is contained in:
Dion Moult
2022-09-27 17:16:26 +10:00
parent aff281d00b
commit 23212003e9
6 changed files with 241 additions and 123 deletions
@@ -20,6 +20,7 @@ import bpy
from . import operator, workspace
classes = (
operator.AddIfcCircle,
operator.CadArcFrom2Points,
operator.CadArcFrom3Points,
operator.CadFillet,
@@ -343,3 +343,35 @@ class CadArcFrom3Points(bpy.types.Operator):
bmesh.update_edit_mesh(mesh)
mesh.update()
return {"FINISHED"}
class AddIfcCircle(bpy.types.Operator):
bl_idname = "bim.add_ifccircle"
bl_label = "Add IfcCircle"
@classmethod
def poll(cls, context):
obj = context.active_object
return bool(obj) and obj.type == "MESH"
def execute(self, context):
obj = context.active_object
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(obj.data)
center = obj.matrix_world.inverted() @ context.scene.cursor.location
radius = 0.5
v1 = bm.verts.new((center[0], center[1] + radius, 0.0))
v2 = bm.verts.new((center[0], center[1] - radius, 0.0))
bm.verts.index_update()
indices = [v1.index, v2.index]
bm.edges.new((v1, v2))
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode="OBJECT")
group = obj.vertex_groups.new(name="IFCCIRCLE")
group.add(indices, 1, "REPLACE")
bpy.ops.object.mode_set(mode="EDIT")
return {"FINISHED"}
@@ -41,12 +41,13 @@ class CadTool(WorkSpaceTool):
("bim.cad_fillet", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
# Enable Mesh Tools add-on to get this amazing tool
("mesh.offset_edges", {"type": "O", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.cad_arc_from_2_points", {"type": "C", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.cad_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.cad_hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
)
def draw_settings(context, layout, tool):
if context.active_object.data.BIMMeshProperties.is_profile:
obj = context.active_object
if obj and obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.is_profile:
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
@@ -58,6 +59,11 @@ class CadTool(WorkSpaceTool):
row.label(text="", icon="EVENT_E")
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_C")
row.operator("bim.add_ifccircle", text="Circle")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_V")
@@ -105,6 +111,12 @@ class CadHotkey(bpy.types.Operator):
getattr(self, f"hotkey_{self.hotkey}")()
return {"FINISHED"}
def hotkey_S_C(self):
if self.is_profile():
bpy.ops.bim.add_ifccircle()
else:
bpy.ops.bim.cad_arc_from_2_points()
def hotkey_S_E(self):
bpy.ops.bim.cad_trim_extend()
@@ -115,7 +127,11 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_mitre()
def hotkey_S_V(self):
if bpy.context.active_object.data.BIMMeshProperties.is_profile:
if self.is_profile():
bpy.ops.bim.set_arc_index()
else:
bpy.ops.bim.cad_arc_from_3_points()
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
@@ -21,7 +21,7 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
from math import pi
from math import pi, pow
from mathutils import Vector, Matrix, geometry
@@ -113,34 +113,52 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
arc_groups = []
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
arc_groups.append(i)
groups["IFCARCINDEX"].append(i)
elif "IFCCIRCLE" in group.name:
groups["IFCCIRCLE"].append(i)
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.delete(bm, geom=bm.faces, context="FACES_ONLY")
# 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
# Sanity check
group_verts = {}
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
for vert in bm.verts:
if len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
total_groups = 0
for group_index in arc_groups:
if group_index in vert[deform_layer]:
group_verts.setdefault(group_index, []).append(vert)
total_groups += 0
if total_groups > 1: # A vert can only belong to one arc
return (False, "AMBIGUOUS_ARC")
for verts in group_verts.values():
if len(verts) != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
is_circle = False
for group_type, group_indices in groups.items():
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
print('it is a circle')
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
@@ -172,20 +190,31 @@ class Helper:
for loop in loops:
loop_vertices = []
total_edges = len(loop)
for i, edge in enumerate(loop):
if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts:
loop_vertices.append(edge.verts[0])
elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts:
loop_vertices.append(edge.verts[1])
elif edge.verts[0] in loop[i + 1].verts:
loop_vertices.append(edge.verts[1])
elif edge.verts[1] in loop[i + 1].verts:
loop_vertices.append(edge.verts[0])
loop_bm = bmesh.new()
for vert in loop_vertices:
loop_bm.verts.new(vert.co)
face = loop_bm.faces.new(loop_bm.verts)
if total_edges == 1:
loop_vertices = [loop[0].verts[0], loop[0].verts[1]]
else:
for i, edge in enumerate(loop):
if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts:
loop_vertices.append(edge.verts[0])
elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts:
loop_vertices.append(edge.verts[1])
elif edge.verts[0] in loop[i + 1].verts:
loop_vertices.append(edge.verts[1])
elif edge.verts[1] in loop[i + 1].verts:
loop_vertices.append(edge.verts[0])
if len(loop_vertices) == 2:
# Unofficial convention right now that a loop of 2 verts always is a circle
radius = (loop_vertices[0].co - loop_vertices[1].co).length / 2
face_area = pi * pow(radius, 2)
else:
loop_bm = bmesh.new()
for vert in loop_vertices:
loop_bm.verts.new(vert.co)
face = loop_bm.faces.new(loop_bm.verts)
face_area = face.calc_area()
loop_bm.free()
loop_vertex_indices = []
active_arc_id = None
@@ -203,7 +232,7 @@ class Helper:
active_arc_id = None
is_arc = False
for group_index in arc_groups:
for group_index in groups["IFCARCINDEX"]:
if group_index in v[deform_layer]:
if active_arc_id is not None and active_arc_id != group_index:
incomplete_arc = arc_stack
@@ -229,14 +258,11 @@ class Helper:
loop_vertex_indices.append(arc_stack)
loop_vertex_indices.append((loop_vertex_indices[-1][-1], loop_vertex_indices[0][0]))
# loop_vertex_indices = [v.index for v in loop_vertices]
face_area = face.calc_area()
if face_area > max_area:
max_area = face_area
outer_loop = loop_vertex_indices
inner_loops.append(loop_vertex_indices)
loop_bm.free()
inner_loops.remove(outer_loop)
@@ -58,7 +58,6 @@ def load_post(*args):
IfcStore.add_element_listener(opening.element_listener)
IfcStore.add_element_listener(wall.element_listener)
IfcStore.add_element_listener(slab.element_listener)
IfcStore.add_element_listener(profile.element_listener)
ifcopenshell.api.add_post_listener(
@@ -34,7 +34,7 @@ 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
from math import pi, degrees, sin, cos, radians
from mathutils import Vector, Matrix
from ifcopenshell.api.pset.data import Data as PsetData
from ifcopenshell.api.material.data import Data as MaterialData
@@ -43,71 +43,6 @@ from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertForma
from gpu_extras.batch import batch_for_shader
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in set(bpy.context.selected_objects + [bpy.context.active_object]):
if (
not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3":
return
if obj.mode == "EDIT":
IfcStore.edited_objs.add(obj)
ensure_solidify_modifier(obj)
else:
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
obj.data.transform(
Matrix.Translation(
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
)
)
obj.matrix_world.translation = new_origin
def ensure_solidify_modifier(obj):
modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"]
if modifier:
return modifier[0]
depth = obj.dimensions.z
if obj.mode == "EDIT":
bm = bmesh.from_edit_mesh(obj.data)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bm.faces.ensure_lookup_table()
non_bottom_faces = []
for face in bm.faces:
if face.normal.z > -0.9:
non_bottom_faces.append(face)
else:
face.normal_flip()
bmesh.ops.delete(bm, geom=non_bottom_faces, context="FACES")
if obj.mode == "EDIT":
bmesh.update_edit_mesh(obj.data)
else:
bm.to_mesh(obj.data)
bm.free()
modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY")
modifier.use_even_offset = True
modifier.offset = 1
modifier.thickness = depth
return modifier
def generate_footprint(usecase_path, ifc_file, settings):
footprint_context = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "FootPrint", "SKETCH_VIEW")
if not footprint_context:
@@ -566,24 +501,21 @@ class EnableEditingExtrusionProfile(bpy.types.Operator):
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = self.get_extrusion(body)
profile = extrusion.SweptArea
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
print('converted', position)
z_values = [v[2] for v in obj.bound_box]
element = tool.Ifc.get_entity(obj)
origin = obj.matrix_world @ Vector((0, 0, max(z_values)))
normal = obj.matrix_world.to_quaternion()
self.vertices = []
self.edges = []
self.arcs = []
self.circles = []
profile = extrusion.SweptArea
if profile.is_a("IfcArbitraryClosedProfileDef"):
self.process_curve(obj, position, profile.OuterCurve)
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
@@ -599,8 +531,12 @@ class EnableEditingExtrusionProfile(bpy.types.Operator):
group = obj.vertex_groups.new(name="IFCARCINDEX")
group.add(arc, 1, "REPLACE")
for circle in self.circles:
group = obj.vertex_groups.new(name="IFCCIRCLE")
group.add(circle, 1, "REPLACE")
bpy.ops.object.mode_set(mode="EDIT")
DecorationsHandler.install(bpy.context)
DecorationsHandler.install(context)
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
return {"FINISHED"}
@@ -625,6 +561,8 @@ class EnableEditingExtrusionProfile(bpy.types.Operator):
continue
global_point = position @ Vector(self.convert_unit_to_si(point.Coordinates)).to_3d()
self.vertices.append(global_point)
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False
if curve.Segments:
@@ -650,11 +588,26 @@ class EnableEditingExtrusionProfile(bpy.types.Operator):
global_point = position @ Vector(self.convert_unit_to_si(local_point)).to_3d()
self.vertices.append(global_point)
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset)
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcCircle"):
center = self.convert_unit_to_si(
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()
)
radius = self.convert_unit_to_si(curve.Radius)
self.vertices.extend(
[
position @ Vector((center[0], center[1] - curve.Radius, 0.0)),
position @ Vector((center[0], center[1] + curve.Radius, 0.0)),
]
)
self.circles.append([offset, offset + 1])
self.edges.append((offset, offset + 1))
def convert_unit_to_si(self, value):
return [v * self.unit_scale for v in value]
if isinstance(value, (tuple, list)):
return [v * self.unit_scale for v in value]
return value * self.unit_scale
class EditExtrusionProfile(bpy.types.Operator):
@@ -686,10 +639,10 @@ class EditExtrusionProfile(bpy.types.Operator):
bpy.ops.object.mode_set(mode="EDIT")
return {"CANCELLED"}
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
self.bm = bmesh.new()
self.bm.from_mesh(obj.data)
self.bm.verts.ensure_lookup_table()
self.bm.edges.ensure_lookup_table()
if indices["inner_curves"]:
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
@@ -706,7 +659,7 @@ class EditExtrusionProfile(bpy.types.Operator):
results.append(self.create_curve(position, inner_curve))
profile.InnerCurves = results
bm.free()
self.bm.free()
extrusion.SweptArea = profile
@@ -731,10 +684,19 @@ class EditExtrusionProfile(bpy.types.Operator):
def create_curve(self, position, edge_indices, points=None):
position_i = position.inverted()
if len(edge_indices) == 2:
diameter = edge_indices[0]
p1 = self.bm.verts[diameter[0]].co
p2 = self.bm.verts[diameter[1]].co
center = self.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
radius = self.convert_si_to_unit((p1 - p2).length / 2)
return tool.Ifc.get().createIfcCircle(
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center)), radius
)
if tool.Ifc.get().schema == "IFC2X3":
points = []
for edge in edge_indices:
local_point = (position_i @ Vector(bm.verts[edge[0]].co)).to_2d()
local_point = (position_i @ Vector(self.bm.verts[edge[0]].co)).to_2d()
points.append(tool.Ifc.get().createIfcCartesianPoint(self.convert_si_to_unit(local_point)))
points.append(points[0])
return tool.Ifc.get().createIfcPolyline(points)
@@ -757,7 +719,9 @@ class EditExtrusionProfile(bpy.types.Operator):
break
def convert_si_to_unit(self, value):
return [v / self.unit_scale for v in value]
if isinstance(value, (tuple, list)):
return [v / self.unit_scale for v in value]
return value / self.unit_scale
class SetArcIndex(bpy.types.Operator):
@@ -807,12 +771,13 @@ class DecorationsHandler:
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)
if not obj.data.is_editmode:
return
all_vertices = []
error_vertices = []
@@ -825,11 +790,15 @@ class DecorationsHandler:
special_edges = []
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)
@@ -852,6 +821,15 @@ class DecorationsHandler:
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:
@@ -913,6 +891,8 @@ class DecorationsHandler:
self.shader.uniform_float("color", green)
batch.draw(self.shader)
# Draw arcs
arc_centroids = []
arc_segments = []
for arc in arcs.values():
@@ -933,7 +913,7 @@ class DecorationsHandler:
centroid = tool.Cad.get_center_of_arc(points)
if centroid:
arc_centroids.append(tuple(centroid))
arc_segments.append(self.generate_3PT_mode_1(pts=points, num_verts=17, make_edges=True))
arc_segments.append(self.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))
@@ -944,10 +924,74 @@ class DecorationsHandler:
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 generate_3PT_mode_1 function is taken from Sverchok, licensed under GPL v2-or-later.
# 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
# https://github.com/nortikin/sverchok/blob/master/nodes/generator/basic_3pt_arc.py
# This function is taken from Sverchok's generate_3PT_mode_1 function, licensed under GPL v2-or-later.
# No functional modifications have been made.
def generate_3PT_mode_1(self, pts=None, num_verts=20, make_edges=False):
def create_arc_segments(self, pts=None, num_verts=20, make_edges=False):
"""
Arc from [start - through - end]
- call this function only if you have 3 pts,