mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 12:36:00 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51640ccc28 | |||
| aefbc134ea | |||
| 29e1d70334 | |||
| 9118e529e6 | |||
| 8ee0a1611d | |||
| 2af4388f40 | |||
| 93a2d18ea6 | |||
| fc49859ea4 | |||
| c0ac9cb380 | |||
| d5cc6c3c8e | |||
| 9b22b5000f | |||
| 3b69a4cc78 | |||
| adf54efcc5 | |||
| f6962eb58e | |||
| 11601d6532 | |||
| be2131d62c | |||
| 3ffbf46ac2 | |||
| 3943107eec | |||
| 94e3f75341 | |||
| 8bc8b950e7 | |||
| 05a95ac666 | |||
| f016c803e4 | |||
| bac0dd37fa | |||
| 75a37cf737 | |||
| bb7e9c4610 |
@@ -32,6 +32,7 @@ def refresh():
|
||||
AnnotationData.is_loaded = False
|
||||
DecoratorData.data = {}
|
||||
DecoratorData.cut_cache = {}
|
||||
DecoratorData.layerset_cache = {}
|
||||
|
||||
|
||||
class ProductAssignmentsData:
|
||||
@@ -155,6 +156,7 @@ class DecoratorData:
|
||||
# stores 1 type of data per object
|
||||
data = {}
|
||||
cut_cache = {}
|
||||
layerset_cache = {}
|
||||
|
||||
# used by Ifc Annotations with ObjectType = "BATTING"
|
||||
@classmethod
|
||||
|
||||
@@ -1993,6 +1993,8 @@ class CutDecorator:
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts:
|
||||
return verts, edges
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
@@ -2004,14 +2006,35 @@ class CutDecorator:
|
||||
return verts, edges
|
||||
|
||||
def slice_layersets(self, context, obj, cut_verts, cut_edges):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
imat = obj.matrix_world.inverted()
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if "LAYER" not in (tool.Model.get_usage_type(element) or ""):
|
||||
|
||||
# Currently selected objects shall not be cached as they may be being moved / edited.
|
||||
# If the camera is selected, we also disable the cache as the user may be moving the camera.
|
||||
if obj.select_get() or context.scene.camera.select_get():
|
||||
verts, edges = None, None
|
||||
else:
|
||||
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
|
||||
|
||||
if verts is False:
|
||||
return None, None
|
||||
elif verts is not None:
|
||||
return verts, edges
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
if tool.Model.get_usage_type(element) != "LAYER2":
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
layers = self.get_layer_data(element)
|
||||
|
||||
if not layers:
|
||||
DecoratorData.layerset_cache[element.id()] = (False, False)
|
||||
return None, None
|
||||
|
||||
minx = min([co[0] for co in obj.bound_box])
|
||||
maxx = max([co[0] for co in obj.bound_box])
|
||||
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
|
||||
@@ -2106,6 +2129,7 @@ class CutDecorator:
|
||||
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
|
||||
offset += len(linestring)
|
||||
|
||||
DecoratorData.layerset_cache[element.id()] = (verts, edges)
|
||||
return verts, edges
|
||||
|
||||
def bisect_mesh(self, obj, bm, camera):
|
||||
@@ -2140,6 +2164,10 @@ class CutDecorator:
|
||||
usage = ifcopenshell.util.element.get_material(element)
|
||||
offset = usage.OffsetFromReferenceLine * self.unit_scale
|
||||
layer_set = usage.ForLayerSet
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
return # No use slicing if there's only one layer
|
||||
|
||||
total_thickness = layer_set.TotalThickness
|
||||
half_thickness = total_thickness / 2
|
||||
min_layers = []
|
||||
@@ -2191,22 +2219,30 @@ class CutDecorator:
|
||||
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
|
||||
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
|
||||
for rel in wall.ConnectedTo:
|
||||
# How do you join to a non layered element? Not sure.
|
||||
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
|
||||
continue
|
||||
if rel.RelatingConnectionType == "ATPATH":
|
||||
connections["ATPATH"].append(
|
||||
self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
)
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
else:
|
||||
connections[rel.RelatingConnectionType] = self.get_connection_metadata(
|
||||
obj, rel.RelatedElement, centerline, min_edge, max_edge
|
||||
)
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections[rel.RelatingConnectionType] = metadata
|
||||
for rel in wall.ConnectedFrom:
|
||||
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
|
||||
continue
|
||||
# We only consider ATPATH since in this situation, we have the
|
||||
# priority. The non-priority wall never has any layers that need to
|
||||
# "turn a corner".
|
||||
if rel.RelatedConnectionType == "ATPATH":
|
||||
connections["ATPATH"].append(
|
||||
self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
|
||||
)
|
||||
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
|
||||
if not metadata:
|
||||
continue
|
||||
connections["ATPATH"].append(metadata)
|
||||
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
|
||||
for connection in connections["ATPATH"]:
|
||||
if connection["angle"] > 0:
|
||||
@@ -2216,9 +2252,10 @@ class CutDecorator:
|
||||
return connections
|
||||
|
||||
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
|
||||
imat = obj.matrix_world.inverted()
|
||||
rel_obj = tool.Ifc.get_object(rel_element)
|
||||
layers = self.get_layer_data(rel_element)
|
||||
if not layers:
|
||||
return
|
||||
minx = min([co[0] for co in rel_obj.bound_box])
|
||||
maxx = max([co[0] for co in rel_obj.bound_box])
|
||||
rel_centerline = [
|
||||
@@ -2227,7 +2264,11 @@ class CutDecorator:
|
||||
]
|
||||
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
|
||||
rel_centerline = [v.to_2d() for v in rel_centerline]
|
||||
intersection, _ = tool.Cad.intersect_edges(centerline, rel_centerline)
|
||||
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
|
||||
if intersection:
|
||||
intersection, _ = intersection
|
||||
else:
|
||||
return
|
||||
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
|
||||
if closest_centerline_point == rel_centerline[1]:
|
||||
rel_centerline = [rel_centerline[1], rel_centerline[0]]
|
||||
|
||||
@@ -829,6 +829,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
ifc = tool.Ifc.get()
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
|
||||
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
|
||||
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
|
||||
continue
|
||||
|
||||
classes = self.get_svg_classes(element)
|
||||
classes.append("cut")
|
||||
|
||||
@@ -865,7 +869,11 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if is_closed_polygon:
|
||||
el.getparent().remove(el)
|
||||
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
try:
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
|
||||
merged_polygons = polygons
|
||||
|
||||
if type(merged_polygons) == shapely.MultiPolygon:
|
||||
merged_polygons = merged_polygons.geoms
|
||||
|
||||
@@ -38,7 +38,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
face = None
|
||||
@@ -62,7 +62,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -90,7 +90,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -292,7 +292,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
@@ -389,7 +389,7 @@ class Helper:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
faces = bm.faces
|
||||
|
||||
@@ -88,7 +88,7 @@ class CommitChanges(bpy.types.Operator):
|
||||
repo
|
||||
and repo.head.is_detached
|
||||
and (
|
||||
not tool.is_valid_ref_format(props.new_branch_name)
|
||||
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
|
||||
or props.new_branch_name in [branch.name for branch in repo.branches]
|
||||
)
|
||||
):
|
||||
|
||||
@@ -96,6 +96,7 @@ classes = (
|
||||
slab.ResetVertex,
|
||||
slab.SetArcIndex,
|
||||
space.GenerateSpace,
|
||||
space.GenerateSpacesFromWalls,
|
||||
prop.BIMModelProperties,
|
||||
prop.BIMArrayProperties,
|
||||
prop.BIMStairProperties,
|
||||
|
||||
@@ -499,12 +499,12 @@ class BIMRailingProperties(PropertyGroup):
|
||||
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
|
||||
)
|
||||
cap_types = (
|
||||
("none", "none", ""),
|
||||
("TO_END_POST_AND_FLOOR", "TO_END_POST_AND_FLOOR", ""),
|
||||
("TO_END_POST", "TO_END_POST", ""),
|
||||
("TO_FLOOR", "TO_FLOOR", ""),
|
||||
("TO_WALL", "TO_WALL", ""),
|
||||
("180", "180", ""),
|
||||
("to_wall", "to_wall", ""),
|
||||
("to_floor", "to_floor", ""),
|
||||
("to_end_post", "to_end_post", ""),
|
||||
("to_end_post_and_floor", "to_end_post_and_floor", ""),
|
||||
("NONE", "NONE", ""),
|
||||
)
|
||||
|
||||
railing_added_previously: bpy.props.BoolProperty(default=False)
|
||||
|
||||
@@ -26,12 +26,20 @@ import blenderbim.tool as tool
|
||||
import blenderbim.core.type
|
||||
from math import pi
|
||||
from mathutils import Vector, Matrix
|
||||
from shapely import Polygon
|
||||
|
||||
|
||||
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_space"
|
||||
bl_label = "Generate Space"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
collection = context.view_layer.active_layer_collection.collection
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
return tool.Ifc.get_entity(collection_obj)
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
@@ -166,3 +174,157 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
||||
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.generate_spaces_from_walls"
|
||||
bl_label = "Generate Spaces From Walls"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Generate spaces from selected walls. The active object must be a wall."
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
active_obj = bpy.context.active_object
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
return context.selected_objects and element.is_a("IfcWall")
|
||||
|
||||
def _execute(self, context):
|
||||
# This only works based on a 2D plan only considering the standard
|
||||
# walls (i.e. prismatic) in the active object storey.
|
||||
# In order to run, the active objct must be a wall and
|
||||
# have to be selected walls
|
||||
props = context.scene.BIMModelProperties
|
||||
active_obj = bpy.context.active_object
|
||||
|
||||
if not active_obj:
|
||||
self.report({'ERROR'}, "No active object. Please select a wall")
|
||||
return
|
||||
|
||||
element = None
|
||||
element = tool.Ifc.get_entity(active_obj)
|
||||
if not element.is_a("IfcWall"):
|
||||
self.report({'ERROR'}, "The active object is not a wall. Please select a wall.")
|
||||
return
|
||||
|
||||
collection = active_obj.users_collection[0]
|
||||
collection_obj = bpy.data.objects.get(collection.name)
|
||||
if not collection_obj:
|
||||
self.report({'ERROR'}, "No collection found. Please insert one.")
|
||||
return
|
||||
|
||||
spatial_element = tool.Ifc.get_entity(collection_obj)
|
||||
if not spatial_element:
|
||||
self.report({'ERROR'}, "The collection hasn't an ifc space entity. Please provide one.")
|
||||
return
|
||||
|
||||
if not bpy.context.selected_objects:
|
||||
self.report({'ERROR'}, "No selected objects found. Please select walls.")
|
||||
return
|
||||
|
||||
x, y, z = active_obj.matrix_world.translation.xyz
|
||||
mat = active_obj.matrix_world
|
||||
h = active_obj.dimensions.z
|
||||
selected_objects = bpy.context.selected_objects
|
||||
|
||||
boundary_elements = self.get_boundary_elements(selected_objects)
|
||||
|
||||
polys = self.get_polygons(boundary_elements)
|
||||
|
||||
converted_tolerance = self.get_converted_tolerance(tolerance = 0.03)
|
||||
|
||||
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style = 2, join_style = 2)
|
||||
|
||||
i=0
|
||||
for linear_ring in union.interiors:
|
||||
poly = Polygon(linear_ring)
|
||||
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style = 2, join_style = 2)
|
||||
|
||||
bm = self.get_bmesh_from_polygon(poly, mat, h)
|
||||
|
||||
name = "Space" + str(i)
|
||||
mesh = bpy.data.meshes.new(name = name)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
obj.matrix_world = mat
|
||||
collection.objects.link(obj)
|
||||
|
||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
||||
i+=1
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_boundary_elements(self, selected_objects):
|
||||
boundary_elements = []
|
||||
for obj in selected_objects:
|
||||
subelement = tool.Ifc.get_entity(obj)
|
||||
if subelement.is_a("IfcWall"):
|
||||
boundary_elements.append(subelement)
|
||||
return boundary_elements
|
||||
|
||||
def get_polygons(self, boundary_elements):
|
||||
polys = []
|
||||
for boundary_element in boundary_elements:
|
||||
obj = tool.Ifc.get_object(boundary_element)
|
||||
if not obj:
|
||||
continue
|
||||
points = []
|
||||
base = self.get_obj_base_points(obj)
|
||||
for index in ["low_left", "low_right", "high_right", "high_left"]:
|
||||
point = base[index]
|
||||
points.append(point)
|
||||
|
||||
polys.append(Polygon(points))
|
||||
return polys
|
||||
|
||||
def get_obj_base_points(self, obj):
|
||||
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box]
|
||||
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
|
||||
return {
|
||||
"low_left": (x_values[0], y_values[0]),
|
||||
"high_left": (x_values[3], y_values[3]),
|
||||
"low_right": (x_values[4], y_values[4]),
|
||||
"high_right": (x_values[7], y_values[7]),
|
||||
}
|
||||
|
||||
def get_converted_tolerance(self, tolerance):
|
||||
model = tool.Ifc.get()
|
||||
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
|
||||
prefix=getattr(project_unit, "Prefix", None)
|
||||
|
||||
converted_tolerance = ifcopenshell.util.unit.convert(
|
||||
value = tolerance,
|
||||
from_prefix = None,
|
||||
from_unit = "METRE",
|
||||
to_prefix = prefix,
|
||||
to_unit = project_unit.Name,
|
||||
)
|
||||
return tolerance
|
||||
|
||||
def get_bmesh_from_polygon(self, poly, mat, h):
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
mat_invert = mat.inverted()
|
||||
|
||||
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]]
|
||||
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
|
||||
bmesh.ops.triangle_fill(bm, edges=bm.edges)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
|
||||
|
||||
bmesh.ops.recalc_face_normals(bm, faces = bm.faces)
|
||||
|
||||
return bm
|
||||
|
||||
@@ -135,6 +135,10 @@ class BIM_PT_authoring(Panel):
|
||||
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
|
||||
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
|
||||
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.generate_space")
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.generate_spaces_from_walls")
|
||||
|
||||
|
||||
class BIM_PT_array(bpy.types.Panel):
|
||||
|
||||
@@ -99,6 +99,8 @@ classes = (
|
||||
operator.RemoveWorkTime,
|
||||
operator.SelectTaskRelatedProducts,
|
||||
operator.SelectTaskRelatedInputs,
|
||||
operator.SelectWorkScheduleProducts,
|
||||
operator.SelectUnassignedWorkScheduleProducts,
|
||||
operator.SetTaskSortColumn,
|
||||
operator.SetupDefaultTaskColumns,
|
||||
operator.UnassignLagTime,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell
|
||||
from ifcopenshell.util.doc import get_predefined_type_doc
|
||||
|
||||
|
||||
def refresh():
|
||||
@@ -34,6 +35,7 @@ class SequenceData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"predefined_types": cls.get_work_schedule_types(),
|
||||
"has_work_plans": cls.has_work_plans(),
|
||||
"has_work_schedules": cls.has_work_schedules(),
|
||||
"has_work_calendars": cls.has_work_calendars(),
|
||||
@@ -223,6 +225,22 @@ class SequenceData:
|
||||
data["HasAssignmentsWorkCalendar"].append(rel.RelatingControl.id())
|
||||
cls.data["tasks"][task.id()] = data
|
||||
|
||||
@classmethod
|
||||
def get_work_schedule_types(cls):
|
||||
results = []
|
||||
declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule")
|
||||
version = tool.Ifc.get_schema()
|
||||
for attribute in declaration.attributes():
|
||||
if attribute.name() == "PredefinedType":
|
||||
results.extend(
|
||||
[
|
||||
(e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e))
|
||||
for e in attribute.type_of_attribute().declared_type().enumeration_items()
|
||||
]
|
||||
)
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
class WorkPlansData:
|
||||
data = {}
|
||||
|
||||
@@ -1327,3 +1327,31 @@ class HighlightTask(bpy.types.Operator):
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectWorkScheduleProducts(bpy.types.Operator):
|
||||
bl_idname = "bim.select_work_schedule_products"
|
||||
bl_label = "Select Work Schedule Products"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
r = core.select_work_schedule_products(
|
||||
tool.Sequence, tool.Spatial, work_schedule=tool.Ifc.get().by_id(self.work_schedule)
|
||||
)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectUnassignedWorkScheduleProducts(bpy.types.Operator):
|
||||
bl_idname = "bim.select_unassigned_work_schedule_products"
|
||||
bl_label = "Select Unassigned Work Schedule Products"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
work_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
r = core.select_unassigned_work_schedule_products(tool.Ifc, tool.Sequence, tool.Spatial)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -20,6 +20,8 @@ import bpy
|
||||
import isodate
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.attribute
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.sequence as core
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.sequence.data import SequenceData
|
||||
import blenderbim.bim.module.pset.data
|
||||
@@ -37,7 +39,6 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
|
||||
taskcolumns_enum = []
|
||||
tasktimecolumns_enum = []
|
||||
|
||||
@@ -108,7 +109,7 @@ def updateTaskName(self, context):
|
||||
props = context.scene.BIMWorkScheduleProperties
|
||||
if not props.is_task_update_enabled or self.name == "Unnamed":
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"sequence.edit_task",
|
||||
self.file,
|
||||
@@ -124,7 +125,7 @@ def updateTaskIdentification(self, context):
|
||||
props = context.scene.BIMWorkScheduleProperties
|
||||
if not props.is_task_update_enabled or self.identification == "XXX":
|
||||
return
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
ifcopenshell.api.run(
|
||||
"sequence.edit_task",
|
||||
self.file,
|
||||
@@ -160,7 +161,7 @@ def updateTaskTimeDateTime(self, context, startfinish):
|
||||
if startfinish_value == "-":
|
||||
return
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
|
||||
try:
|
||||
startfinish_datetime = parser.isoparse(startfinish_value)
|
||||
@@ -208,7 +209,7 @@ def updateTaskDuration(self, context):
|
||||
self.duration = "-"
|
||||
return
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
self.file = tool.Ifc.get()
|
||||
task = self.file.by_id(self.ifc_definition_id)
|
||||
if task.TaskTime:
|
||||
task_time = task.TaskTime
|
||||
@@ -228,6 +229,12 @@ def updateTaskDuration(self, context):
|
||||
bpy.ops.bim.load_task_properties()
|
||||
|
||||
|
||||
def get_schedule_predefined_types(self, context):
|
||||
if not SequenceData.is_loaded:
|
||||
SequenceData.load()
|
||||
return SequenceData.data["predefined_types"]
|
||||
|
||||
|
||||
def update_visualisation_start(self, context):
|
||||
update_visualisation_start_finish(self, context, "visualisation_start")
|
||||
|
||||
@@ -277,6 +284,9 @@ def update_color_progress(self, context):
|
||||
color[1] = color_progress.g
|
||||
color[2] = color_progress.b
|
||||
|
||||
def update_sort_reversed(self, context):
|
||||
if context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
|
||||
core.create_task_tree(tool.Sequence, work_schedule=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_work_schedule_id))
|
||||
|
||||
class Task(PropertyGroup):
|
||||
name: StringProperty(name="Name", update=updateTaskName)
|
||||
@@ -335,6 +345,9 @@ class ISODuration(PropertyGroup):
|
||||
|
||||
|
||||
class BIMWorkScheduleProperties(PropertyGroup):
|
||||
work_schedule_predefined_types: EnumProperty(
|
||||
items=get_schedule_predefined_types, name="Predefined Type", default=None
|
||||
)
|
||||
durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration)
|
||||
work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars")
|
||||
work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute)
|
||||
@@ -346,12 +359,12 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
active_task_id: IntProperty(name="Active Task Id")
|
||||
task_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
|
||||
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False)
|
||||
should_show_bar_visual_option: BoolProperty(name="Should Show Settings UI", default=False)
|
||||
should_show_bar_visual_option: BoolProperty(name="Add to task bar", default=False)
|
||||
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
|
||||
columns: CollectionProperty(name="Columns", type=Attribute)
|
||||
active_column_index: IntProperty(name="Active Column Index")
|
||||
sort_column: StringProperty(name="Sort Column")
|
||||
is_sort_reversed: BoolProperty(name="Is Sort Reversed")
|
||||
is_sort_reversed: BoolProperty(name="Is Sort Reversed", update=update_sort_reversed)
|
||||
column_types: EnumProperty(
|
||||
items=[
|
||||
("IfcTask", "IfcTask", ""),
|
||||
@@ -501,3 +514,4 @@ class BIMAnimationProperties(PropertyGroup):
|
||||
description="color picker",
|
||||
update=update_color_progress,
|
||||
)
|
||||
should_show_task_bar_options: BoolProperty(name="Show Task Bar Options", default=False)
|
||||
|
||||
@@ -117,15 +117,19 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.tprops = context.scene.BIMTaskTreeProperties
|
||||
self.animation_props = context.scene.BIMAnimationProperties
|
||||
|
||||
row = self.layout.row()
|
||||
if SequenceData.data["has_work_schedules"]:
|
||||
row.label(
|
||||
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
|
||||
icon="TEXT",
|
||||
)
|
||||
else:
|
||||
row.label(text="No Work Schedules found.", icon="TEXT")
|
||||
row.operator("bim.add_work_schedule", text="", icon="ADD")
|
||||
if not self.props.active_work_schedule_id:
|
||||
row = self.layout.row()
|
||||
if SequenceData.data["has_work_schedules"]:
|
||||
row.label(
|
||||
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
|
||||
icon="TEXT",
|
||||
)
|
||||
else:
|
||||
row.label(text="No Work Schedules found.", icon="TEXT")
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.prop(self.props, "work_schedule_predefined_types")
|
||||
row.operator("bim.add_work_schedule", text="Add new", icon="ADD")
|
||||
|
||||
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
|
||||
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
|
||||
@@ -133,17 +137,41 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
|
||||
|
||||
if self.props.active_work_schedule_id == work_schedule_id:
|
||||
if self.props.editing_type == "WORK_SCHEDULE":
|
||||
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
|
||||
elif self.props.editing_type == "TASKS":
|
||||
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
|
||||
row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO")
|
||||
row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id
|
||||
row.operator("bim.recalculate_schedule", text="", icon="FILE_REFRESH").work_schedule = work_schedule_id
|
||||
row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id
|
||||
row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL")
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "LEFT"
|
||||
row1.label(text="Schedule tools")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.operator(
|
||||
"bim.generate_gantt_chart", text="Generate Gantt", icon="NLA"
|
||||
).work_schedule = work_schedule_id
|
||||
row1.operator(
|
||||
"bim.recalculate_schedule", text="Re-calculate Schedule", icon="FILE_REFRESH"
|
||||
).work_schedule = work_schedule_id
|
||||
row2 = col.row(align=True)
|
||||
row2.alignment = "RIGHT"
|
||||
row2.operator(
|
||||
"bim.select_work_schedule_products", text="Select Assigned", icon="RESTRICT_SELECT_OFF"
|
||||
).work_schedule = work_schedule_id
|
||||
row2.operator(
|
||||
"bim.select_unassigned_work_schedule_products", text="Select Unassigned", icon="RESTRICT_SELECT_OFF"
|
||||
).work_schedule = work_schedule_id
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "LEFT"
|
||||
row1.label(text="Settings")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
|
||||
row1.prop(self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO")
|
||||
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
|
||||
else:
|
||||
row.operator(
|
||||
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
|
||||
@@ -188,8 +216,9 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.operator("bim.remove_task", text="", icon="X").task = ifc_definition_id
|
||||
|
||||
def draw_column_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.setup_default_task_columns", text="Show Default Columns", icon="ANCHOR_BOTTOM")
|
||||
row = self.layout.row()
|
||||
row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM")
|
||||
row.alignment = "RIGHT"
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "column_types", text="")
|
||||
column_type = self.props.column_types
|
||||
@@ -226,7 +255,7 @@ class BIM_PT_work_schedules(Panel):
|
||||
op.work_schedule = self.props.active_work_schedule_id
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Animation Options")
|
||||
row.label(text="Speed Settings")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "speed_types", text="")
|
||||
if self.props.speed_types == "FRAME_SPEED":
|
||||
@@ -237,15 +266,35 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.prop(self.props, "speed_real_duration", text="")
|
||||
elif self.props.speed_types == "MULTIPLIER_SPEED":
|
||||
row.prop(self.props, "speed_multiplier", text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Display Settings")
|
||||
row = self.layout.row(align=True)
|
||||
if not self.animation_props.is_editing:
|
||||
op = row.operator(
|
||||
"bim.enable_editing_task_animation_colors", text="Customize Animation Colors", icon="SEQUENCE_COLOR_04"
|
||||
"bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04"
|
||||
)
|
||||
else:
|
||||
op = row.operator(
|
||||
"bim.disable_editing_task_animation_colors", text="Hide Animation Colors", icon="SEQUENCE_COLOR_01"
|
||||
"bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01"
|
||||
)
|
||||
|
||||
row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN")
|
||||
if self.animation_props.should_show_task_bar_options:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_task_bars", text="Add Bar Visual", icon="NLA_PUSHDOWN")
|
||||
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
# Column1
|
||||
col = grid.column()
|
||||
row1 = col.row(align=True)
|
||||
row1.label(text="Bar Colors", icon="NLA_PUSHDOWN")
|
||||
|
||||
row2 = col.row(align=True)
|
||||
row2.prop(self.animation_props, "color_progress")
|
||||
|
||||
row3 = col.row(align=True)
|
||||
row3.prop(self.animation_props, "color_full")
|
||||
|
||||
if self.animation_props.is_editing:
|
||||
self.draw_visualisation_settings_ui()
|
||||
|
||||
@@ -286,6 +335,13 @@ class BIM_PT_work_schedules(Panel):
|
||||
draw_attributes(self.props.work_schedule_attributes, self.layout)
|
||||
|
||||
def draw_editable_task_ui(self, work_schedule_id):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="")
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
row.operator("bim.add_summary_task", text="Add Summary Task", icon="ADD").work_schedule = work_schedule_id
|
||||
row.operator("bim.expand_all_tasks", text="Expand All")
|
||||
row.operator("bim.contract_all_tasks", text="Contract All")
|
||||
self.draw_task_operators()
|
||||
self.layout.template_list(
|
||||
"BIM_UL_tasks",
|
||||
@@ -295,9 +351,6 @@ class BIM_PT_work_schedules(Panel):
|
||||
self.props,
|
||||
"active_task_index",
|
||||
)
|
||||
row = self.layout.row()
|
||||
row.operator("bim.expand_all_tasks", text="Expand All")
|
||||
row.operator("bim.contract_all_tasks", text="Contract All")
|
||||
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
|
||||
self.draw_editable_task_attributes_ui()
|
||||
elif self.props.active_task_id and self.props.editing_task_type == "CALENDAR":
|
||||
@@ -430,7 +483,7 @@ class BIM_PT_task_icom(Panel):
|
||||
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
|
||||
op.related_object = input_id
|
||||
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
|
||||
op.task = task.ifc_definition_id
|
||||
|
||||
row2 = col.row()
|
||||
|
||||
@@ -430,6 +430,20 @@ def select_task_inputs(sequence, spatial, task=None):
|
||||
spatial.select_products(products=sequence.get_task_inputs(task))
|
||||
|
||||
|
||||
def select_work_schedule_products(sequence, spatial, work_schedule=None):
|
||||
products = sequence.get_work_schedule_products(work_schedule)
|
||||
spatial.select_products(products)
|
||||
|
||||
|
||||
def select_unassigned_work_schedule_products(ifc, sequence, spatial):
|
||||
spatial.deselect_all()
|
||||
products = ifc.get().by_type("IfcElement")
|
||||
work_schedule = sequence.get_active_work_schedule()
|
||||
schedule_products = sequence.get_work_schedule_products(work_schedule)
|
||||
selection = [product for product in products if product not in schedule_products]
|
||||
spatial.select_products(selection)
|
||||
|
||||
|
||||
def recalculate_schedule(ifc, work_schedule=None):
|
||||
ifc.run("sequence.recalculate_schedule", work_schedule=work_schedule)
|
||||
|
||||
@@ -522,5 +536,6 @@ def generate_gantt_chart(sequence, work_schedule):
|
||||
json = sequence.create_tasks_json(work_schedule)
|
||||
sequence.generate_gantt_browser_chart(json)
|
||||
|
||||
|
||||
def load_product_tasks(sequence, product=None):
|
||||
sequence.load_product_tasks(product)
|
||||
sequence.load_product_tasks(product)
|
||||
|
||||
@@ -619,11 +619,24 @@ class Search:
|
||||
@interface
|
||||
class Sequence:
|
||||
def add_task_column(cls, column_type, name, data_type): pass
|
||||
def add_text_animation_handler(cls, settings): pass
|
||||
def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass
|
||||
def animate_creation(cls, obj, start_frame, product_frame, color): pass
|
||||
def animate_destruction(cls, obj, start_frame, color, animation_type): pass
|
||||
def animate_input(cls, obj, start_frame, product_frame, animation_type): pass
|
||||
def animate_movement_from(cls, obj, start_frame, color, animation_type): pass
|
||||
def animate_movement_to(cls, obj, start_frame, product_frame, color): pass
|
||||
def animate_objects(cls, settings, frames, clear_previous, animation_type): pass
|
||||
def animate_operation(cls, obj, start_frame, product_frame, color): pass
|
||||
def animate_output(cls, obj, start_frame, product_frame): pass
|
||||
def clear_object_animation(cls, obj): pass
|
||||
def clear_objects_animation(cls, include_blender_objects): pass
|
||||
def contract_all_tasks(cls): pass
|
||||
def contract_task(cls, task): pass
|
||||
def create_bars(cls, tasks): pass
|
||||
def create_bars(cls, tasks):pass
|
||||
def create_new_task_json(cls, task, json, type_map=None): pass
|
||||
def create_task_tree(cls, work_schedule): pass
|
||||
def create_tasks_json(cls, work_schedule=None): pass
|
||||
def disable_editing_rel_sequence(cls): pass
|
||||
def disable_editing_task_animation_colors(cls): pass
|
||||
def disable_editing_task_time(cls): pass
|
||||
@@ -651,11 +664,13 @@ class Sequence:
|
||||
def expand_all_tasks(cls): pass
|
||||
def expand_task(cls, task): pass
|
||||
def find_related_input_tasks(cls, product): pass
|
||||
def find_related_output_tasks(cls, column): pass
|
||||
def find_related_output_tasks(cls, product): pass
|
||||
def generate_gantt_browser_chart(cls, task_json): pass
|
||||
def get_active_task(cls): pass
|
||||
def get_active_work_schedule(cls): pass
|
||||
def get_animation_bar_tasks(cls): pass
|
||||
def get_animation_product_frames(cls, work_schedule, settings): pass
|
||||
def get_animation_settings(cls): pass
|
||||
def get_checked_tasks(cls): pass
|
||||
def get_direct_nested_tasks(cls, task):pass
|
||||
def get_direct_task_outputs(cls, task): pass
|
||||
@@ -665,7 +680,6 @@ class Sequence:
|
||||
def get_recurrence_pattern_attributes(cls, recurrence_pattern): pass
|
||||
def get_recurrence_pattern_times(cls): pass
|
||||
def get_rel_sequence_attributes(cls): pass
|
||||
|
||||
def get_selected_resource(cls): pass
|
||||
def get_start_date(cls): pass
|
||||
def get_task_attribute_value(cls, attribute_name): pass
|
||||
@@ -678,12 +692,15 @@ class Sequence:
|
||||
def get_work_calendar_attributes(cls): pass
|
||||
def get_work_plan_attributes(cls): pass
|
||||
def get_work_schedule_attributes(cls): pass
|
||||
def get_work_schedule_products(cls, work_schedule): pass
|
||||
def get_work_schedule(cls, task): pass
|
||||
def get_work_time_attributes(cls): pass
|
||||
def guess_date_range(cls, work_schedule): pass
|
||||
def has_task_assignments(cls, product, cost_schedule=None): pass
|
||||
def highlight_task(cls, task): pass
|
||||
def is_work_schedule_active(cls, work_schedule): pass
|
||||
def load_lag_time_attributes(cls, lag_time): pass
|
||||
def load_product_tasks(cls, product): pass
|
||||
def load_rel_sequence_attributes(cls, rel_sequence): pass
|
||||
def load_resources(cls): pass
|
||||
def load_task_animation_colors(cls): pass
|
||||
@@ -707,6 +724,7 @@ class Sequence:
|
||||
def show_snapshot(cls, product_states): pass
|
||||
def update_visualisation_date(cls, start_date, finish_date): pass
|
||||
|
||||
|
||||
@interface
|
||||
class Spatial:
|
||||
def can_contain(cls, structure_obj, element_obj): pass
|
||||
|
||||
@@ -1347,15 +1347,16 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
"""returns a set of elements that are included in the drawing"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
|
||||
include = pset.get("Include", None)
|
||||
if include:
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include))
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
|
||||
else:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||
else:
|
||||
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
|
||||
elements = {e for e in elements if e.is_a() != "IfcSpace"}
|
||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
|
||||
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
|
||||
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
|
||||
elements.update(annotations)
|
||||
|
||||
@@ -1370,7 +1371,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||
include = pset.get("Include", None)
|
||||
elements = set(ifc_file.by_type("IfcSpace"))
|
||||
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")])
|
||||
if include:
|
||||
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
|
||||
exclude = pset.get("Exclude", None)
|
||||
@@ -1449,14 +1450,13 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
|
||||
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
|
||||
drawing = tool.Ifc.get_entity(camera)
|
||||
all_elements = set(tool.Ifc.get().by_type("IfcElement")) - set(tool.Ifc.get().by_type("IfcOpeningElement"))
|
||||
filtered_elements = cls.get_drawing_elements(drawing)
|
||||
hidden_elements = list(all_elements - filtered_elements)
|
||||
hidden_objs = [tool.Ifc.get_object(e) for e in hidden_elements]
|
||||
|
||||
# Running operators is much more efficient in this scenario than looping through each element
|
||||
bpy.ops.object.hide_view_clear()
|
||||
|
||||
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
|
||||
hidden_objs = [o for o in bpy.context.visible_objects if tool.Ifc.get_entity(o) not in filtered_elements]
|
||||
|
||||
for hidden_obj in hidden_objs:
|
||||
if bpy.context.view_layer.objects.get(hidden_obj.name):
|
||||
hidden_obj.hide_set(True)
|
||||
@@ -1492,6 +1492,36 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
)
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def get_elements_in_camera_view(cls, camera, objs):
|
||||
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
|
||||
x = camera.data.ortho_scale / 2
|
||||
y = (camera.data.BIMCameraProperties.raster_y / camera.data.BIMCameraProperties.raster_x) * x
|
||||
else:
|
||||
y = camera.data.ortho_scale / 2
|
||||
x = (camera.data.BIMCameraProperties.raster_x / camera.data.BIMCameraProperties.raster_y) * y
|
||||
|
||||
camera_inverse_matrix = camera.matrix_world.inverted()
|
||||
return set([
|
||||
tool.Ifc.get_entity(o)
|
||||
for o in objs
|
||||
if cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end)
|
||||
and tool.Ifc.get_entity(o)
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def is_in_camera_view(cls, obj, camera_inverse_matrix, x, y, clip_start, clip_end):
|
||||
local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box]
|
||||
for v in local_bbox:
|
||||
if v.z < -clip_start and v.z > -clip_end and abs(v.x) < x and abs(v.y) < y:
|
||||
return True
|
||||
if any([v.z > -clip_start for v in local_bbox]) and any([v.z < -clip_end for v in local_bbox]):
|
||||
return True
|
||||
elif any([v.x < -x for v in local_bbox]) and any([v.x > x for v in local_bbox]):
|
||||
return True
|
||||
elif any([v.y < -y for v in local_bbox]) and any([v.y > y for v in local_bbox]):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_intersecting_camera(cls, obj, camera):
|
||||
# Based on separating axis theorem
|
||||
|
||||
@@ -84,6 +84,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
bpy.data.collections.remove(obj.users_collection[0])
|
||||
if getattr(element, "FillsVoids", None):
|
||||
bpy.ops.bim.remove_filling(filling=element.id())
|
||||
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
if element.HasFillings:
|
||||
for rel in element.HasFillings:
|
||||
@@ -97,7 +98,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
bpy.ops.bim.remove_opening(opening_id=rel.RelatedOpeningElement.id())
|
||||
for port in ifcopenshell.util.system.get_ports(element):
|
||||
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
|
||||
try:
|
||||
obj.name
|
||||
bpy.data.objects.remove(obj)
|
||||
@@ -316,7 +317,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
|
||||
@classmethod
|
||||
def is_edited(cls, obj):
|
||||
return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs
|
||||
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
|
||||
|
||||
@classmethod
|
||||
def is_mapped_representation(cls, representation):
|
||||
|
||||
@@ -285,9 +285,6 @@ class IfcGit:
|
||||
for obj in blender_collection.objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
bpy.data.collections.remove(blender_collection)
|
||||
for collection in bpy.data.collections:
|
||||
if not collection.users:
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
@classmethod
|
||||
def is_valid_branch_name(cls, new_branch_name):
|
||||
|
||||
@@ -133,8 +133,8 @@ class Project(blenderbim.core.tool.Project):
|
||||
def set_default_modeling_dimensions(cls):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
props.extrusion_depth = 3 / unit_scale
|
||||
props.length = 1 / unit_scale
|
||||
props.extrusion_depth = 3
|
||||
props.length = 1
|
||||
props.rl1 = 0
|
||||
props.rl2 = 1 / unit_scale
|
||||
props.x = 0.5 / unit_scale
|
||||
|
||||
@@ -143,10 +143,29 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def get_sorted_tasks_ids(cls, tasks):
|
||||
cls.sort_keys = {task.id(): cls.get_sort_key(task) for task in tasks}
|
||||
related_object_ids = sorted(cls.sort_keys, key=cls.natural_sort_key)
|
||||
def get_sort_key(task):
|
||||
# Sorting only applies to actual tasks, not the WBS
|
||||
for rel in task.IsNestedBy:
|
||||
for object in rel.RelatedObjects:
|
||||
if object.is_a("IfcTask"):
|
||||
return "0000000000" + (task.Identification or "")
|
||||
if not bpy.context.scene.BIMWorkScheduleProperties.sort_column:
|
||||
return task.Identification or ""
|
||||
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
|
||||
if column_type == "IfcTask":
|
||||
return task.Name or ""
|
||||
elif column_type == "IfcTaskTime" and task.TaskTime:
|
||||
return task.TaskTime.Name or ""
|
||||
return task.Identification or ""
|
||||
|
||||
def natural_sort_key(i, _nsre=re.compile("([0-9]+)")):
|
||||
s = sort_keys[i]
|
||||
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
|
||||
|
||||
sort_keys = {task.id(): get_sort_key(task) for task in tasks}
|
||||
related_object_ids = sorted(sort_keys, key=natural_sort_key)
|
||||
if bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed:
|
||||
return related_object_ids.reverse()
|
||||
related_object_ids.reverse()
|
||||
return related_object_ids
|
||||
|
||||
@classmethod
|
||||
@@ -162,27 +181,6 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
for related_object_id in cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.get_nested_tasks(task)):
|
||||
cls.create_new_task_li(related_object_id, level_index + 1)
|
||||
|
||||
@classmethod
|
||||
def natural_sort_key(cls, i, _nsre=re.compile("([0-9]+)")):
|
||||
s = cls.sort_keys[i]
|
||||
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
|
||||
|
||||
@classmethod
|
||||
def get_sort_key(cls, task):
|
||||
# Sorting only applies to actual tasks, not the WBS
|
||||
for rel in task.IsNestedBy:
|
||||
for object in rel.RelatedObjects:
|
||||
if object.is_a("IfcTask"):
|
||||
return "0000000000" + (task.Identification or "")
|
||||
if not bpy.context.scene.BIMWorkScheduleProperties.sort_column:
|
||||
return task.Identification or ""
|
||||
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
|
||||
if column_type == "IfcTask":
|
||||
return task.Name or ""
|
||||
elif column_type == "IfcTaskTime" and task.TaskTime:
|
||||
return task.TaskTime.Name or ""
|
||||
return task.Identification or ""
|
||||
|
||||
@classmethod
|
||||
def load_task_properties(cls, task=None):
|
||||
def canonicalise_time(time):
|
||||
@@ -1526,3 +1524,19 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
new = props.product_output_tasks.add()
|
||||
new.name = task.Name or "Unnamed"
|
||||
new.ifc_definition_id = task.id()
|
||||
|
||||
@classmethod
|
||||
def get_work_schedule_products(cls, work_schedule):
|
||||
products = []
|
||||
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
|
||||
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
|
||||
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
|
||||
return products
|
||||
|
||||
@classmethod
|
||||
def has_task_assignments(cls, product, work_schedule=None):
|
||||
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
|
||||
if work_schedule:
|
||||
task_inputs = [task for task in task_inputs or [] if cls.get_work_schedule(task) == work_schedule]
|
||||
task_ouputs = [task for task in task_ouputs or [] if cls.get_work_schedule(task) == work_schedule]
|
||||
return bool(task_inputs or task_ouputs)
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
from itertools import chain
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
import collections
|
||||
import mathutils
|
||||
from pprint import pprint
|
||||
from math import pi, cos, sin, tan
|
||||
from math import pi, cos, sin, tan, radians
|
||||
|
||||
|
||||
def mm(x):
|
||||
@@ -75,7 +75,8 @@ class Usecase:
|
||||
railing_radius = self.settings["railing_diameter"] / 2
|
||||
support_spacing = self.settings["support_spacing"]
|
||||
clear_width = self.settings["clear_width"]
|
||||
height = self.settings["height"]
|
||||
# for calculations purposes we use height without railing radius
|
||||
height = self.settings["height"] - railing_radius
|
||||
cap_type = self.settings["terminal_type"]
|
||||
ifc_context = self.settings["context"]
|
||||
railing_coords = self.settings["railing_path"]
|
||||
@@ -106,8 +107,11 @@ class Usecase:
|
||||
solid = builder.create_swept_disk_solid(polyline, support_radius)
|
||||
|
||||
support_disk_circle = builder.circle(radius=support_disk_radius)
|
||||
|
||||
angle = V(0, 1).angle_signed(ortho_dir.xy)
|
||||
y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_by_y_kwargs(), angle)
|
||||
support_disk = builder.extrude(
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **builder.extrude_by_y_kwargs()
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **y_extrusion_kwargs
|
||||
)
|
||||
return [solid, support_disk]
|
||||
|
||||
@@ -127,7 +131,7 @@ class Usecase:
|
||||
)[0]
|
||||
|
||||
midpointco = center + ((fillet_v1co.lerp(fillet_v2co, 0.5) - center).normalized() * radius)
|
||||
return fillet_v1co, midpointco, fillet_v2co
|
||||
return [fillet_v1co, midpointco, fillet_v2co]
|
||||
|
||||
def add_arcs_on_turnings_points(base_points):
|
||||
"""add 3 point fillet arcs on turning points of the railing path"""
|
||||
@@ -204,12 +208,54 @@ class Usecase:
|
||||
# TODO: implement more cap types
|
||||
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
|
||||
|
||||
start = railing_coords_for_cap[-1]
|
||||
start_point = railing_coords_for_cap[-1]
|
||||
cap_dir = (railing_coords_for_cap[-1] - railing_coords_for_cap[-2]).xy.to_3d().normalized()
|
||||
arc_point = start + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start + terminal_radius * 2 * z_down]
|
||||
ortho_dir = (cap_dir.yx * V(1, -1)).to_3d().normalized()
|
||||
if start:
|
||||
ortho_dir = -ortho_dir
|
||||
|
||||
arc_middle_point_cos = sin(radians(45))
|
||||
|
||||
if cap_type in ('180', 'TO_END_POST'):
|
||||
arc_point = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + terminal_radius * 2 * z_down]
|
||||
|
||||
if cap_type == 'TO_END_POST':
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point.z -= terminal_radius * 2
|
||||
cap_coords.append(end_point)
|
||||
|
||||
elif cap_type == 'TO_WALL':
|
||||
arc_point = start_point + cap_dir * clear_width * arc_middle_point_cos + ortho_dir * clear_width * (1-arc_middle_point_cos)
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + ortho_dir * clear_width + cap_dir * clear_width]
|
||||
|
||||
elif cap_type == 'TO_FLOOR':
|
||||
arc_point = start_point + cap_dir * terminal_radius * arc_middle_point_cos + z_down * terminal_radius * (1-arc_middle_point_cos)
|
||||
arc_points.append(arc_point)
|
||||
arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
cap_coords = [
|
||||
arc_point,
|
||||
arc_end,
|
||||
arc_end+z_down*(height-terminal_radius),
|
||||
]
|
||||
|
||||
elif cap_type == 'TO_END_POST_AND_FLOOR':
|
||||
first_arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
first_arc_coords = get_fillet_points(
|
||||
start_point, start_point + cap_dir * terminal_radius,
|
||||
first_arc_end, terminal_radius)
|
||||
arc_points.append(first_arc_coords[1])
|
||||
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point.z -= height
|
||||
second_arc_coords = get_fillet_points(
|
||||
first_arc_end, first_arc_end + z_down * terminal_radius, end_point, terminal_radius
|
||||
)
|
||||
arc_points.append(second_arc_coords[1])
|
||||
cap_coords = [start_point] + first_arc_coords + second_arc_coords + [end_point]
|
||||
|
||||
railing_coords = railing_coords_for_cap + cap_coords
|
||||
|
||||
if start:
|
||||
@@ -219,8 +265,9 @@ class Usecase:
|
||||
items_3d.extend(create_supports_items(railing_coords, manual_supports=use_manual_supports))
|
||||
railing_coords = add_arcs_on_turnings_points(railing_coords)
|
||||
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
if cap_type != 'NONE':
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
|
||||
railing_path = builder.polyline(
|
||||
railing_coords, closed=False, arc_points=[railing_coords.index(p) for p in arc_points]
|
||||
|
||||
@@ -49,8 +49,9 @@ class Usecase:
|
||||
self.settings = {"task": task}
|
||||
|
||||
def execute(self):
|
||||
result = ifcopenshell.util.element.copy(self.file, self.settings["task"])
|
||||
result = ifcopenshell.util.element.copy_deep(self.file, self.settings["task"])
|
||||
self.copy_indirect_attributes(self.settings["task"], result)
|
||||
self.copy_sequence_relationship([self.settings["task"]], [result])
|
||||
return result
|
||||
|
||||
def copy_sequence_relationship(self, original_tasks, duplicated_tasks):
|
||||
|
||||
@@ -781,6 +781,18 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]):
|
||||
):
|
||||
to_delete.add(subelement)
|
||||
subelement_queue.extend(ifc_file.traverse(subelement, max_levels=1)[1:])
|
||||
# See #3052. IfcOpenShell is extremely slow in removing elements if
|
||||
# the element has an inverse, and that inverse references that
|
||||
# element in a big list. The most common example is an
|
||||
# IfcPolygonalFaceSet with a Faces attribute of tens of thousands
|
||||
# of IfcIndexedPolygonalFace. In this situation, removing a
|
||||
# IfcIndexedPolygonalFace will take very, very long. If we are
|
||||
# going to delete an element (i.e. added to the to_delete set), we
|
||||
# clear any large lists (10 is an arbitrary threshold) to prevent
|
||||
# this issue.
|
||||
for i, attribute in enumerate(subelement):
|
||||
if isinstance(attribute, tuple) and len(attribute) > 10:
|
||||
subelement[i] = []
|
||||
# We delete elements from subgraph in reverse order to allow batching to work
|
||||
for subelement in filter(lambda e: e in to_delete, subgraph[::-1]):
|
||||
ifc_file.remove(subelement)
|
||||
|
||||
@@ -568,3 +568,18 @@ class ShapeBuilder:
|
||||
"position_z_axis": Vector((0, -1, 0)),
|
||||
"extrusion_vector": Vector((0, 0, -1)),
|
||||
}
|
||||
|
||||
def rotate_extrusion_kwargs_by_z(self, kwargs, angle, counter_clockwise=False):
|
||||
"""shortcut to rotate extrusion kwargs by z axis
|
||||
|
||||
`kwargs` expected to have `position_x_axis` and `position_z_axis` keys
|
||||
|
||||
`angle` is a rotation value in radians
|
||||
|
||||
by default rotation is clockwise, to make it counter clockwise use `counter_clockwise` flag
|
||||
"""
|
||||
rot = Matrix.Rotation(-angle, 3, "Z")
|
||||
kwargs = kwargs.copy() # prevent mutation of original kwargs
|
||||
kwargs["position_x_axis"].rotate(rot)
|
||||
kwargs["position_z_axis"].rotate(rot)
|
||||
return kwargs
|
||||
|
||||
Reference in New Issue
Block a user