mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
New smart profile editor for extruded elements inspired by CAD Sketcher
This commit is contained in:
@@ -37,6 +37,7 @@ class CadTool(WorkSpaceTool):
|
||||
bl_keymap = (
|
||||
("bim.cad_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
("bim.cad_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
|
||||
("bim.cad_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
|
||||
("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": []}),
|
||||
@@ -45,6 +46,12 @@ class CadTool(WorkSpaceTool):
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Q")
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Extend", icon="EVENT_E")
|
||||
@@ -91,3 +98,6 @@ class CadHotkey(bpy.types.Operator):
|
||||
|
||||
def hotkey_S_T(self):
|
||||
bpy.ops.bim.cad_mitre()
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
|
||||
@@ -130,9 +130,11 @@ class CostClassificationsData(ReferencesData):
|
||||
@classmethod
|
||||
def references(cls):
|
||||
results = []
|
||||
element = tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id)
|
||||
element = tool.Ifc.get().by_id(
|
||||
bpy.context.scene.BIMCostProperties.cost_items[
|
||||
bpy.context.scene.BIMCostProperties.active_cost_item_index
|
||||
].ifc_definition_id
|
||||
)
|
||||
if element:
|
||||
for reference in ifcopenshell.util.classification.get_references(element):
|
||||
data = reference.get_info()
|
||||
|
||||
@@ -79,19 +79,23 @@ class ConnectionsData:
|
||||
results = []
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
for rel in getattr(element, "ConnectedTo", []):
|
||||
results.append({
|
||||
"id": rel.id(),
|
||||
"is_relating": True,
|
||||
"Name": rel.RelatedElement.Name or "Unnamed",
|
||||
"ConnectionType": rel.RelatingConnectionType,
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"id": rel.id(),
|
||||
"is_relating": True,
|
||||
"Name": rel.RelatedElement.Name or "Unnamed",
|
||||
"ConnectionType": rel.RelatingConnectionType,
|
||||
}
|
||||
)
|
||||
for rel in getattr(element, "ConnectedFrom", []):
|
||||
results.append({
|
||||
"id": rel.id(),
|
||||
"is_relating": False,
|
||||
"Name": rel.RelatingElement.Name or "Unnamed",
|
||||
"ConnectionType": rel.RelatedConnectionType,
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"id": rel.id(),
|
||||
"is_relating": False,
|
||||
"Name": rel.RelatingElement.Name or "Unnamed",
|
||||
"ConnectionType": rel.RelatedConnectionType,
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -112,6 +112,142 @@ class Helper:
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
|
||||
arc_groups = []
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
arc_groups.append(i)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
|
||||
# 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 = {}
|
||||
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")
|
||||
|
||||
loop_edges = set(bm.edges)
|
||||
|
||||
# Create loops from edges
|
||||
loops = []
|
||||
while loop_edges:
|
||||
edge = loop_edges.pop()
|
||||
loop = [edge]
|
||||
has_found_connected_edge = True
|
||||
while has_found_connected_edge:
|
||||
has_found_connected_edge = False
|
||||
for edge in loop_edges.copy():
|
||||
edge_verts = set(edge.verts)
|
||||
if edge_verts & set(loop[0].verts):
|
||||
loop.insert(0, edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
elif edge_verts & set(loop[-1].verts):
|
||||
loop.append(edge)
|
||||
loop_edges.remove(edge)
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Determine outer loop
|
||||
max_area = 0
|
||||
outer_loop = None
|
||||
inner_loops = []
|
||||
|
||||
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)
|
||||
|
||||
loop_vertex_indices = []
|
||||
active_arc_id = None
|
||||
arc_stack = []
|
||||
last_index = len(loop_vertices) - 1
|
||||
|
||||
# It is possible to loop through loop_vertices halfway through an arc.
|
||||
# If that is the case, we store it in incomplete_arc
|
||||
incomplete_arc = []
|
||||
for i, v in enumerate(loop_vertices):
|
||||
if len(arc_stack) == 3:
|
||||
loop_vertex_indices.append(arc_stack)
|
||||
loop_vertex_indices.append((arc_stack[-1], v.index))
|
||||
arc_stack = []
|
||||
active_arc_id = None
|
||||
|
||||
is_arc = False
|
||||
for group_index in arc_groups:
|
||||
if group_index in v[deform_layer]:
|
||||
if active_arc_id is not None and active_arc_id != group_index:
|
||||
incomplete_arc = arc_stack
|
||||
arc_stack = []
|
||||
active_arc_id = group_index
|
||||
is_arc = True
|
||||
break
|
||||
|
||||
if i == last_index and not is_arc:
|
||||
continue
|
||||
|
||||
if is_arc:
|
||||
arc_stack.append(v.index)
|
||||
else:
|
||||
if active_arc_id is not None:
|
||||
incomplete_arc = arc_stack
|
||||
active_arc_id = None
|
||||
arc_stack = []
|
||||
loop_vertex_indices.append((v.index, loop_vertices[i + 1].index))
|
||||
|
||||
if active_arc_id is not None:
|
||||
arc_stack.extend(incomplete_arc)
|
||||
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)
|
||||
|
||||
points = [v.co for v in bm.verts]
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
return {"points": points, "profile": outer_loop, "inner_curves": inner_loops}
|
||||
|
||||
# An arbitrary closed profile with voids is similar to one without voids.
|
||||
# We start the same way with any ngon (no tri), but instead of being the entire
|
||||
# profile, it is only one of the possible faces that make up the end of our
|
||||
|
||||
@@ -40,8 +40,12 @@ classes = (
|
||||
profile.ExtendProfile,
|
||||
profile.RecalculateProfile,
|
||||
profile.Rotate90,
|
||||
slab.EnableEditingExtrusionProfile,
|
||||
slab.DisableEditingExtrusionProfile,
|
||||
slab.EditExtrusionProfile,
|
||||
slab.EditSketchExtrusionProfile,
|
||||
slab.EnableEditingExtrusionProfile,
|
||||
slab.EnableEditingSketchExtrusionProfile,
|
||||
slab.SetArcIndex,
|
||||
prop.BIMModelProperties,
|
||||
prop.ConstrTypeInfo,
|
||||
ui.BIM_PT_authoring,
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
# 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 bmesh
|
||||
import math
|
||||
@@ -28,12 +32,15 @@ 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
|
||||
from mathutils import Vector, Matrix
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
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
|
||||
|
||||
|
||||
def element_listener(element, obj):
|
||||
@@ -385,9 +392,9 @@ class DumbSlabPlaner:
|
||||
obj.location[2] -= delta_thickness
|
||||
|
||||
|
||||
class EnableEditingExtrusionProfile(bpy.types.Operator):
|
||||
class EnableEditingSketchExtrusionProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.xxx"
|
||||
bl_label = "Enable Editing Extrusion Profile"
|
||||
bl_label = "Enable Editing Sketch Extrusion Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
@@ -446,7 +453,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator):
|
||||
break
|
||||
|
||||
|
||||
class EditExtrusionProfile(bpy.types.Operator):
|
||||
class EditSketchExtrusionProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.xxe"
|
||||
bl_label = "Edit Extrusion Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -464,7 +471,6 @@ class EditExtrusionProfile(bpy.types.Operator):
|
||||
sketch = context.scene.sketcher.active_sketch
|
||||
converter = cad_sketcher.convertors.BezierConverter(context.scene, sketch)
|
||||
converter.run()
|
||||
print(converter.paths)
|
||||
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
for path in converter.paths:
|
||||
@@ -513,3 +519,458 @@ class EditExtrusionProfile(bpy.types.Operator):
|
||||
item = item.FirstOperand
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class DisableEditingExtrusionProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_extrusion_profile"
|
||||
bl_label = "Disable Editing Extrusion Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def execute(self, context):
|
||||
DecorationsHandler.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.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingExtrusionProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_extrusion_profile"
|
||||
bl_label = "Enable Editing Extrusion Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_objects
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
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())
|
||||
|
||||
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.process_curve(obj, position, profile.OuterCurve)
|
||||
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
for inner_curve in profile.InnerCurves:
|
||||
self.process_curve(obj, position, inner_curve)
|
||||
|
||||
mesh = bpy.data.meshes.new("Profile")
|
||||
mesh.from_pydata(self.vertices, self.edges, [])
|
||||
obj.data = mesh
|
||||
|
||||
for arc in self.arcs:
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
group.add(arc, 1, "REPLACE")
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
DecorationsHandler.install(bpy.context)
|
||||
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_extrusion(self, representation):
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
return item
|
||||
elif item.is_a("IfcBooleanClippingResult"):
|
||||
item = item.FirstOperand
|
||||
else:
|
||||
break
|
||||
|
||||
def process_curve(self, obj, position, curve):
|
||||
offset = len(self.vertices)
|
||||
|
||||
if curve.is_a("IfcPolyline"):
|
||||
total_points = len(curve.Points)
|
||||
last_index = len(curve.Points) - 1
|
||||
for i, point in enumerate(curve.Points):
|
||||
if i == last_index:
|
||||
continue
|
||||
global_point = position @ Vector(point.Coordinates).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
elif curve.is_a("IfcIndexedPolyCurve"):
|
||||
is_arc = False
|
||||
if curve.Segments:
|
||||
for segment in curve.Segments:
|
||||
if len(segment[0]) == 3: # IfcArcIndex
|
||||
is_arc = True
|
||||
global_point = position @ Vector(curve.Points.CoordList[segment[0][0] - 1]).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
global_point = position @ Vector(curve.Points.CoordList[segment[0][1] - 1]).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1])
|
||||
else:
|
||||
global_point = position @ Vector(curve.Points.CoordList[segment[0][0] - 1]).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
if is_arc:
|
||||
self.arcs[-1].append(len(self.vertices) - 1)
|
||||
is_arc = False
|
||||
else:
|
||||
for point in curve.PointsCoordList:
|
||||
global_point = position @ Vector(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)
|
||||
|
||||
|
||||
class EditExtrusionProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_extrusion_profile"
|
||||
bl_label = "Edit Extrusion Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
DecorationsHandler.uninstall()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
extrusion = self.get_extrusion(representation)
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
|
||||
helper = Helper(tool.Ifc.get())
|
||||
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
|
||||
if isinstance(indices, tuple) and indices[0] is False: # Ugly
|
||||
self.report({"ERROR"}, "INVALID PROFILE: " + indices[1])
|
||||
DecorationsHandler.install(context)
|
||||
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()
|
||||
|
||||
if indices["inner_curves"]:
|
||||
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
|
||||
else:
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
self.points = self.create_points(position, indices["points"])
|
||||
|
||||
profile.OuterCurve = self.create_curve(position, indices["profile"])
|
||||
if indices["inner_curves"]:
|
||||
results = []
|
||||
for inner_curve in indices["inner_curves"]:
|
||||
results.append(self.create_curve(position, inner_curve))
|
||||
profile.InnerCurves = results
|
||||
|
||||
bm.free()
|
||||
|
||||
extrusion.SweptArea = profile
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
enable_dynamic_voids=False,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def create_points(self, position, indices):
|
||||
position_i = position.inverted()
|
||||
points = []
|
||||
for point in indices:
|
||||
local_point = (position_i @ point).to_2d()
|
||||
points.append(list(local_point))
|
||||
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
|
||||
def create_curve(self, position, edge_indices, points=None):
|
||||
position_i = position.inverted()
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
points = []
|
||||
for edge in edge_indices:
|
||||
local_point = (position_i @ Vector(bm.verts[edge[0]].co)).to_2d()
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(local_point))
|
||||
points.append(points[0])
|
||||
return tool.Ifc.get().createIfcPolyline(points)
|
||||
segments = []
|
||||
for segment in edge_indices:
|
||||
if len(segment) == 2:
|
||||
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
|
||||
elif len(segment) == 3:
|
||||
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
|
||||
return tool.Ifc.get().createIfcIndexedPolyCurve(self.points, segments, False)
|
||||
|
||||
def get_extrusion(self, representation):
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
return item
|
||||
elif item.is_a("IfcBooleanClippingResult"):
|
||||
item = item.FirstOperand
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class SetArcIndex(bpy.types.Operator):
|
||||
bl_idname = "bim.set_arc_index"
|
||||
bl_label = "Set Arc Index"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return bool(obj) and obj.type == "MESH"
|
||||
|
||||
def cancel_message(self, msg):
|
||||
self.report({"WARNING"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
selected_vertices = [v.index for v in obj.data.vertices if v.select]
|
||||
for group in obj.vertex_groups:
|
||||
group.remove(selected_vertices)
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
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
|
||||
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 = []
|
||||
selected_vertices = []
|
||||
unselected_vertices = []
|
||||
special_vertices = []
|
||||
special_vertex_indices = {}
|
||||
selected_edges = []
|
||||
unselected_edges = []
|
||||
special_edges = []
|
||||
|
||||
arc_groups = []
|
||||
for i, group in enumerate(obj.vertex_groups):
|
||||
if "IFCARCINDEX" in group.name:
|
||||
arc_groups.append(i)
|
||||
|
||||
arcs = {}
|
||||
|
||||
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
|
||||
|
||||
if vertex.select:
|
||||
selected_vertices.append(co)
|
||||
else:
|
||||
if 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)
|
||||
|
||||
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(self.generate_3PT_mode_1(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)
|
||||
|
||||
# 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.
|
||||
# No functional modifications have been made.
|
||||
def generate_3PT_mode_1(self, pts=None, num_verts=20, make_edges=False):
|
||||
"""
|
||||
Arc from [start - through - end]
|
||||
- call this function only if you have 3 pts,
|
||||
- do your error checking before passing to it.
|
||||
"""
|
||||
num_verts -= 1
|
||||
verts, edges = [], []
|
||||
V = Vector
|
||||
|
||||
# construction
|
||||
v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2])
|
||||
edge1_mid = v1.lerp(v2, 0.5)
|
||||
edge2_mid = v3.lerp(v4, 0.5)
|
||||
axis = mathutils.geometry.normal(v1, v2, v4)
|
||||
mat_rot = mathutils.Matrix.Rotation(math.radians(90.0), 4, axis)
|
||||
|
||||
# triangle edges
|
||||
v1_ = ((v1 - edge1_mid) @ mat_rot) + edge1_mid
|
||||
v2_ = ((v2 - edge1_mid) @ mat_rot) + edge1_mid
|
||||
v3_ = ((v3 - edge2_mid) @ mat_rot) + edge2_mid
|
||||
v4_ = ((v4 - edge2_mid) @ mat_rot) + edge2_mid
|
||||
|
||||
r = mathutils.geometry.intersect_line_line(v1_, v2_, v3_, v4_)
|
||||
if r:
|
||||
# do arc
|
||||
p1, _ = r
|
||||
|
||||
# find arc angle.
|
||||
a = (v1 - p1).angle((v4 - p1), 0)
|
||||
s = (2 * math.pi) - a
|
||||
|
||||
interior_angle = (v1 - v2).angle(v4 - v3, 0)
|
||||
if interior_angle > 0.5 * math.pi:
|
||||
s = math.pi + 2 * (0.5 * math.pi - interior_angle)
|
||||
|
||||
for i in range(num_verts + 1):
|
||||
mat_rot = mathutils.Matrix.Rotation(((s / num_verts) * i), 4, axis)
|
||||
vec = ((v4 - p1) @ mat_rot) + p1
|
||||
verts.append(vec[:])
|
||||
else:
|
||||
# do straight line
|
||||
step_size = 1 / num_verts
|
||||
verts = [v1_.lerp(v4_, i * step_size)[:] for i in range(num_verts + 1)]
|
||||
|
||||
if make_edges:
|
||||
edges = [(n, n + 1) for n in range(len(verts) - 1)]
|
||||
|
||||
return verts, edges
|
||||
|
||||
@@ -179,6 +179,15 @@ class BimTool(WorkSpaceTool):
|
||||
row.label(text="", icon="EVENT_S")
|
||||
row.operator("bim.hotkey", text="Split").hotkey = "S_S"
|
||||
|
||||
if ifc_class == "IfcSlabType":
|
||||
if not context.active_object:
|
||||
pass
|
||||
elif context.active_object.mode == "OBJECT":
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Profile").hotkey = "S_E"
|
||||
|
||||
if ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"):
|
||||
row = layout.row(align=True)
|
||||
row.prop(data=props, property="cardinal_point", text="Axis")
|
||||
@@ -290,6 +299,11 @@ class Hotkey(bpy.types.Operator):
|
||||
return
|
||||
if self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.join_wall(join_type="T")
|
||||
elif self.props.ifc_class == "IfcSlabType":
|
||||
if not bpy.context.active_object:
|
||||
pass
|
||||
elif bpy.context.active_object.mode == "OBJECT":
|
||||
bpy.ops.bim.enable_editing_extrusion_profile()
|
||||
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
|
||||
bpy.ops.bim.extend_profile(join_type="T")
|
||||
|
||||
@@ -303,6 +317,24 @@ class Hotkey(bpy.types.Operator):
|
||||
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
|
||||
bpy.ops.bim.recalculate_profile()
|
||||
|
||||
def hotkey_S_M(self):
|
||||
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.merge_wall()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if not self.has_ifc_class:
|
||||
return
|
||||
if self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.rotate_90(axis="Z")
|
||||
elif self.props.ifc_class == "IfcColumnType":
|
||||
bpy.ops.bim.rotate_90(axis="Z")
|
||||
elif self.props.ifc_class in ["IfcBeamType", "IfcMemberType"]:
|
||||
bpy.ops.bim.rotate_90(axis="Y")
|
||||
|
||||
def hotkey_S_S(self):
|
||||
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.split_wall()
|
||||
|
||||
def hotkey_S_T(self):
|
||||
if not self.has_ifc_class:
|
||||
return
|
||||
@@ -324,24 +356,6 @@ class Hotkey(bpy.types.Operator):
|
||||
else:
|
||||
bpy.ops.bim.align_product(align_type="NEGATIVE")
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if not self.has_ifc_class:
|
||||
return
|
||||
if self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.rotate_90(axis="Z")
|
||||
elif self.props.ifc_class == "IfcColumnType":
|
||||
bpy.ops.bim.rotate_90(axis="Z")
|
||||
elif self.props.ifc_class in ["IfcBeamType", "IfcMemberType"]:
|
||||
bpy.ops.bim.rotate_90(axis="Y")
|
||||
|
||||
def hotkey_S_S(self):
|
||||
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.split_wall()
|
||||
|
||||
def hotkey_S_M(self):
|
||||
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.merge_wall()
|
||||
|
||||
def hotkey_S_Y(self):
|
||||
if self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.join_wall(join_type="V")
|
||||
|
||||
@@ -21,6 +21,7 @@ from bpy_extras.io_utils import ImportHelper
|
||||
import blenderbim.core.resource as core
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class LoadResources(bpy.types.Operator):
|
||||
bl_idname = "bim.load_resources"
|
||||
bl_label = "Load Resources"
|
||||
|
||||
@@ -114,11 +114,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
should_play_chaching_sound: BoolProperty(
|
||||
name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False
|
||||
)
|
||||
lock_grids_on_import: BoolProperty(
|
||||
name="Will lock grids upon import", default=True
|
||||
)
|
||||
|
||||
|
||||
lock_grids_on_import: BoolProperty(name="Will lock grids upon import", default=True)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -356,8 +356,8 @@ class Cad:
|
||||
return bm
|
||||
|
||||
@classmethod
|
||||
def get_center_of_arc(cls, pts, obj):
|
||||
mw = obj.matrix_world
|
||||
def get_center_of_arc(cls, pts, obj=None):
|
||||
mw = obj.matrix_world if obj else None
|
||||
V = Vector
|
||||
|
||||
# construction
|
||||
@@ -376,7 +376,7 @@ class Cad:
|
||||
r = geometry.intersect_line_line(v1_, v2_, v3_, v4_)
|
||||
if r:
|
||||
p1, _ = r
|
||||
cp = mw @ p1
|
||||
cp = mw @ p1 if mw else p1
|
||||
return cp
|
||||
else:
|
||||
print("not on a circle")
|
||||
|
||||
Reference in New Issue
Block a user