Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b302f8d21 | |||
| dfeebd6b6b | |||
| 641253c4c6 | |||
| 6ce49b7130 | |||
| 804ad0cece | |||
| 6c60d14c05 | |||
| def2d2bcc9 | |||
| 9bca705dc3 | |||
| 2dcc271dff | |||
| 33415fa1af | |||
| 10315e0e29 | |||
| f321cc81c8 | |||
| 68b685f580 | |||
| c87c79b937 | |||
| de3dfcdc43 | |||
| 657ddc0fa1 | |||
| 1da01489f1 | |||
| 64a27614ff | |||
| 6c5acfc8bb | |||
| 884ee5b587 | |||
| c2320a3c1e | |||
| ab23030051 | |||
| 699c692f6c | |||
| 0c85e26950 | |||
| 2cd7e737cd | |||
| a1adbe2b34 | |||
| a144c2b7ea | |||
| 201c056ffa | |||
| 2e95168610 | |||
| b18c05cbb9 | |||
| 102a4c6110 | |||
| b6c0ef3d19 | |||
| ea3065dbc2 | |||
| 534c3ad329 | |||
| 90a7d807a1 | |||
| 4baafb5e2f | |||
| f911765cdb | |||
| d0a36c79d5 | |||
| ffea5320a0 | |||
| 0b54e41a40 | |||
| 26c67f770a | |||
| f6623db0e6 | |||
| d7844ee8e8 | |||
| 1c8970bbf2 | |||
| eb70849a30 | |||
| 77ce3c9067 | |||
| 56cd226a27 | |||
| a1acdc0977 | |||
| 1a624c957b | |||
| 1c9eeb2aca | |||
| 45f06eeea7 | |||
| 66a53b486a | |||
| fc45f5b48f | |||
| c7105a22e6 | |||
| e1fc7cad44 | |||
| afeeaeb984 | |||
| 2bc60b87f2 | |||
| 4ad9ecb7db | |||
| 3ec7b82b85 | |||
| 48666def6b | |||
| 5732921fb6 | |||
| b86c11e1dc | |||
| 1bae4b219a | |||
| 70ff73f805 | |||
| a9636c5038 | |||
| a406b9f7ef | |||
| f291345111 | |||
| c947ccb8be | |||
| 28c6b0d4bb | |||
| 19317a9cc2 | |||
| d67da6425e | |||
| 901f41358f | |||
| 061922aea2 | |||
| 5f7b6a5498 | |||
| 2ef9d5eed9 | |||
| 9fba57d5db | |||
| e9074e3b3f | |||
| f053ba714f | |||
| b6598f09a3 | |||
| b9d7b95304 | |||
| 3db540f786 | |||
| 071b99c6c6 | |||
| 3a3d96be0a | |||
| a03fbab4d5 | |||
| cc3b119288 | |||
| 1a750d1b85 | |||
| 5b3ae9b9fd | |||
| 81471d80cd | |||
| eb14db4deb | |||
| 50d989363a | |||
| e52a99bd71 | |||
| a7a7c86657 |
@@ -36,7 +36,7 @@ requirements:
|
||||
run:
|
||||
- python
|
||||
- boost-cpp
|
||||
- occt
|
||||
- {{ pin_compatible('occt', max_pin='x.x.x') }}
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim
|
||||
import importlib
|
||||
from . import handler, ui, prop, operator, helper
|
||||
|
||||
@@ -171,7 +172,21 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
if ( #these panels haven't the cls.is_registered attribute
|
||||
cls == blenderbim.bim.module.drawing.gizmos.ExtrusionWidget or
|
||||
cls == blenderbim.bim.module.drawing.gizmos.ExtrusionGuidesGizmo or
|
||||
cls == blenderbim.bim.module.drawing.gizmos.DimensionLabelGizmo or
|
||||
cls == blenderbim.bim.module.drawing.gizmos.DotGizmo or
|
||||
cls == blenderbim.bim.module.drawing.gizmos.UglyDotGizmo
|
||||
):
|
||||
if bpy.data.scenes['Scene'].BIMProperties.module_visibility['drawing'].is_visible is True:
|
||||
bpy.utils.unregister_class(cls)
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
if cls.is_registered is not False:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
bpy.app.handlers.load_post.remove(handler.setDefaultProperties)
|
||||
bpy.app.handlers.load_post.remove(handler.loadIfcStore)
|
||||
bpy.app.handlers.save_post.remove(handler.ensureIfcExported)
|
||||
|
||||
@@ -96,4 +96,12 @@
|
||||
<path d="M 7.5 10 L 10 0 L 15 10" class="annotation" style="stroke-width:1; fill: white;" />
|
||||
</g>
|
||||
</marker>
|
||||
<filter x="0" y="0" width="1" height="1" id="fill-background">
|
||||
<feFlood flood-color="white" result="bg" />
|
||||
<feMerge>
|
||||
<feMergeNode in="bg"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -126,7 +126,7 @@ class IfcExporter:
|
||||
continue
|
||||
try:
|
||||
if isinstance(obj, bpy.types.Material):
|
||||
if self.has_changed_shading(obj):
|
||||
if tool.Ifc.has_changed_shading(obj):
|
||||
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=obj)
|
||||
else:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -149,10 +149,6 @@ class IfcExporter:
|
||||
checksum = obj.data.BIMMeshProperties.material_checksum
|
||||
return checksum != str([s.id() for s in tool.Geometry.get_styles(obj) if s])
|
||||
|
||||
def has_changed_shading(self, obj):
|
||||
checksum = obj.BIMMaterialProperties.shading_checksum
|
||||
return checksum != repr(np.array(obj.diffuse_color).tobytes())
|
||||
|
||||
def sync_object_placement(self, obj):
|
||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
if not tool.Ifc.is_moved(obj):
|
||||
|
||||
@@ -105,16 +105,16 @@ def active_material_index_callback(obj, data):
|
||||
refresh_ui_data()
|
||||
|
||||
|
||||
def subscribe_to(object, data_path, callback):
|
||||
def subscribe_to(obj, data_path, callback):
|
||||
try:
|
||||
subscribe_to = object.path_resolve(data_path, False)
|
||||
subscribe_to = obj.path_resolve(data_path, False)
|
||||
except:
|
||||
return
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
owner=object,
|
||||
owner=obj,
|
||||
args=(
|
||||
object,
|
||||
obj,
|
||||
data_path,
|
||||
),
|
||||
notify=callback,
|
||||
|
||||
@@ -24,6 +24,7 @@ import zipfile
|
||||
import tempfile
|
||||
import ifcopenshell
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -371,7 +372,7 @@ class IfcStore:
|
||||
def unlink_element(element=None, obj=None):
|
||||
if element is None:
|
||||
try:
|
||||
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -335,23 +335,7 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
|
||||
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
else:
|
||||
if self.file.schema == "IFC2X3":
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType")
|
||||
+ self.file.by_type("IfcDoorStyle")
|
||||
+ self.file.by_type("IfcWindowStyle")
|
||||
)
|
||||
elif self.file.schema == "IFC4":
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType")
|
||||
+ self.file.by_type("IfcDoorStyle")
|
||||
+ self.file.by_type("IfcWindowStyle")
|
||||
+ self.file.by_type("IfcSpatialElementType")
|
||||
)
|
||||
else:
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType") + self.file.by_type("IfcSpatialElementType")
|
||||
)
|
||||
self.element_types = set(self.file.by_type("IfcTypeProduct"))
|
||||
|
||||
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
|
||||
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
|
||||
|
||||
@@ -59,9 +59,13 @@ class MaterialAttributesData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"attributes": cls.attributes()}
|
||||
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def ifc_definition_id(cls):
|
||||
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
|
||||
|
||||
@classmethod
|
||||
def attributes(cls):
|
||||
results = []
|
||||
|
||||
@@ -94,4 +94,10 @@ class BIM_PT_material_attributes(Panel):
|
||||
def draw(self, context):
|
||||
if not MaterialAttributesData.is_loaded:
|
||||
MaterialAttributesData.load()
|
||||
elif (
|
||||
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
|
||||
!= MaterialAttributesData.data["ifc_definition_id"]
|
||||
):
|
||||
MaterialAttributesData.load()
|
||||
|
||||
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
|
||||
|
||||
@@ -119,7 +119,7 @@ class Loader:
|
||||
|
||||
class LoadProjectSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_project_space_boundaries"
|
||||
bl_label = "Load all project space boundaries"
|
||||
bl_label = "Load All Project Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -131,7 +131,7 @@ class LoadProjectSpaceBoundaries(bpy.types.Operator):
|
||||
|
||||
class LoadBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.load_boundary"
|
||||
bl_label = "Load boundary"
|
||||
bl_label = "Load Boundary"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
boundary_id: bpy.props.IntProperty()
|
||||
|
||||
@@ -147,7 +147,7 @@ class LoadBoundary(bpy.types.Operator):
|
||||
|
||||
class LoadSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_space_boundaries"
|
||||
bl_label = "Load selected space boundaries"
|
||||
bl_label = "Load Selected Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -168,7 +168,7 @@ def get_element_boundaries(element):
|
||||
|
||||
class SelectRelatedElementBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_related_element_boundaries"
|
||||
bl_label = "Select related element space boundaries"
|
||||
bl_label = "Select Related Element Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
related_element: bpy.props.IntProperty()
|
||||
|
||||
@@ -185,7 +185,7 @@ class SelectRelatedElementBoundaries(bpy.types.Operator):
|
||||
|
||||
class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_related_element_type_boundaries"
|
||||
bl_label = "Select related element type space boundaries"
|
||||
bl_label = "Select Related Element Type Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
related_element: bpy.props.IntProperty()
|
||||
|
||||
@@ -208,7 +208,7 @@ class SelectRelatedElementTypeBoundaries(bpy.types.Operator):
|
||||
|
||||
class SelectSpaceBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_space_boundaries"
|
||||
bl_label = "Select all space boundaries"
|
||||
bl_label = "Select All Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -224,7 +224,7 @@ class SelectSpaceBoundaries(bpy.types.Operator):
|
||||
|
||||
class SelectProjectBoundaries(bpy.types.Operator):
|
||||
bl_idname = "bim.select_project_space_boundaries"
|
||||
bl_label = "Select all project space boundaries"
|
||||
bl_label = "Select All Project Space Boundaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -260,7 +260,7 @@ def get_colour(ifc_boundary):
|
||||
|
||||
class ColourByRelatedBuildingElement(bpy.types.Operator):
|
||||
bl_idname = "bim.colour_by_related_building_element"
|
||||
bl_label = "Apply colour based on related building elements"
|
||||
bl_label = "Apply Colour Based on Related Building Elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -308,7 +308,7 @@ EDITABLE_ATTRIBUTES = {
|
||||
|
||||
class EnableEditingBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_boundary"
|
||||
bl_label = "Edit boundary relations"
|
||||
bl_label = "Edit Boundary Relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -327,7 +327,7 @@ class EnableEditingBoundary(bpy.types.Operator):
|
||||
|
||||
class DisableEditingBoundary(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_boundary"
|
||||
bl_label = "Disable editing boundary relations"
|
||||
bl_label = "Disable Editing Boundary Relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -340,7 +340,7 @@ class DisableEditingBoundary(bpy.types.Operator):
|
||||
|
||||
class EditBoundaryAttributes(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_boundary_attributes"
|
||||
bl_label = "Disable editing boundary relations"
|
||||
bl_label = "Disable Editing Boundary Relations"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -361,7 +361,7 @@ class EditBoundaryAttributes(bpy.types.Operator):
|
||||
|
||||
class UpdateBoundaryGeometry(bpy.types.Operator):
|
||||
bl_idname = "bim.update_boundary_geometry"
|
||||
bl_label = "Update boundary geometry"
|
||||
bl_label = "Update Boundary Geometry"
|
||||
bl_description = """
|
||||
Update boundary connection geometry from mesh.
|
||||
Mesh must lie on a single plane. It should look like a face or a face with holes.
|
||||
|
||||
@@ -156,7 +156,7 @@ class CadTool(WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.hotkey", text="Set gable roof angle").hotkey = "S_R"
|
||||
row.operator("bim.cad_hotkey", text="Set Gable Roof Angle").hotkey = "S_R"
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
|
||||
@@ -149,7 +149,7 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_cost_item"
|
||||
bl_label = "Remove Cost item"
|
||||
bl_label = "Remove Cost Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cost_item: bpy.props.IntProperty()
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ classes = (
|
||||
operator.LoadSheets,
|
||||
operator.OpenSchedule,
|
||||
operator.OpenSheet,
|
||||
operator.ChangeSheetTitleBlock,
|
||||
operator.OpenView,
|
||||
operator.RemoveDrawing,
|
||||
operator.RemoveDrawingFromSheet,
|
||||
|
||||
@@ -92,51 +92,17 @@ class Annotator:
|
||||
|
||||
@staticmethod
|
||||
def add_plane_to_annotation(obj):
|
||||
co1, co2, co3, co4 = Annotator.get_placeholder_coords()
|
||||
co1 = obj.matrix_world.inverted() @ co1 # bot left
|
||||
co2 = obj.matrix_world.inverted() @ co2 # top left
|
||||
co3 = obj.matrix_world.inverted() @ co3 # bot right
|
||||
co4 = obj.matrix_world.inverted() @ co4 # top right
|
||||
# default order = bot left, top left, bot right, top right
|
||||
# therefore we redefine the order
|
||||
face_verts = [0, 2, 3, 1]
|
||||
|
||||
obj.data.vertices.add(4)
|
||||
v1 = obj.data.vertices[-4]
|
||||
v2 = obj.data.vertices[-3]
|
||||
v3 = obj.data.vertices[-2]
|
||||
v4 = obj.data.vertices[-1]
|
||||
v1.co = co4
|
||||
v2.co = co2
|
||||
v3.co = co1
|
||||
v4.co = co3
|
||||
verts_world_space = Annotator.get_placeholder_coords()
|
||||
verts_local = [obj.matrix_world.inverted() @ v for v in verts_world_space]
|
||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True)
|
||||
new_verts = [bm.verts.new(v) for v in verts_local]
|
||||
|
||||
obj.data.edges.add(4)
|
||||
e1 = obj.data.edges[-4]
|
||||
e2 = obj.data.edges[-3]
|
||||
e3 = obj.data.edges[-2]
|
||||
e4 = obj.data.edges[-1]
|
||||
e1.vertices = (v1.index, v2.index)
|
||||
e2.vertices = (v2.index, v3.index)
|
||||
e3.vertices = (v3.index, v4.index)
|
||||
e4.vertices = (v4.index, v1.index)
|
||||
|
||||
obj.data.loops.add(4)
|
||||
l1 = obj.data.loops[-4]
|
||||
l2 = obj.data.loops[-3]
|
||||
l3 = obj.data.loops[-2]
|
||||
l4 = obj.data.loops[-1]
|
||||
l1.vertex_index = v1.index
|
||||
l1.edge_index = e1.index
|
||||
l2.vertex_index = v2.index
|
||||
l2.edge_index = e2.index
|
||||
l3.vertex_index = v3.index
|
||||
l3.edge_index = e3.index
|
||||
l4.vertex_index = v4.index
|
||||
l4.edge_index = e4.index
|
||||
|
||||
obj.data.polygons.add(1)
|
||||
p1 = obj.data.polygons[-1]
|
||||
p1.vertices = (v1.index, v2.index, v3.index, v4.index)
|
||||
p1.loop_start = l1.index
|
||||
p1.loop_total = 4
|
||||
bm.faces.new([new_verts[i] for i in face_verts])
|
||||
tool.Blender.apply_bmesh(obj.data, bm, obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
|
||||
import bpy
|
||||
import blf
|
||||
import math
|
||||
import gpu, bgl
|
||||
import gpu
|
||||
from bpy import types
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils import Vector
|
||||
from mathutils import geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
|
||||
@@ -273,7 +272,7 @@ class CustomGizmo:
|
||||
gpu.matrix.multiply_matrix(self.matrix_world)
|
||||
batch.draw()
|
||||
|
||||
bgl.glDisable(bgl.GL_BLEND)
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
class OffsetHandle:
|
||||
@@ -500,7 +499,7 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides")
|
||||
gz.matrix_basis = basis
|
||||
gz.color = gz.color_highlight = tuple(theme.gizmo_secondary)
|
||||
gz.alpha = gz.alpha_highlight = 0.5
|
||||
gz.alpha = gz.alpha_highlight = 0.75
|
||||
gz.use_draw_modal = True
|
||||
gz.target_set_prop("depth", prop, "value")
|
||||
gz.scale_value = scale_value
|
||||
|
||||
@@ -46,6 +46,7 @@ from mathutils import Vector
|
||||
from timeit import default_timer as timer
|
||||
from blenderbim.bim.module.drawing.prop import RasterStyleProperty, Literal
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from pathlib import Path
|
||||
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
@@ -147,7 +148,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
"Creates/refreshes a .svg drawing based on currently active camera.\n\n"
|
||||
+ "SHIFT+CLICK to print all annotations"
|
||||
)
|
||||
print_all: bpy.props.BoolProperty(name="Print all", default=False)
|
||||
print_all: bpy.props.BoolProperty(name="Print All", default=False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -178,6 +179,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
self.camera = context.scene.camera
|
||||
self.camera_element = tool.Ifc.get_entity(self.camera)
|
||||
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
with profile("Drawing generation process"):
|
||||
@@ -197,14 +199,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.svg_writer.camera_projection = tuple(
|
||||
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
)
|
||||
pset = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]
|
||||
related_paths = {
|
||||
"Stylesheet": pset.get("Stylesheet"),
|
||||
"Markers": pset.get("Markers"),
|
||||
"Symbols": pset.get("Symbols"),
|
||||
"Patterns": pset.get("Patterns"),
|
||||
}
|
||||
self.svg_writer.define_related_paths(**related_paths)
|
||||
|
||||
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
|
||||
|
||||
with profile("Generate underlay"):
|
||||
underlay_svg = self.generate_underlay(context)
|
||||
@@ -236,7 +232,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
def combine_svgs(self, context, underlay, linework, annotation):
|
||||
# Hacky :)
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
|
||||
svg_path = self.get_svg_path()
|
||||
with open(svg_path, "w") as outfile:
|
||||
self.svg_writer.create_blank_svg(svg_path).define_boilerplate()
|
||||
boilerplate = self.svg_writer.svg.tostring()
|
||||
@@ -281,7 +277,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def generate_underlay(self, context):
|
||||
if not self.cprops.has_underlay:
|
||||
return
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-underlay.svg")
|
||||
svg_path = self.get_svg_path(cache_type="underlay")
|
||||
context.scene.render.filepath = svg_path[0:-4] + ".png"
|
||||
drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
|
||||
|
||||
@@ -321,7 +317,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def generate_linework(self, context):
|
||||
if not self.cprops.has_linework:
|
||||
return
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-linework.svg")
|
||||
svg_path = self.get_svg_path(cache_type="linework")
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
|
||||
@@ -604,7 +600,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def generate_annotation(self, context):
|
||||
if not self.cprops.has_annotation:
|
||||
return
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-annotation.svg")
|
||||
svg_path = self.get_svg_path(cache_type="annotation")
|
||||
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
|
||||
return svg_path
|
||||
|
||||
@@ -661,6 +657,17 @@ class CreateDrawing(bpy.types.Operator):
|
||||
return element.LayerSetName
|
||||
return "mat-" + str(element.id())
|
||||
|
||||
def get_svg_path(self, cache_type=None):
|
||||
drawing_path = tool.Drawing.get_document_uri(self.camera_document)
|
||||
drawings_dir = os.path.dirname(drawing_path)
|
||||
|
||||
if cache_type:
|
||||
drawings_dir = os.path.join(drawings_dir, "cache")
|
||||
os.makedirs(drawings_dir, exist_ok=True)
|
||||
return os.path.join(drawings_dir, f"{self.drawing_name}-{cache_type}.svg")
|
||||
os.makedirs(drawings_dir, exist_ok=True)
|
||||
return drawing_path
|
||||
|
||||
|
||||
class AddAnnotation(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_annotation"
|
||||
@@ -668,16 +675,23 @@ class AddAnnotation(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
object_type: bpy.props.StringProperty()
|
||||
data_type: bpy.props.StringProperty()
|
||||
description: bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return IfcStore.get_file() and context.scene.camera
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return operator.description or ""
|
||||
|
||||
def _execute(self, context):
|
||||
drawing = tool.Ifc.get_entity(context.scene.camera)
|
||||
if not drawing:
|
||||
self.report({"WARNING"}, "Not a BIM camera")
|
||||
return
|
||||
# TODO: support adding multiple annotations if there are multiple selected objects
|
||||
# that can be used as annotations references
|
||||
r = core.add_annotation(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing, object_type=self.object_type)
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
@@ -703,7 +717,7 @@ class OpenSheet(bpy.types.Operator, Operator):
|
||||
core.open_sheet(tool.Drawing, sheet=sheet)
|
||||
|
||||
|
||||
class AddDrawingToSheet(bpy.types.Operator):
|
||||
class AddDrawingToSheet(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_drawing_to_sheet"
|
||||
bl_label = "Add Drawing To Sheet"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -713,11 +727,13 @@ class AddDrawingToSheet(bpy.types.Operator):
|
||||
props = context.scene.DocProperties
|
||||
return props.drawings and props.sheets and context.scene.BIMProperties.data_dir
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
props = context.scene.DocProperties
|
||||
active_drawing = props.drawings[props.active_drawing_index]
|
||||
active_sheet = props.sheets[props.active_sheet_index]
|
||||
drawing = tool.Ifc.get().by_id(active_drawing.ifc_definition_id)
|
||||
drawing_reference = tool.Drawing.get_drawing_document(drawing)
|
||||
|
||||
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
|
||||
if not sheet.is_a("IfcDocumentInformation"):
|
||||
return {"FINISHED"}
|
||||
@@ -729,13 +745,7 @@ class AddDrawingToSheet(bpy.types.Operator):
|
||||
|
||||
has_drawing = False
|
||||
for reference in references:
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
element = [r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference][
|
||||
0
|
||||
].RelatedObjects[0]
|
||||
else:
|
||||
element = reference.DocumentRefForObjects[0].RelatedObjects[0]
|
||||
if element == drawing:
|
||||
if reference.Location == drawing_reference.Location:
|
||||
has_drawing = True
|
||||
break
|
||||
|
||||
@@ -744,11 +754,17 @@ class AddDrawingToSheet(bpy.types.Operator):
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))}
|
||||
references = sheet.DocumentReferences
|
||||
id_attr = "ItemReference"
|
||||
else:
|
||||
attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))}
|
||||
references = sheet.HasDocumentReferences
|
||||
id_attr = "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
|
||||
"Location": drawing_reference.Location,
|
||||
"Description": "DRAWING",
|
||||
}
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
tool.Ifc.run("document.assign_document", product=drawing, document=reference)
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
sheet_builder.add_drawing(reference, drawing, sheet)
|
||||
@@ -802,15 +818,28 @@ class CreateSheets(bpy.types.Operator):
|
||||
name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = scene.BIMProperties.data_dir
|
||||
sheet_builder.build(sheet)
|
||||
|
||||
# These variables will be made available to the evaluated commands
|
||||
svg = sheet_builder.build(sheet)
|
||||
basename = os.path.basename(svg)
|
||||
path = os.path.dirname(svg)
|
||||
pdf = os.path.splitext(svg)[0] + ".pdf"
|
||||
eps = os.path.splitext(svg)[0] + ".eps"
|
||||
dxf = os.path.splitext(svg)[0] + ".dxf"
|
||||
|
||||
references = getattr(sheet, "HasDocumentReferences", getattr(sheet, "DocumentReferences", []))
|
||||
if not [r for r in references if r.Description == "SHEET"]:
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
tool.Ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=reference,
|
||||
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
|
||||
)
|
||||
|
||||
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
|
||||
svg2dxf_command = context.preferences.addons["blenderbim"].preferences.svg2dxf_command
|
||||
|
||||
if svg2pdf_command:
|
||||
path = os.path.join(scene.BIMProperties.data_dir, "build", name)
|
||||
svg = os.path.join(path, name + ".svg")
|
||||
pdf = os.path.join(path, name + ".pdf")
|
||||
# With great power comes great responsibility. Example:
|
||||
# [['inkscape', svg, '-o', pdf]]
|
||||
commands = eval(svg2pdf_command)
|
||||
@@ -818,11 +847,6 @@ class CreateSheets(bpy.types.Operator):
|
||||
subprocess.run(command)
|
||||
|
||||
if svg2dxf_command:
|
||||
path = os.path.join(scene.BIMProperties.data_dir, "build", name)
|
||||
svg = os.path.join(path, name + ".svg")
|
||||
eps = os.path.join(path, name + ".eps")
|
||||
dxf = os.path.join(path, name + ".dxf")
|
||||
base = os.path.join(path, name)
|
||||
# With great power comes great responsibility. Example:
|
||||
# [['inkscape', svg, '-o', eps], ['pstoedit', '-dt', '-f', 'dxf:-polyaslines -mm', eps, dxf, '-psarg', '-dNOSAFER']]
|
||||
commands = eval(svg2dxf_command)
|
||||
@@ -832,10 +856,32 @@ class CreateSheets(bpy.types.Operator):
|
||||
if svg2pdf_command:
|
||||
open_with_user_command(context.preferences.addons["blenderbim"].preferences.pdf_command, pdf)
|
||||
else:
|
||||
open_with_user_command(
|
||||
context.preferences.addons["blenderbim"].preferences.svg_command,
|
||||
os.path.join(scene.BIMProperties.data_dir, "build", name, name + ".svg"),
|
||||
)
|
||||
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ChangeSheetTitleBlock(bpy.types.Operator):
|
||||
bl_idname = "bim.change_sheet_title_block"
|
||||
bl_label = "Change Sheet Title Block"
|
||||
bl_description = "Change the title block of the active sheet"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
props = scene.DocProperties
|
||||
|
||||
if not len(props.sheets):
|
||||
return {"CANCELLED"}
|
||||
|
||||
active_sheet = props.sheets[props.active_sheet_index]
|
||||
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = scene.BIMProperties.data_dir
|
||||
titleblock = scene.DocProperties.titleblock
|
||||
sheet_builder.change_titleblock(sheet, titleblock)
|
||||
|
||||
self.report({"INFO"}, 'Title block changed for sheet "{}" to {}'.format(sheet.Name, titleblock))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -843,12 +889,47 @@ class OpenView(bpy.types.Operator):
|
||||
bl_idname = "bim.open_view"
|
||||
bl_label = "Open View"
|
||||
view: bpy.props.StringProperty()
|
||||
bl_description = (
|
||||
"Opens a .svg drawing based on currently active camera with default system viewer\n"
|
||||
+ 'or using "svg_command" from the BlenderBIM preferences (if provided).\n\n'
|
||||
+ "SHIFT+CLICK to open all drawings"
|
||||
)
|
||||
open_all: bpy.props.BoolProperty(name="Open All", default=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# opening all annotations on shift+click
|
||||
if event.type == "LEFTMOUSE" and event.shift:
|
||||
self.open_all = True
|
||||
else:
|
||||
# can't rely on default value since `self.open_all = True`
|
||||
# will set the value to `True` for all future calls
|
||||
self.open_all = False
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
open_with_user_command(
|
||||
context.preferences.addons["blenderbim"].preferences.svg_command,
|
||||
os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.view + ".svg"),
|
||||
)
|
||||
if self.open_all:
|
||||
drawings = [tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings]
|
||||
else:
|
||||
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
|
||||
|
||||
drawing_uris = []
|
||||
drawings_not_found = []
|
||||
|
||||
for drawing in drawings:
|
||||
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
|
||||
drawing_uris.append(drawing_uri)
|
||||
if not os.path.exists(drawing_uri):
|
||||
drawings_not_found.append(drawing.Name)
|
||||
|
||||
if drawings_not_found:
|
||||
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
|
||||
"\n".join(drawings_not_found)
|
||||
)
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
for drawing_uri in drawing_uris:
|
||||
open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, drawing_uri)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1072,12 +1153,15 @@ class AddSchedule(bpy.types.Operator, Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
|
||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||
|
||||
def _execute(self, context):
|
||||
filepath = self.filepath
|
||||
if self.use_relative_path:
|
||||
filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
|
||||
ifc_path = tool.Ifc.get_path()
|
||||
if os.path.isfile(ifc_path):
|
||||
ifc_path = os.path.dirname(ifc_path)
|
||||
filepath = os.path.relpath(filepath, ifc_path)
|
||||
core.add_schedule(
|
||||
tool.Ifc,
|
||||
tool.Drawing,
|
||||
@@ -1118,25 +1202,29 @@ class BuildSchedule(bpy.types.Operator, Operator):
|
||||
core.build_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
|
||||
|
||||
|
||||
class AddScheduleToSheet(bpy.types.Operator):
|
||||
class AddScheduleToSheet(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.add_schedule_to_sheet"
|
||||
bl_label = "Add Schedule To Sheet"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
# TODO: check undo redo
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.DocProperties
|
||||
return props.schedules and props.sheets and context.scene.BIMProperties.data_dir
|
||||
|
||||
def execute(self, context):
|
||||
def _execute(self, context):
|
||||
props = context.scene.DocProperties
|
||||
active_schedule = props.schedules[props.active_schedule_index]
|
||||
active_sheet = props.sheets[props.active_sheet_index]
|
||||
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
schedule_location = tool.Drawing.get_path_with_ext(schedule.DocumentReferences[0].Location, "svg")
|
||||
else:
|
||||
schedule_location = tool.Drawing.get_path_with_ext(schedule.HasDocumentReferences[0].Location, "svg")
|
||||
|
||||
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
|
||||
if not sheet.is_a("IfcDocumentInformation"):
|
||||
return {"FINISHED"}
|
||||
return
|
||||
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
references = sheet.DocumentReferences or []
|
||||
@@ -1145,19 +1233,25 @@ class AddScheduleToSheet(bpy.types.Operator):
|
||||
|
||||
has_schedule = False
|
||||
for reference in references:
|
||||
if reference.Location == tool.Drawing.get_schedule_location(schedule):
|
||||
if reference.Location == schedule_location:
|
||||
has_schedule = True
|
||||
break
|
||||
|
||||
if has_schedule:
|
||||
return {"FINISHED"}
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))}
|
||||
references = sheet.DocumentReferences
|
||||
id_attr = "ItemReference"
|
||||
else:
|
||||
attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))}
|
||||
attributes["Location"] = tool.Drawing.get_schedule_location(schedule)
|
||||
references = sheet.HasDocumentReferences
|
||||
id_attr = "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
|
||||
"Location": schedule_location,
|
||||
"Description": "SCHEDULE",
|
||||
}
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
@@ -1165,7 +1259,6 @@ class AddScheduleToSheet(bpy.types.Operator):
|
||||
sheet_builder.add_schedule(reference, schedule, sheet)
|
||||
|
||||
tool.Drawing.import_sheets()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddDrawingStyleAttribute(bpy.types.Operator):
|
||||
@@ -1219,7 +1312,7 @@ class EditTextPopup(bpy.types.Operator):
|
||||
props = context.active_object.BIMTextProperties
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="Add literal")
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="Add Literal")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "font_size")
|
||||
@@ -1302,7 +1395,7 @@ class DisableEditingText(bpy.types.Operator, Operator):
|
||||
|
||||
class AddTextLiteral(bpy.types.Operator):
|
||||
bl_idname = "bim.add_text_literal"
|
||||
bl_label = "Add text literal"
|
||||
bl_label = "Add Text Literal"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -1337,7 +1430,7 @@ class AddTextLiteral(bpy.types.Operator):
|
||||
|
||||
class RemoveTextLiteral(bpy.types.Operator):
|
||||
bl_idname = "bim.remove_text_literal"
|
||||
bl_label = "Remove text literal"
|
||||
bl_label = "Remove Text Literal"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
literal_prop_id: bpy.props.IntProperty()
|
||||
@@ -1387,6 +1480,23 @@ class LoadSheets(bpy.types.Operator, Operator):
|
||||
def _execute(self, context):
|
||||
core.load_sheets(tool.Drawing)
|
||||
|
||||
props = context.scene.DocProperties
|
||||
sheets_not_found = []
|
||||
for sheet_prop in props.sheets:
|
||||
if not sheet_prop.is_sheet:
|
||||
continue
|
||||
|
||||
sheet = tool.Ifc.get().by_id(sheet_prop.ifc_definition_id)
|
||||
document_uri = tool.Drawing.get_document_uri(sheet)
|
||||
|
||||
filepath = Path(document_uri)
|
||||
if not filepath.is_file():
|
||||
sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}"
|
||||
sheets_not_found.append(f'"{sheet_name}" - {document_uri}')
|
||||
|
||||
if sheets_not_found:
|
||||
self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found))
|
||||
|
||||
|
||||
class DisableEditingSheets(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.disable_editing_sheets"
|
||||
|
||||
@@ -250,11 +250,18 @@ class Schedule(PropertyGroup):
|
||||
class Sheet(PropertyGroup):
|
||||
def set_name(self, new):
|
||||
old = self.get("name")
|
||||
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets")
|
||||
if old and os.path.isfile(os.path.join(path, old + ".svg")):
|
||||
os.rename(os.path.join(path, old + ".svg"), os.path.join(path, new + ".svg"))
|
||||
|
||||
if new == old:
|
||||
return
|
||||
sheet = tool.Ifc.get().by_id(self.ifc_definition_id)
|
||||
old_path = Path(tool.Drawing.get_document_uri(sheet))
|
||||
core.update_sheet_name(tool.Ifc, tool.Drawing, sheet=sheet, name=new)
|
||||
self["name"] = new
|
||||
|
||||
new_path = Path(tool.Drawing.get_document_uri(sheet))
|
||||
if old and old_path.is_file():
|
||||
old_path.rename(new_path)
|
||||
|
||||
def get_name(self):
|
||||
return self.get("name")
|
||||
|
||||
@@ -347,6 +354,14 @@ class DocProperties(PropertyGroup):
|
||||
decorations_colour: FloatVectorProperty(
|
||||
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
docs_dir: StringProperty(default=os.path.join("docs") + os.path.sep, name="Default Docs Directory")
|
||||
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
|
||||
titleblocks_dir: StringProperty(default=os.path.join("sheets", "titleblocks") + os.path.sep, name="Default Titleblocks Directory")
|
||||
drawings_dir: StringProperty(default=os.path.join("drawings") + os.path.sep, name="Default Drawings Directory")
|
||||
stylesheet_path: StringProperty(default=os.path.join("drawings", "assets", "default.css"), name="Default Stylesheet")
|
||||
markers_path: StringProperty(default=os.path.join("drawings", "assets", "markers.svg"), name="Default Markers")
|
||||
symbols_path: StringProperty(default=os.path.join("drawings", "assets", "symbols.svg"), name="Default Symbols")
|
||||
patterns_path: StringProperty(default=os.path.join("drawings", "assets", "patterns.svg"), name="Default Patterns")
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
|
||||
@@ -16,10 +16,177 @@
|
||||
# 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 bgl
|
||||
from mathutils import Matrix
|
||||
from gpu.types import GPUShader
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import gpu
|
||||
|
||||
|
||||
BASE_DEF_GLSL = """
|
||||
#define PI 3.141592653589793
|
||||
#define MAX_POINTS 64
|
||||
#define CIRCLE_SEGS 12
|
||||
#define SMOOTH_WIDTH 1.0
|
||||
#define lineSmooth true
|
||||
"""
|
||||
|
||||
# since `bgl` is deprecated, creating smoothing lines became tricky
|
||||
# Notes for creating shaders with smoothed lines:
|
||||
# in geom shader - use triangle_strip, DEFAULT_SETUP, do_edge_verts or do_vertex to emit vertices
|
||||
# in frag shader - use lineWidth uniform, smoothline flaot in, smoothing shader code from base shader
|
||||
# mind the vertex limit since emitting vertices for smoothed lines produces twice as much vertices
|
||||
|
||||
BASE_LIB_GLSL = """
|
||||
// TODO: redefine as macor instead
|
||||
uniform vec2 winsize;
|
||||
uniform float lineWidth;
|
||||
#define half_winsize (winsize / 2)
|
||||
// convert camera to window
|
||||
#define C2W(v) vec4(v.x * half_winsize.x / v.w, v.y * half_winsize.y / v.w, v.z / v.w, 1)
|
||||
// convert window to camera
|
||||
#define W2C(v) vec4(v.x * v.w / half_winsize.x, v.y * v.w / half_winsize.y, v.z * v.w, 1)
|
||||
|
||||
void emitSegment(vec4 p0, vec4 p1) {
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
gl_Position = p2;
|
||||
EmitVertex();
|
||||
gl_Position = p3;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
#define matCLIP2WIN() vec4(winsize.x/2, winsize.y/2, 1, 1)
|
||||
#define CLIP2WIN(v) (clip2win * (v) / (v).w)
|
||||
#define matWIN2CLIP() vec4(2/winsize.x, 2/winsize.y, 1, 1)
|
||||
#define WIN2CLIP(v) (win2clip * (v) * (v).w)
|
||||
#define DEFAULT_SETUP() vec4 clip2win = matCLIP2WIN(), win2clip = matWIN2CLIP(); vec2 EDGE_DIR
|
||||
|
||||
out float smoothline;
|
||||
|
||||
void arrow_head(in vec4 dir, in float size, in float angle, out vec4 head[3]) {
|
||||
vec4 nose = dir * size;
|
||||
float c = cos(angle), s = sin(angle);
|
||||
head[0] = nose;
|
||||
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
|
||||
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
|
||||
}
|
||||
|
||||
void circle_head(in float size, out vec4 head[CIRCLE_SEGS]) {
|
||||
float angle_d = PI * 2 / CIRCLE_SEGS;
|
||||
for(int i = 0; i<CIRCLE_SEGS; i++) {
|
||||
float angle = angle_d * i;
|
||||
head[i] = vec4(cos(angle), sin(angle), 0, 0) * size;
|
||||
}
|
||||
}
|
||||
|
||||
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
|
||||
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
|
||||
}
|
||||
|
||||
void angle_circle_head(
|
||||
in vec4 circle_start, in float circle_angle,
|
||||
in bool counterclockwise,
|
||||
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
|
||||
|
||||
// 1 added to CIRCLE_SEGS because we're number of vertices
|
||||
// for n segments is n+1
|
||||
|
||||
float angle_d;
|
||||
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
|
||||
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
|
||||
angle_segs = max(1, ceil(circle_angle / angle_d));
|
||||
angle_d = circle_angle / angle_segs;
|
||||
|
||||
for(int i = 0; i < (angle_segs + 1); i++) {
|
||||
float angle = angle_d * i;
|
||||
if (counterclockwise) {
|
||||
head[i] = vec4(
|
||||
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
|
||||
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
|
||||
0, 0
|
||||
);
|
||||
} else {
|
||||
head[i] = vec4(
|
||||
circle_start.x * cos(angle) + circle_start.y * sin(angle),
|
||||
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
|
||||
0, 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
|
||||
vec4 nose = dir * size;
|
||||
float c = cos(PI/2), s = sin(PI/2);
|
||||
head[0] = nose;
|
||||
head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0);
|
||||
head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0);
|
||||
}
|
||||
|
||||
// do_vertex_util is custom EmitVertex method to support line smoothing
|
||||
// `e` - edge xy direction in win/clip space (directions are invariant in both spaces)
|
||||
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
|
||||
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
|
||||
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// to avoid smoothing artifacts
|
||||
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
|
||||
// `pos0` / `pos1` - vertex position in clip space
|
||||
#define do_edge_verts(pos0, pos1) ( EDGE_DIR = normalize( ( (pos1) - (pos0) ).xy ), do_vertex( pos0, EDGE_DIR ), do_vertex( pos1, EDGE_DIR) )
|
||||
#define do_edge_verts_win(pos0, pos1) ( do_edge_verts( WIN2CLIP(pos0), WIN2CLIP(pos1) ) )
|
||||
void do_vertex_util(vec4 pos, vec2 ofs)
|
||||
{
|
||||
float final_line_width = lineWidth + SMOOTH_WIDTH * float(lineSmooth);
|
||||
ofs *= final_line_width;
|
||||
|
||||
smoothline = final_line_width * 0.5;
|
||||
gl_Position = pos;
|
||||
gl_Position.xy += ofs * pos.w;
|
||||
EmitVertex();
|
||||
|
||||
smoothline = -final_line_width * 0.5;
|
||||
gl_Position = pos;
|
||||
gl_Position.xy -= ofs * pos.w;
|
||||
EmitVertex();
|
||||
}
|
||||
|
||||
// geometry utils
|
||||
void triangle_head(in vec4 side, in vec4 dir, in float length, in float width, in float radius, out vec4 head[5]) {
|
||||
vec4 nose = side * length;
|
||||
vec4 ear = dir * width;
|
||||
head[0] = side * -radius;
|
||||
head[1] = nose * -.5;
|
||||
head[2] = vec4(0) + ear;
|
||||
head[3] = nose * .5;
|
||||
head[4] = side * radius;
|
||||
}
|
||||
|
||||
void do_triangle_head(vec4 pos_w, vec4 head[5]) {
|
||||
DEFAULT_SETUP();
|
||||
do_edge_verts_win(pos_w + head[0], pos_w + head[1]);
|
||||
do_edge_verts_win(pos_w + head[1], pos_w + head[2]);
|
||||
do_edge_verts_win(pos_w + head[2], pos_w + head[3]);
|
||||
do_edge_verts_win(pos_w + head[3], pos_w + head[4]);
|
||||
}
|
||||
|
||||
void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
|
||||
DEFAULT_SETUP();
|
||||
for(int i=0; i<CIRCLE_SEGS-1; i++) {
|
||||
do_edge_verts_win(pos_w + head[i], pos_w + head[i+1]);
|
||||
EmitVertex();
|
||||
}
|
||||
do_edge_verts_win( pos_w + head[CIRCLE_SEGS-1], pos_w + head[0] );
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class BaseShader:
|
||||
@@ -28,7 +195,7 @@ class BaseShader:
|
||||
|
||||
The Geometry shader works in clipping coords (aftre projecting before division and window scaling).
|
||||
To make window-scale geometry, vectors should be calculated in window space and than back-projected to clipping spae.
|
||||
Provide `winSize` uniform vector with halfsize of window, and use W2C and C2W macros.
|
||||
Provide `half_winsize` uniform vector with halfsize of window, and use W2C and C2W macros.
|
||||
|
||||
Replace glsl code in derived classes.
|
||||
Beware of unused attributes and uniforms: glsl compiler will optimize them out and blender fail with an exception.
|
||||
@@ -43,9 +210,7 @@ class BaseShader:
|
||||
|
||||
TYPE = None # should be LINES|POINTS|etc
|
||||
|
||||
DEF_GLSL = """
|
||||
#define PI 3.141592653589793
|
||||
"""
|
||||
DEF_GLSL = BASE_DEF_GLSL
|
||||
|
||||
VERT_GLSL = """
|
||||
uniform mat4 viewMatrix;
|
||||
@@ -63,46 +228,45 @@ class BaseShader:
|
||||
"""
|
||||
|
||||
# prepended to geom_glsl
|
||||
LIB_GLSL = """
|
||||
uniform vec2 winSize;
|
||||
// convert camera to window
|
||||
#define C2W(v) vec4(v.x * winSize.x / v.w, v.y * winSize.y / v.w, v.z / v.w, 1)
|
||||
// convert window to camera
|
||||
#define W2C(v) vec4(v.x * v.w / winSize.x, v.y * v.w / winSize.y, v.z * v.w, 1)
|
||||
|
||||
void emitSegment(vec4 p0, vec4 p1) {
|
||||
gl_Position = p0;
|
||||
EmitVertex();
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
void emitTriangle(vec4 p1, vec4 p2, vec4 p3) {
|
||||
gl_Position = p1;
|
||||
EmitVertex();
|
||||
gl_Position = p2;
|
||||
EmitVertex();
|
||||
gl_Position = p3;
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
LIB_GLSL = BASE_LIB_GLSL
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
|
||||
void main() {
|
||||
// default setup for macro to work
|
||||
vec4 clip2win = matCLIP2WIN();
|
||||
vec4 win2clip = matWIN2CLIP();
|
||||
vec2 EDGE_DIR;
|
||||
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
do_edge_verts(p0, p1);
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
|
||||
FRAG_GLSL = """
|
||||
uniform vec4 color;
|
||||
uniform float lineWidth;
|
||||
|
||||
in float smoothline;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec2 co = gl_FragCoord.xy;
|
||||
|
||||
fragColor = color;
|
||||
if (lineSmooth) {
|
||||
fragColor.a *= clamp((lineWidth + SMOOTH_WIDTH) * 0.5 - abs(smoothline), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# NB: libcode arg doesn't work
|
||||
# TODO: rename to .shader
|
||||
self.prog = GPUShader(
|
||||
vertexcode=self.VERT_GLSL,
|
||||
fragcode=self.FRAG_GLSL,
|
||||
@@ -120,27 +284,29 @@ class BaseShader:
|
||||
self.prog.bind()
|
||||
|
||||
def glenable(self):
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
bgl.glBlendEquation(bgl.GL_FUNC_ADD)
|
||||
|
||||
# bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||
# bgl.glDepthFunc(bgl.GL_LEQUAL)
|
||||
# bgl.glDepthMask(True)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.depth_test_set("LESS_EQUAL")
|
||||
|
||||
def uniform_region(self, ctx):
|
||||
region = ctx.region
|
||||
region3d = ctx.region_data
|
||||
try:
|
||||
self.prog.uniform_float("viewMatrix", region3d.perspective_matrix)
|
||||
except ValueError: # unused uniform
|
||||
pass
|
||||
try:
|
||||
self.prog.uniform_float("winSize", (region.width / 2, region.height / 2))
|
||||
except ValueError: # unused uniform
|
||||
pass
|
||||
|
||||
uniform_floats = {
|
||||
"viewMatrix": region3d.perspective_matrix,
|
||||
"winsize": (region.width, region.height),
|
||||
"lineWidth": 2.5,
|
||||
}
|
||||
|
||||
for name, value in uniform_floats.items():
|
||||
try:
|
||||
self.prog.uniform_float(name, value)
|
||||
# TODO: shouldn't just try'n'catch them
|
||||
# because they may indicate errors in code
|
||||
except ValueError: # unused uniform
|
||||
pass
|
||||
|
||||
|
||||
# TODO: add smoothing if this shades is going to be used
|
||||
class BaseLinesShader(BaseShader):
|
||||
"""Draws line segments with gaps around vertices at endpoints"""
|
||||
|
||||
@@ -185,7 +351,6 @@ class BaseLinesShader(BaseShader):
|
||||
|
||||
def glenable(self):
|
||||
super().glenable()
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
|
||||
class GizmoShader(BaseShader):
|
||||
@@ -207,6 +372,7 @@ class GizmoShader(BaseShader):
|
||||
"""
|
||||
|
||||
|
||||
# TODO: add smoothing if this shader is going to be used
|
||||
class DotsGizmoShader(GizmoShader):
|
||||
"""Draws circles of radius 1 around points"""
|
||||
|
||||
@@ -274,24 +440,26 @@ class ExtrusionGuidesShader(GizmoShader):
|
||||
uniform mat4 ModelViewProjectionMatrix;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=10) out;
|
||||
layout(triangle_strip, max_vertices=MAX_POINTS) out;
|
||||
|
||||
void main() {
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
vec4 p0w = C2W(p0), p1w = C2W(p1);
|
||||
// default setup for macro to work
|
||||
vec2 EDGE_DIR;
|
||||
|
||||
emitSegment(p0, p1);
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
do_edge_verts(p0, p1);
|
||||
EndPrimitive();
|
||||
|
||||
vec4 bx = ModelViewProjectionMatrix[0] * CROSS_SIZE;
|
||||
vec4 by = ModelViewProjectionMatrix[1] * CROSS_SIZE;
|
||||
|
||||
emitSegment(p0 - bx, p0 + bx);
|
||||
emitSegment(p0 - by, p0 + by);
|
||||
emitSegment(p1 - bx, p1 + bx);
|
||||
emitSegment(p1 - by, p1 + by);
|
||||
do_edge_verts(p0 - bx, p0 + bx);
|
||||
EndPrimitive();
|
||||
do_edge_verts(p0 - by, p0 + by);
|
||||
EndPrimitive();
|
||||
do_edge_verts(p1 - bx, p1 + bx);
|
||||
EndPrimitive();
|
||||
do_edge_verts(p1 - by, p1 + by);
|
||||
EndPrimitive();
|
||||
}
|
||||
"""
|
||||
|
||||
def glenable(self):
|
||||
super().glenable()
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import uuid
|
||||
import shutil
|
||||
import ntpath
|
||||
import pystache
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.util.geolocation
|
||||
from shutil import copy
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
@@ -34,21 +35,24 @@ class SheetBuilder:
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, sheet_path, titleblock_name):
|
||||
sheet_dir = os.path.dirname(sheet_path)
|
||||
|
||||
root = ET.Element("svg")
|
||||
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
||||
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
root.attrib["id"] = "root"
|
||||
root.attrib["version"] = "1.1"
|
||||
|
||||
view_root = ET.parse(
|
||||
os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
).getroot()
|
||||
titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
view_root = ET.parse(titleblock_path).getroot()
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
view = ET.SubElement(root, "g")
|
||||
view.attrib["data-type"] = "titleblock"
|
||||
titleblock = ET.SubElement(view, "image")
|
||||
titleblock.attrib["xlink:href"] = "../templates/titleblocks/" + titleblock_name + ".svg"
|
||||
titleblock.attrib["xlink:href"] = os.path.relpath(
|
||||
tool.Drawing.get_default_titleblock_path(titleblock_name), sheet_dir
|
||||
)
|
||||
titleblock.attrib["x"] = "0"
|
||||
titleblock.attrib["y"] = "0"
|
||||
titleblock.attrib["width"] = str(view_width)
|
||||
@@ -58,17 +62,23 @@ class SheetBuilder:
|
||||
root.attrib["height"] = "{}mm".format(view_height)
|
||||
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
|
||||
|
||||
os.makedirs(sheet_dir, exist_ok=True)
|
||||
os.makedirs(os.path.join(sheet_dir, "titleblocks"), exist_ok=True)
|
||||
sheet_titleblock_path = os.path.join(sheet_dir, "titleblocks", titleblock_name + ".svg")
|
||||
if not os.path.exists(sheet_titleblock_path):
|
||||
shutil.copy(titleblock_path, sheet_titleblock_path)
|
||||
with open(sheet_path, "w") as f:
|
||||
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
|
||||
|
||||
def add_drawing(self, reference, drawing, sheet):
|
||||
filename = drawing.Name
|
||||
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
sheet_dir = os.path.join(self.data_dir, "sheets")
|
||||
drawing_dir = os.path.join(self.data_dir, "diagrams")
|
||||
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
|
||||
drawing_path = os.path.join(drawing_dir, filename + ".svg")
|
||||
underlay_path = os.path.join(drawing_dir, filename + "-underlay.png")
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet)
|
||||
sheet_name = os.path.splitext(os.path.basename(sheet_path))[0]
|
||||
sheet_dir = os.path.dirname(sheet_path)
|
||||
|
||||
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
|
||||
drawing_path = tool.Ifc.resolve_uri(drawing_path)
|
||||
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
|
||||
|
||||
if not os.path.isfile(sheet_path):
|
||||
raise FileNotFoundError
|
||||
@@ -110,7 +120,7 @@ class SheetBuilder:
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(30, view_height + 35, view)
|
||||
self.add_view_title(30, view_height + 35, view, sheet_dir)
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def remove_drawing(self, reference, sheet):
|
||||
@@ -128,11 +138,12 @@ class SheetBuilder:
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def add_schedule(self, reference, schedule, sheet):
|
||||
view_path = tool.Drawing.get_document_uri(schedule)
|
||||
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg")
|
||||
if not os.path.exists(view_path):
|
||||
tool.Drawing.create_svg_schedule(schedule)
|
||||
schedule_name = os.path.splitext(os.path.basename(view_path))[0]
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet)
|
||||
sheet_dir = os.path.dirname(sheet_path)
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
@@ -152,31 +163,40 @@ class SheetBuilder:
|
||||
|
||||
foreground = ET.SubElement(view, "image")
|
||||
foreground.attrib["data-type"] = "table"
|
||||
foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name)
|
||||
foreground.attrib["xlink:href"] = os.path.relpath(view_path, sheet_dir)
|
||||
foreground.attrib["x"] = "30"
|
||||
foreground.attrib["y"] = "30"
|
||||
foreground.attrib["width"] = str(view_width)
|
||||
foreground.attrib["height"] = str(view_height)
|
||||
|
||||
self.add_view_title(30, view_height + 35, view)
|
||||
self.add_view_title(30, view_height + 35, view, sheet_dir)
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def add_view_title(self, x, y, parent):
|
||||
title_tree = ET.parse(os.path.join(self.data_dir, "templates", "view-title.svg"))
|
||||
def add_view_title(self, x, y, parent, sheet_dir):
|
||||
title_path = os.path.join(sheet_dir, "assets", "view-title.svg")
|
||||
os.makedirs(os.path.dirname(title_path), exist_ok=True)
|
||||
if not os.path.exists(title_path):
|
||||
ootb_title = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "view-title.svg")
|
||||
shutil.copy(ootb_title, title_path)
|
||||
|
||||
title_tree = ET.parse(title_path)
|
||||
title_root = title_tree.getroot()
|
||||
title = ET.SubElement(parent, "image")
|
||||
title.attrib["data-type"] = "view-title"
|
||||
title.attrib["xlink:href"] = "../templates/view-title.svg"
|
||||
title.attrib["xlink:href"] = os.path.relpath(title_path, sheet_dir)
|
||||
title.attrib["x"] = str(x)
|
||||
title.attrib["y"] = str(y)
|
||||
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
|
||||
|
||||
def build(self, sheet):
|
||||
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet)
|
||||
sheet_name = os.path.splitext(os.path.basename(sheet_path))[0]
|
||||
self.sheet_dir = os.path.dirname(sheet_path)
|
||||
|
||||
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
|
||||
docs_dir = tool.Ifc.resolve_uri(os.path.join(bpy.context.scene.DocProperties.docs_dir, sheet_name))
|
||||
|
||||
os.makedirs(docs_dir, exist_ok=True)
|
||||
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
@@ -191,9 +211,13 @@ class SheetBuilder:
|
||||
self.build_drawings(root, sheet)
|
||||
self.build_schedules(root)
|
||||
|
||||
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
|
||||
output_filename = os.path.join(docs_dir, f"{sheet_name}.svg")
|
||||
|
||||
with open(output_filename, "wb") as output:
|
||||
tree.write(output)
|
||||
|
||||
return output_filename
|
||||
|
||||
def build_titleblock(self, root, sheet):
|
||||
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
|
||||
@@ -233,7 +257,7 @@ class SheetBuilder:
|
||||
|
||||
if background is not None:
|
||||
background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
|
||||
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
|
||||
shutil.copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
|
||||
|
||||
if view_title is not None:
|
||||
foreground_path = self.get_href(foreground)
|
||||
@@ -298,7 +322,7 @@ class SheetBuilder:
|
||||
self.defs.append(clip_path)
|
||||
|
||||
svg_path = self.get_href(image)
|
||||
with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
|
||||
with open(os.path.join(self.sheet_dir, svg_path), "r") as template:
|
||||
embedded = ET.fromstring(pystache.render(template.read(), data))
|
||||
# viewBox should not be nested
|
||||
embedded.attrib["viewBox"] = ""
|
||||
@@ -314,6 +338,33 @@ class SheetBuilder:
|
||||
group.append(child)
|
||||
return group
|
||||
|
||||
def change_titleblock(self, sheet, titleblock_name):
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
|
||||
|
||||
titleblock_svg_path = os.path.join(self.data_dir, "templates", "titleblocks", f"{titleblock_name}.svg")
|
||||
view_root = ET.parse(titleblock_svg_path).getroot()
|
||||
view_width = self.convert_to_mm(view_root.attrib.get("width"))
|
||||
view_height = self.convert_to_mm(view_root.attrib.get("height"))
|
||||
|
||||
sheet_dir = os.path.join(self.data_dir, "sheets")
|
||||
sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
|
||||
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
|
||||
sheet_tree = ET.parse(sheet_path)
|
||||
root = sheet_tree.getroot()
|
||||
|
||||
titleblock = sheet_tree.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image[@{http://www.w3.org/1999/xlink}href]")[0]
|
||||
image.attrib["{http://www.w3.org/1999/xlink}href"] = f"../templates/titleblocks/{titleblock_name}.svg"
|
||||
image.attrib["width"] = str(view_width)
|
||||
image.attrib["height"] = str(view_height)
|
||||
|
||||
root.attrib["width"] = "{}mm".format(view_width)
|
||||
root.attrib["height"] = "{}mm".format(view_height)
|
||||
root.attrib["viewBox"] = "0 0 {} {}".format(view_width, view_height)
|
||||
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
# CSS is what defines these possibilities
|
||||
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
|
||||
|
||||
@@ -21,6 +21,7 @@ import re
|
||||
import bpy
|
||||
import math
|
||||
import bmesh
|
||||
import shutil
|
||||
import pystache
|
||||
import mathutils
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -65,10 +66,9 @@ class SvgWriter:
|
||||
self.scale = 1 / 100 # 1:100
|
||||
self.camera_width = None
|
||||
self.camera_height = None
|
||||
self.related_paths = None
|
||||
self.resource_paths = {}
|
||||
|
||||
def create_blank_svg(self, output_path):
|
||||
self.define_related_paths() # making sure all paths are defined
|
||||
self.calculate_scale()
|
||||
self.svg = svgwrite.Drawing(
|
||||
output_path,
|
||||
@@ -94,26 +94,20 @@ class SvgWriter:
|
||||
)
|
||||
return self
|
||||
|
||||
def define_related_paths(self, **related_paths):
|
||||
if not self.related_paths:
|
||||
self.related_paths = {}
|
||||
|
||||
if not related_paths:
|
||||
related_paths = {
|
||||
"Stylesheet": os.path.join(self.data_dir, "styles", f"default.css"),
|
||||
"Markers": os.path.join(self.data_dir, "templates", "markers.svg"),
|
||||
"Symbols": os.path.join(self.data_dir, "templates", "symbols.svg"),
|
||||
"Patterns": os.path.join(self.data_dir, "templates", "patterns.svg"),
|
||||
}
|
||||
for path_name in list(related_paths.keys()):
|
||||
if path_name in self.related_paths:
|
||||
del related_paths[path_name]
|
||||
|
||||
for path_name in related_paths:
|
||||
uri = related_paths[path_name]
|
||||
custom_path = tool.Ifc.resolve_uri(uri)
|
||||
if custom_path:
|
||||
self.related_paths[path_name] = custom_path
|
||||
def setup_drawing_resource_paths(self, element):
|
||||
pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing")
|
||||
for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"):
|
||||
resource_path = pset.get(resource)
|
||||
if not resource_path:
|
||||
self.resource_paths[resource] = None
|
||||
continue
|
||||
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
|
||||
if not os.path.exists(resource_path):
|
||||
resource_basename = os.path.basename(resource_path)
|
||||
ootb_resource = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", resource_basename)
|
||||
if os.path.exists(ootb_resource):
|
||||
shutil.copy(ootb_resource, resource_path)
|
||||
self.resource_paths[resource] = tool.Ifc.resolve_uri(resource_path)
|
||||
|
||||
def define_boilerplate(self):
|
||||
self.add_stylesheet()
|
||||
@@ -132,28 +126,36 @@ class SvgWriter:
|
||||
self.height = self.raw_height * self.svg_scale
|
||||
|
||||
def add_stylesheet(self):
|
||||
with open(self.related_paths["Stylesheet"], "r") as stylesheet:
|
||||
if not self.resource_paths["Stylesheet"] or not os.path.exists(self.resource_paths["Stylesheet"]):
|
||||
return
|
||||
with open(self.resource_paths["Stylesheet"], "r") as stylesheet:
|
||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||
|
||||
def add_markers(self):
|
||||
tree = ET.parse(self.related_paths["Markers"])
|
||||
if not self.resource_paths["Markers"] or not os.path.exists(self.resource_paths["Markers"]):
|
||||
return
|
||||
tree = ET.parse(self.resource_paths["Markers"])
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def add_symbols(self):
|
||||
tree = ET.parse(self.related_paths["Symbols"])
|
||||
if not self.resource_paths["Symbols"] or not os.path.exists(self.resource_paths["Symbols"]):
|
||||
return
|
||||
tree = ET.parse(self.resource_paths["Symbols"])
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
|
||||
def find_xml_symbol_by_id(self, id):
|
||||
tree = ET.parse(self.related_paths["Symbols"])
|
||||
tree = ET.parse(self.resource_paths["Symbols"])
|
||||
xml_symbol = tree.find(f'.//*[@id="{id}"]')
|
||||
return External(xml_symbol) if xml_symbol else None
|
||||
|
||||
def add_patterns(self):
|
||||
tree = ET.parse(self.related_paths["Patterns"])
|
||||
if not self.resource_paths["Patterns"] or not os.path.exists(self.resource_paths["Patterns"]):
|
||||
return
|
||||
tree = ET.parse(self.resource_paths["Patterns"])
|
||||
root = tree.getroot()
|
||||
for child in root:
|
||||
self.svg.defs.add(External(child))
|
||||
@@ -662,7 +664,8 @@ class SvgWriter:
|
||||
)
|
||||
|
||||
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
|
||||
classes_str = " ".join(self.get_attribute_classes(text_obj))
|
||||
classes = self.get_attribute_classes(text_obj)
|
||||
classes_str = " ".join(classes)
|
||||
|
||||
symbol = tool.Drawing.get_annotation_symbol(element)
|
||||
template_text_fields = []
|
||||
@@ -731,12 +734,24 @@ class SvgWriter:
|
||||
)
|
||||
self.svg.add(text_tag)
|
||||
|
||||
for line_number, text_line in enumerate(text.replace("\\n", "\n").split("\n")):
|
||||
text_lines = text.replace("\\n", "\n").split("\n")
|
||||
|
||||
# adding fill background tspan
|
||||
# tspans and text doesn't support setting up "background-color"
|
||||
# so we add a filter, we do it in separate span because otherwise it will produce blurry image
|
||||
# adding just 1 tspan seems to work
|
||||
if "fill-bg" in classes:
|
||||
tspan = self.svg.tspan(
|
||||
text_lines[0], class_=classes_str, insert=text_position_svg, filter="url(#fill-background)"
|
||||
)
|
||||
text_tag.add(tspan)
|
||||
|
||||
for line_number, text_line in enumerate(text_lines):
|
||||
# position has to be inserted at tspan to avoid x offset between tspans
|
||||
t_span = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
|
||||
tspan = self.svg.tspan(text_line, class_=classes_str, insert=text_position_svg)
|
||||
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
|
||||
t_span.update({"dy": f"{line_number}em"})
|
||||
text_tag.add(t_span)
|
||||
tspan.update({"dy": f"{line_number}em"})
|
||||
text_tag.add(tspan)
|
||||
|
||||
def draw_break_annotations(self, obj):
|
||||
x_offset = self.raw_width / 2
|
||||
|
||||
@@ -274,6 +274,7 @@ class BIM_PT_sheets(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "titleblock", text="")
|
||||
row.operator("bim.add_sheet", text="", icon="ADD")
|
||||
row.operator("bim.change_sheet_title_block", text="", icon="MODIFIER_DATA")
|
||||
row.operator("bim.disable_editing_sheets", text="", icon="CANCEL")
|
||||
|
||||
if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets):
|
||||
@@ -446,6 +447,7 @@ class BIM_PT_annotation_utilities(Panel):
|
||||
op = row.operator("bim.add_annotation", text="Stair Arrow", icon="SCREEN_BACK")
|
||||
op.object_type = "STAIR_ARROW"
|
||||
op.data_type = "curve"
|
||||
op.description = "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation"
|
||||
op = row.operator("bim.add_annotation", text="Hidden", icon="CON_TRACKTO")
|
||||
op.object_type = "HIDDEN_LINE"
|
||||
op.data_type = "mesh"
|
||||
@@ -470,6 +472,7 @@ class BIM_PT_annotation_utilities(Panel):
|
||||
op = row.operator("bim.add_annotation", text="Batting", icon="FORCE_FORCE")
|
||||
op.object_type = "BATTING"
|
||||
op.data_type = "mesh"
|
||||
op.description = "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set"
|
||||
op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
|
||||
op.object_type = "FILL_AREA"
|
||||
|
||||
@@ -503,26 +506,32 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
|
||||
class BIM_UL_sheets(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
|
||||
if item.is_sheet:
|
||||
if item.is_expanded:
|
||||
row.operator(
|
||||
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.operator(
|
||||
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.reference_type == "DRAWING":
|
||||
row.label(text="", icon="IMAGE_DATA")
|
||||
elif item.reference_type == "SCHEDULE":
|
||||
row.label(text="", icon="LONGDISPLAY")
|
||||
|
||||
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
|
||||
row.label(text=name)
|
||||
else:
|
||||
if not item:
|
||||
layout.label(text="", translate=False)
|
||||
return
|
||||
|
||||
row = layout.row()
|
||||
if item.is_sheet:
|
||||
if item.is_expanded:
|
||||
row.operator(
|
||||
"bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
|
||||
).sheet = item.ifc_definition_id
|
||||
else:
|
||||
row.operator(
|
||||
"bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
|
||||
).sheet = item.ifc_definition_id
|
||||
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
|
||||
row.prop(item, "name", text=item.identification or "X", emboss=False)
|
||||
else:
|
||||
row.label(text="", icon="BLANK1")
|
||||
if item.reference_type == "DRAWING":
|
||||
row.label(text="", icon="IMAGE_DATA")
|
||||
elif item.reference_type == "SCHEDULE":
|
||||
row.label(text="", icon="LONGDISPLAY")
|
||||
elif item.reference_type == "TITLEBLOCK":
|
||||
row.label(text="", icon="MENU_PANEL")
|
||||
if item.identification:
|
||||
name = f"{item.identification} - {item.name or 'Unnamed'}"
|
||||
else:
|
||||
name = item.name or "Unnamed"
|
||||
row.label(text=name)
|
||||
|
||||
@@ -27,6 +27,8 @@ classes = (
|
||||
operator.OverrideDelete,
|
||||
operator.OverrideDuplicateMove,
|
||||
operator.OverrideDuplicateMoveLinked,
|
||||
operator.OverrideDuplicateMoveLinkedMacro,
|
||||
operator.OverrideDuplicateMoveMacro,
|
||||
operator.OverrideJoin,
|
||||
operator.OverrideModeSetEdit,
|
||||
operator.OverrideModeSetObject,
|
||||
@@ -52,6 +54,11 @@ addon_keymaps = []
|
||||
|
||||
|
||||
def register():
|
||||
operator.OverrideDuplicateMoveMacro.define("BIM_OT_override_object_duplicate_move")
|
||||
operator.OverrideDuplicateMoveMacro.define("TRANSFORM_OT_translate")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
|
||||
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
|
||||
|
||||
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
|
||||
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
|
||||
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
|
||||
@@ -60,9 +67,8 @@ def register():
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
|
||||
kmi = km.keymap_items.new("bim.override_object_join", "J", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True)
|
||||
kmi.properties.is_interactive = True
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_macro", "D", "PRESS", shift=True)
|
||||
kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked_macro", "D", "PRESS", alt=True)
|
||||
kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True)
|
||||
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
|
||||
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
|
||||
|
||||
@@ -498,6 +498,12 @@ class OverrideOutlinerDelete(bpy.types.Operator, OverrideDeleteTrait):
|
||||
return {"objects": objects, "children": children}
|
||||
|
||||
|
||||
class OverrideDuplicateMoveMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_object_duplicate_move_macro"
|
||||
bl_label = "IFC Duplicate Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
class OverrideDuplicateMove(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_duplicate_move"
|
||||
bl_label = "IFC Duplicate Objects"
|
||||
@@ -529,8 +535,6 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
if self.is_interactive:
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -551,17 +555,26 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
# Copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
if new:
|
||||
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new]
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
if self.is_interactive:
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
|
||||
bl_idname = "bim.override_object_duplicate_move_linked_macro"
|
||||
bl_label = "IFC Duplicate Linked"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
|
||||
class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
bl_idname = "bim.override_object_duplicate_move_linked"
|
||||
bl_label = "IFC Duplicate Linked"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -586,7 +599,6 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
new_obj.select_set(True)
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -596,6 +608,8 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
old_to_new = {}
|
||||
for obj in context.selected_objects:
|
||||
new_obj = obj.copy()
|
||||
if obj.data:
|
||||
new_obj.data = obj.data.copy()
|
||||
if obj == context.active_object:
|
||||
self.new_active_obj = new_obj
|
||||
for collection in obj.users_collection:
|
||||
@@ -605,10 +619,13 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
# Copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
if new:
|
||||
array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array")
|
||||
if array_pset:
|
||||
array_pset = tool.Ifc.get().by_id(array_pset["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset)
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = new
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
blenderbim.bim.handler.purge_module_data()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -616,6 +633,12 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
|
||||
class OverrideJoin(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.override_object_join"
|
||||
bl_label = "IFC Join"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not tool.Ifc.get():
|
||||
return bpy.ops.object.join()
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
if not tool.Ifc.get():
|
||||
@@ -703,6 +726,7 @@ class OverrideJoin(bpy.types.Operator, Operator):
|
||||
class OverridePasteBuffer(bpy.types.Operator):
|
||||
bl_idname = "bim.override_paste_buffer"
|
||||
bl_label = "IFC Paste BIM Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.pastebuffer()
|
||||
@@ -724,7 +748,6 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
objs = context.selected_objects or [context.active_object]
|
||||
if context.active_object:
|
||||
context.active_object.select_set(True)
|
||||
edited_objs = []
|
||||
for obj in objs:
|
||||
if not obj:
|
||||
continue
|
||||
@@ -742,26 +765,38 @@ class OverrideModeSetEdit(bpy.types.Operator):
|
||||
obj.select_set(False)
|
||||
continue
|
||||
|
||||
if obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
if representation.RepresentationType in ("Tessellation", "Brep"):
|
||||
if element.HasOpenings:
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
|
||||
if not context.selected_objects:
|
||||
return {"FINISHED"}
|
||||
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
if not representation:
|
||||
continue
|
||||
|
||||
if tool.Geometry.is_meshlike(representation):
|
||||
if getattr(element, "HasOpenings", None):
|
||||
# Mesh elements with openings must disable openings
|
||||
# so that you can edit the original topology.
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
apply_openings=False,
|
||||
)
|
||||
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
|
||||
if not context.selected_objects or len(context.selected_objects) != len(objs):
|
||||
# We are trying to edit at least one non-mesh-like object : Display a hint to the user
|
||||
self.report({"INFO"}, "Only mesh-compatible representations may be edited in edit mode.")
|
||||
|
||||
if context.active_object not in context.selected_objects:
|
||||
# The active object is non-mesh-like. Set a valid object (or None) as active
|
||||
context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None
|
||||
if context.active_object:
|
||||
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -825,14 +860,13 @@ class OverrideModeSetObject(bpy.types.Operator):
|
||||
continue
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
if not element or element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
|
||||
if obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
if representation.RepresentationType in (
|
||||
"Tessellation",
|
||||
"Brep",
|
||||
if tool.Geometry.is_meshlike(
|
||||
representation
|
||||
) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
|
||||
self.edited_objs.append(obj)
|
||||
elif element.HasOpenings:
|
||||
|
||||
@@ -49,7 +49,7 @@ class BIM_OT_cityjson2ifc(Operator):
|
||||
|
||||
class BIM_OT_find_cityjson_lod(Operator):
|
||||
bl_idname = "bim.find_cityjson_lod"
|
||||
bl_label = "Find LODs in CityJSON file"
|
||||
bl_label = "Find LODs in CityJSON File"
|
||||
bl_context = "scene"
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -24,6 +24,7 @@ import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.style
|
||||
import blenderbim.core.material as core
|
||||
import blenderbim.bim.module.model.profile as model_profile
|
||||
from blenderbim.bim.module.material.prop import purge as material_prop_purge
|
||||
@@ -666,8 +667,12 @@ class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
blender_material = context.active_object.active_material
|
||||
material = tool.Ifc.get_entity(blender_material)
|
||||
|
||||
if tool.Ifc.has_changed_shading(blender_material):
|
||||
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=blender_material)
|
||||
|
||||
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
|
||||
copied_blender_material = bpy.data.materials.new(blender_material.name)
|
||||
copied_blender_material = blender_material.copy()
|
||||
copied_style = self.get_style(copied_material)
|
||||
tool.Ifc.link(copied_material, copied_blender_material)
|
||||
if copied_style:
|
||||
|
||||
@@ -47,6 +47,9 @@ classes = (
|
||||
array.EnableEditingArray,
|
||||
array.RemoveArray,
|
||||
array.SelectArrayParent,
|
||||
array.Input3DCursorXArray,
|
||||
array.Input3DCursorYArray,
|
||||
array.Input3DCursorZArray,
|
||||
product.AddConstrTypeInstance,
|
||||
product.AddEmptyType,
|
||||
product.AlignProduct,
|
||||
|
||||
@@ -22,7 +22,7 @@ import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -34,7 +34,16 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0, "use_local_space": True}
|
||||
array = {
|
||||
"children": [],
|
||||
"count": 1,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"use_local_space": True,
|
||||
"sync_children": False,
|
||||
"dimension_input_type": "Increment",
|
||||
}
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
|
||||
@@ -81,6 +90,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.y = data["y"]
|
||||
props.z = data["z"]
|
||||
props.use_local_space = data.get("use_local_space", False)
|
||||
props.sync_children = data.get("sync_children", False)
|
||||
props.dimension_input_type = data.get("dimension_input_type", "Increment")
|
||||
props.is_editing = self.item
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -105,6 +116,8 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"y": props.y,
|
||||
"z": props.z,
|
||||
"use_local_space": props.use_local_space,
|
||||
"sync_children": props.sync_children,
|
||||
"dimension_input_type": props.dimension_input_type,
|
||||
}
|
||||
|
||||
props.is_editing = -1
|
||||
@@ -173,3 +186,48 @@ class SelectArrayParent(bpy.types.Operator):
|
||||
context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
class Input3DCursorXArray(bpy.types.Operator):
|
||||
bl_idname = "bim.input_cursor_x_array"
|
||||
bl_label = "Get 3d Cursor X Input for Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMArrayProperties
|
||||
cursor = context.scene.cursor
|
||||
if props.use_local_space:
|
||||
props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).x
|
||||
else:
|
||||
props.x = cursor.location.x - obj.location.x
|
||||
return {"FINISHED"}
|
||||
|
||||
class Input3DCursorYArray(bpy.types.Operator):
|
||||
bl_idname = "bim.input_cursor_y_array"
|
||||
bl_label = "Get 3d Cursor Y Input for Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMArrayProperties
|
||||
cursor = context.scene.cursor
|
||||
if props.use_local_space:
|
||||
props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).y
|
||||
else:
|
||||
props.y = cursor.location.y - obj.location.y
|
||||
return {"FINISHED"}
|
||||
|
||||
class Input3DCursorZArray(bpy.types.Operator):
|
||||
bl_idname = "bim.input_cursor_z_array"
|
||||
bl_label = "Get 3d Cursor Z Input for Array"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMArrayProperties
|
||||
cursor = context.scene.cursor
|
||||
if props.use_local_space:
|
||||
props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.col[3]).z
|
||||
else:
|
||||
props.z = cursor.location.z - obj.location.z
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -64,6 +64,7 @@ class AuthoringData:
|
||||
cls.data["has_visible_openings"] = cls.has_visible_openings()
|
||||
cls.data["active_class"] = cls.active_class()
|
||||
cls.data["active_material_usage"] = cls.active_material_usage()
|
||||
cls.data["active_representation_type"] = cls.active_representation_type()
|
||||
|
||||
@classmethod
|
||||
def type_class(cls):
|
||||
@@ -187,16 +188,24 @@ class AuthoringData:
|
||||
if element:
|
||||
return tool.Model.get_usage_type(element)
|
||||
|
||||
@classmethod
|
||||
def active_representation_type(cls):
|
||||
if bpy.context.active_object:
|
||||
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
|
||||
if representation:
|
||||
return representation.RepresentationType
|
||||
|
||||
@classmethod
|
||||
def ifc_classes(cls):
|
||||
results = []
|
||||
classes = {
|
||||
e.is_a()
|
||||
for e in tool.Ifc.get().by_type("IfcElementType")
|
||||
+ tool.Ifc.get().by_type("IfcDoorStyle")
|
||||
+ tool.Ifc.get().by_type("IfcWindowStyle")
|
||||
+ tool.Ifc.get().by_type("IfcSpaceType")
|
||||
e.is_a() for e in (tool.Ifc.get().by_type("IfcElementType") + tool.Ifc.get().by_type("IfcSpaceType"))
|
||||
}
|
||||
|
||||
if tool.Ifc.get_schema() in ("IFC2X3", "IFC4"):
|
||||
classes.update(
|
||||
{e.is_a() for e in (tool.Ifc.get().by_type("IfcDoorStyle") + tool.Ifc.get().by_type("IfcWindowStyle"))}
|
||||
)
|
||||
results.extend([(c, c, "") for c in sorted(classes)])
|
||||
return results
|
||||
|
||||
|
||||
@@ -17,18 +17,16 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gpu
|
||||
import bgl
|
||||
import bmesh
|
||||
import blenderbim.tool as tool
|
||||
from math import pi, degrees, sin, cos, radians
|
||||
from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Vector, Matrix
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
white = (1, 1, 1, 1)
|
||||
lightgrey = (0.7, 0.7, 0.7, 1)
|
||||
white_t = (1, 1, 1, 0.5)
|
||||
green = (0.545, 0.863, 0, 1)
|
||||
red = (1, 0.2, 0.322, 1)
|
||||
blue = (0.157, 0.565, 1, 1)
|
||||
@@ -71,13 +69,11 @@ class ProfileDecorator:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def create_batch(self, shader_type, content_pos, color, indices=None, bind=True):
|
||||
batch = batch_for_shader(self.shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
# TODO: what's bind is for?
|
||||
if bind:
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", color)
|
||||
batch.draw(self.shader)
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_faces(self, bm, vertices_coords):
|
||||
"""mutates original bm (triangulates it)
|
||||
@@ -87,7 +83,7 @@ class ProfileDecorator:
|
||||
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
|
||||
|
||||
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
|
||||
self.create_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
|
||||
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||
obj = context.active_object
|
||||
@@ -103,20 +99,8 @@ class ProfileDecorator:
|
||||
else:
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
|
||||
def gl_init(use_bgl=False):
|
||||
# TODO: remove as deprecated?
|
||||
if use_bgl:
|
||||
bgl.glLineWidth(2)
|
||||
bgl.glPointSize(6)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
else:
|
||||
gpu.state.line_width_set(2)
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
gl_init(True)
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
### Actually drawing
|
||||
all_vertices = []
|
||||
@@ -202,22 +186,31 @@ class ProfileDecorator:
|
||||
unselected_edges.append(edge_indices)
|
||||
|
||||
### Actually drawing
|
||||
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind()
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
self.shader.bind()
|
||||
|
||||
# Draw faces
|
||||
if draw_faces:
|
||||
self.draw_faces(bm, all_vertices)
|
||||
|
||||
self.create_batch("LINES", all_vertices, lightgrey, unselected_edges)
|
||||
self.create_batch("LINES", all_vertices, green, selected_edges)
|
||||
self.create_batch("LINES", all_vertices, grey, arc_edges)
|
||||
self.create_batch("LINES", all_vertices, preview_edges_color, preview_edges)
|
||||
self.create_batch("LINES", all_vertices, blue, roof_angle_edges)
|
||||
self.draw_batch("LINES", all_vertices, white_t, unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, green, selected_edges)
|
||||
self.draw_batch("LINES", all_vertices, grey, arc_edges)
|
||||
self.draw_batch("LINES", all_vertices, preview_edges_color, preview_edges)
|
||||
self.draw_batch("LINES", all_vertices, blue, roof_angle_edges)
|
||||
|
||||
self.create_batch("POINTS", unselected_vertices, lightgrey)
|
||||
self.create_batch("POINTS", error_vertices, red)
|
||||
self.create_batch("POINTS", special_vertices, blue)
|
||||
self.create_batch("POINTS", selected_vertices, green)
|
||||
self.draw_batch("POINTS", unselected_vertices, white_t)
|
||||
self.draw_batch("POINTS", error_vertices, red)
|
||||
self.draw_batch("POINTS", special_vertices, blue)
|
||||
self.draw_batch("POINTS", selected_vertices, green)
|
||||
|
||||
# Draw arcs
|
||||
arc_centroids = []
|
||||
@@ -242,9 +235,9 @@ class ProfileDecorator:
|
||||
arc_centroids.append(tuple(centroid))
|
||||
arc_segments.append(tool.Cad.create_arc_segments(pts=points, num_verts=17, make_edges=True))
|
||||
|
||||
self.create_batch("POINTS", arc_centroids, grey, bind=False)
|
||||
self.draw_batch("POINTS", arc_centroids, grey)
|
||||
for verts, edges in arc_segments:
|
||||
self.create_batch("LINES", verts, blue, edges, bind=False)
|
||||
self.draw_batch("LINES", verts, blue, edges)
|
||||
|
||||
# Draw circles
|
||||
circle_centroids = []
|
||||
@@ -263,9 +256,9 @@ class ProfileDecorator:
|
||||
segments = [[list(matrix @ Vector(v)) for v in segments[0]], segments[1]]
|
||||
circle_segments.append(segments)
|
||||
|
||||
self.create_batch("POINTS", circle_centroids, grey, bind=False)
|
||||
self.draw_batch("POINTS", circle_centroids, grey)
|
||||
for verts, edges in circle_segments:
|
||||
self.create_batch("LINES", verts, blue, edges, bind=False)
|
||||
self.draw_batch("LINES", verts, blue, edges)
|
||||
|
||||
def create_matrix(self, p, x, y, z):
|
||||
return Matrix([x, y, z, p]).to_4x4().transposed()
|
||||
|
||||
@@ -43,6 +43,7 @@ def update_door_modifier_representation(context):
|
||||
props = obj.BIMDoorProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
sliding_door = "SLIDING" in props.door_type
|
||||
|
||||
representation_data = {
|
||||
"operation_type": props.door_type,
|
||||
@@ -87,13 +88,37 @@ def update_door_modifier_representation(context):
|
||||
model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
|
||||
tool.Model.replace_object_ifc_representation(body, obj, model_representation)
|
||||
|
||||
# PLAN_VIEW representation
|
||||
plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
|
||||
if plan:
|
||||
representation_data["context"] = plan
|
||||
# Body/PLAN_VIEW representation
|
||||
plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW")
|
||||
if plan_body:
|
||||
representation_data["context"] = plan_body
|
||||
plan_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data)
|
||||
tool.Model.replace_object_ifc_representation(plan, obj, plan_representation)
|
||||
tool.Model.replace_object_ifc_representation(plan_body, obj, plan_representation)
|
||||
|
||||
# Annotation/PLAN_VIEW representation
|
||||
plan_annotation = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Annotation", "PLAN_VIEW")
|
||||
if plan_annotation:
|
||||
if not sliding_door:
|
||||
# only sliding doors have Annotation/PLAN_VIEW
|
||||
# for other types we just check for old representation and remove it if it's there
|
||||
old_representation = ifcopenshell.util.representation.get_representation(
|
||||
element, "Plan", "Annotation", "PLAN_VIEW"
|
||||
)
|
||||
if old_representation:
|
||||
core.remove_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=old_representation,
|
||||
)
|
||||
else:
|
||||
representation_data["context"] = plan_annotation
|
||||
plan_representation = ifcopenshell.api.run(
|
||||
"geometry.add_door_representation", ifc_file, **representation_data
|
||||
)
|
||||
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
|
||||
|
||||
if plan_body or plan_annotation:
|
||||
# adding switch representation at the end instead of changing order of representations
|
||||
# to prevent #2744
|
||||
core.switch_representation(
|
||||
@@ -263,13 +288,17 @@ def update_door_modifier_bmesh(context):
|
||||
door_type = props.door_type
|
||||
double_swing_door = "DOUBLE_SWING" in door_type
|
||||
double_door = "DOUBLE_DOOR" in door_type
|
||||
sliding_door = "SLIDING" in door_type
|
||||
|
||||
# lining params
|
||||
lining_depth = props.lining_depth * si_conversion
|
||||
lining_thickness_default = props.lining_thickness * si_conversion
|
||||
lining_offset = props.lining_offset * si_conversion
|
||||
lining_to_panel_offset_x = props.lining_to_panel_offset_x * si_conversion
|
||||
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion
|
||||
lining_to_panel_offset_x = (
|
||||
props.lining_to_panel_offset_x * si_conversion if not sliding_door else lining_thickness_default
|
||||
)
|
||||
panel_depth = props.panel_depth * si_conversion
|
||||
lining_to_panel_offset_y = props.lining_to_panel_offset_y * si_conversion if not sliding_door else -panel_depth
|
||||
|
||||
transom_thickness = props.transom_thickness * si_conversion / 2
|
||||
transfom_offset = props.transom_offset * si_conversion
|
||||
@@ -278,10 +307,10 @@ def update_door_modifier_bmesh(context):
|
||||
window_lining_height = overall_height - transfom_offset - transom_thickness
|
||||
|
||||
side_lining_thickness = lining_thickness_default
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
|
||||
top_lining_thickness = transom_thickness or lining_thickness_default
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
door_opening_width = overall_width - lining_to_panel_offset_x * 2
|
||||
if double_swing_door:
|
||||
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
|
||||
@@ -296,7 +325,6 @@ def update_door_modifier_bmesh(context):
|
||||
casing_depth = props.casing_depth * si_conversion
|
||||
|
||||
# panel params
|
||||
panel_depth = props.panel_depth * si_conversion
|
||||
panel_width = door_opening_width * props.panel_width_ratio
|
||||
frame_depth = props.frame_depth * si_conversion
|
||||
frame_thickness = props.frame_thickness * si_conversion
|
||||
@@ -389,7 +417,7 @@ def update_door_modifier_bmesh(context):
|
||||
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness)
|
||||
|
||||
if double_door:
|
||||
# TODO: keep a little space between doors for readibility?
|
||||
# keeping a little space between doors for readibility
|
||||
double_door_offset = 0.001 * si_conversion
|
||||
panel_size.x = panel_size.x / 2 - double_door_offset
|
||||
door_verts.extend(create_bm_door_panel(panel_size, panel_position, "LEFT"))
|
||||
@@ -516,7 +544,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_door"
|
||||
bl_label = "Cancel editing Door"
|
||||
bl_label = "Cancel Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -545,7 +573,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_door"
|
||||
bl_label = "Finish editing door"
|
||||
bl_label = "Finish Editing Door"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -60,9 +60,6 @@ def load_post(*args):
|
||||
product.regenerate_profile_usage,
|
||||
)
|
||||
|
||||
IfcStore.add_element_listener(wall.element_listener)
|
||||
IfcStore.add_element_listener(profile.element_listener)
|
||||
|
||||
ifcopenshell.api.add_post_listener(
|
||||
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
import bgl
|
||||
import bmesh
|
||||
import logging
|
||||
import numpy as np
|
||||
@@ -36,7 +35,6 @@ from bpy.types import Operator
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy.props import FloatProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
@@ -217,8 +215,9 @@ class FilledOpeningGenerator:
|
||||
|
||||
def generate_opening_from_filling(self, filling, filling_obj, voided_obj):
|
||||
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
ifc_file = tool.Ifc.get()
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
|
||||
|
||||
profile = None
|
||||
filling_type = ifcopenshell.util.element.get_type(filling)
|
||||
@@ -227,47 +226,59 @@ class FilledOpeningGenerator:
|
||||
filling_type, "Model", "Profile", "ELEVATION_VIEW"
|
||||
)
|
||||
filling_obj = tool.Ifc.get_object(filling_type)
|
||||
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
if profile:
|
||||
curve_3d = ifcopenshell.util.representation.resolve_representation(profile).Items[0]
|
||||
|
||||
def get_curve_2d_from_3d(curve_3d):
|
||||
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
|
||||
ifc_points = ifc_file.createIfcCartesianPointList2D([Vector(p).xz for p in curve_3d.Points.CoordList])
|
||||
ifc_curve = ifc_file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
|
||||
return ifc_curve
|
||||
|
||||
extrusion = shape_builder.extrude(
|
||||
ifcopenshell.util.representation.resolve_representation(profile).Items[0],
|
||||
get_curve_2d_from_3d(curve_3d),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
|
||||
extrusion_vector=Vector([0.0, 1.0, 0.0]),
|
||||
position_x_axis=Vector((1, 0, 0)),
|
||||
position_z_axis=Vector((0, -1, 0)),
|
||||
extrusion_vector=Vector((0, 0, -1)),
|
||||
)
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
|
||||
x, y, z = filling_obj.dimensions
|
||||
opening_position = Vector([0.0, -0.1 / unit_scale, 0.0])
|
||||
opening_size = Vector([x, 0, z]) / unit_scale
|
||||
opening_size = Vector([x, z]) / unit_scale
|
||||
|
||||
# Windows and doors can have a casing that overlaps the wall
|
||||
# but shouldn't affect the size of the opening.
|
||||
# So we shouldn't use object dimensions in that case. More: #2784
|
||||
# Just keeping it for windows and doors for now to be safe
|
||||
x_redefined, z_redefined = False, False
|
||||
has_width_attribute, has_height_attribute = False, False
|
||||
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
|
||||
if filling.OverallWidth:
|
||||
opening_size.x = filling.OverallWidth
|
||||
x_redefined = True
|
||||
has_width_attribute = True
|
||||
if filling.OverallHeight:
|
||||
opening_size.z = filling.OverallHeight
|
||||
z_redefined = True
|
||||
opening_size.y = filling.OverallHeight
|
||||
has_height_attribute = True
|
||||
|
||||
# making sure if min_x or min_z != 0 to shift the opening accordingly
|
||||
# to prevent something like #2784
|
||||
if not x_redefined:
|
||||
if not has_width_attribute:
|
||||
opening_position.x = min(v[0] for v in filling_obj.bound_box)
|
||||
|
||||
if not z_redefined:
|
||||
if not has_height_attribute:
|
||||
opening_position.z = min(v[2] for v in filling_obj.bound_box)
|
||||
|
||||
extrusion = shape_builder.extrude(
|
||||
shape_builder.rectangle(size=opening_size),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=opening_position,
|
||||
extrusion_vector=Vector([0.0, 1.0, 0.0]),
|
||||
position_z_axis=Vector((0.0, -1.0, 0.0)),
|
||||
position_x_axis=Vector((1.0, 0.0, 0.0)),
|
||||
extrusion_vector=Vector((0.0, 0.0, -1.0)),
|
||||
)
|
||||
|
||||
return shape_builder.get_representation(context, [extrusion])
|
||||
@@ -709,13 +720,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
if opening_obj:
|
||||
if tool.Ifc.is_edited(opening_obj):
|
||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
||||
building_objs.add(obj)
|
||||
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
|
||||
elif tool.Ifc.is_moved(opening_obj):
|
||||
blenderbim.core.geometry.edit_object_placement(
|
||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||
)
|
||||
building_objs.add(obj)
|
||||
building_objs.add(obj)
|
||||
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
|
||||
tool.Ifc.unlink(element=opening, obj=opening_obj)
|
||||
bpy.data.objects.remove(opening_obj)
|
||||
|
||||
@@ -758,7 +768,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
||||
results.add(obj)
|
||||
return results
|
||||
|
||||
|
||||
# TODO: merge with ProfileDecorator?
|
||||
class DecorationsHandler:
|
||||
installed = None
|
||||
|
||||
@@ -777,11 +787,15 @@ class DecorationsHandler:
|
||||
pass
|
||||
cls.installed = None
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def __call__(self, context):
|
||||
bgl.glLineWidth(2)
|
||||
bgl.glPointSize(6)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
gpu.state.point_size_set(6)
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
for opening in context.scene.BIMModelProperties.openings:
|
||||
obj = opening.obj
|
||||
@@ -790,7 +804,7 @@ class DecorationsHandler:
|
||||
continue
|
||||
|
||||
white = (1, 1, 1, 1)
|
||||
white_t = (1, 1, 1, 0.1)
|
||||
white_t = (1, 1, 1, 0.5)
|
||||
green = (0.545, 0.863, 0, 1)
|
||||
red = (1, 0.2, 0.322, 1)
|
||||
red_t = (1, 0.2, 0.322, 0.1)
|
||||
@@ -798,6 +812,13 @@ class DecorationsHandler:
|
||||
blue_t = (0.157, 0.565, 1, 0.1)
|
||||
grey = (0.2, 0.2, 0.2, 1)
|
||||
|
||||
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
|
||||
|
||||
verts = []
|
||||
@@ -829,41 +850,21 @@ class DecorationsHandler:
|
||||
else:
|
||||
unselected_edges.append(edge_indices)
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=unselected_edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", white)
|
||||
batch.draw(self.shader)
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=selected_edges)
|
||||
self.shader.uniform_float("color", green)
|
||||
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": selected_vertices})
|
||||
self.shader.uniform_float("color", green)
|
||||
batch.draw(self.shader)
|
||||
self.draw_batch("LINES", verts, white_t, unselected_edges)
|
||||
self.draw_batch("LINES", verts, green, selected_edges)
|
||||
self.draw_batch("POINTS", unselected_vertices, white)
|
||||
self.draw_batch("POINTS", selected_vertices, green)
|
||||
else:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
|
||||
edges = [tuple([v.index for v in e.verts]) for e in bm.edges]
|
||||
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
|
||||
batch.draw(self.shader)
|
||||
self.draw_batch("LINES", verts, green if obj in context.selected_objects else blue, edges)
|
||||
|
||||
obj.data.calc_loop_triangles()
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
|
||||
batch = batch_for_shader(self.shader, "TRIS", {"pos": verts}, indices=tris)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", blue_t)
|
||||
batch.draw(self.shader)
|
||||
self.draw_batch("TRIS", verts, blue_t, tris)
|
||||
|
||||
if "HalfSpaceSolid" in obj.name:
|
||||
# Arrow shape
|
||||
@@ -876,10 +877,7 @@ class DecorationsHandler:
|
||||
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
|
||||
]
|
||||
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
|
||||
batch = batch_for_shader(self.shader, "LINES", {"pos": verts}, indices=edges)
|
||||
self.shader.bind()
|
||||
self.shader.uniform_float("color", green if obj in context.selected_objects else blue)
|
||||
batch.draw(self.shader)
|
||||
self.draw_batch("LINES", verts, green if obj in context.selected_objects else blue, edges)
|
||||
|
||||
if obj.mode != "EDIT":
|
||||
bm.free()
|
||||
|
||||
@@ -402,7 +402,7 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.mirror_elements"
|
||||
bl_label = "Mirror Elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Faux-mirrors the selected object by an active empty along a mirror plane."
|
||||
bl_description = "Faux-mirrors the selected objects by an active empty along a mirror plane"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -429,6 +429,13 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
mirror.select_set(False)
|
||||
|
||||
if not context.selected_objects:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.ops.bim.override_object_duplicate_move(is_interactive=False)
|
||||
|
||||
for obj in context.selected_objects:
|
||||
|
||||
@@ -31,64 +31,10 @@ import blenderbim.core.geometry
|
||||
from math import pi, degrees, inf
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from blenderbim.bim.module.geometry.helper import Helper
|
||||
from blenderbim.bim.module.model.wall import DumbWallRecalculator
|
||||
from blenderbim.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
|
||||
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 = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
|
||||
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
||||
if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile":
|
||||
return
|
||||
if obj.mode == "EDIT":
|
||||
tool.Ifc.edit(obj)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
else:
|
||||
material_usage = ifcopenshell.util.element.get_material(product)
|
||||
x, y = obj.dimensions[0:2]
|
||||
if not material_usage.CardinalPoint:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 1:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[4])
|
||||
elif material_usage.CardinalPoint == 2:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 3:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[0])
|
||||
elif material_usage.CardinalPoint == 4:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 5:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 6:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 7:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[7])
|
||||
elif material_usage.CardinalPoint == 8:
|
||||
new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2))
|
||||
elif material_usage.CardinalPoint == 9:
|
||||
new_origin = obj.matrix_world @ Vector(obj.bound_box[3])
|
||||
if (obj.matrix_world.translation - new_origin).length < 0.001:
|
||||
return
|
||||
obj.data.transform(
|
||||
Matrix.Translation(
|
||||
(obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin))
|
||||
)
|
||||
)
|
||||
obj.matrix_world.translation = new_origin
|
||||
|
||||
|
||||
class DumbProfileGenerator:
|
||||
def __init__(self, relating_type):
|
||||
self.relating_type = relating_type
|
||||
@@ -892,15 +838,24 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return context.selected_objects
|
||||
|
||||
def _execute(self, context):
|
||||
objs = []
|
||||
profile_objs = []
|
||||
layer2_objs = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
usage = tool.Model.get_usage_type(element)
|
||||
if usage == "PROFILE":
|
||||
profile_objs.append(obj)
|
||||
elif usage == "LAYER2":
|
||||
layer2_objs.append(obj)
|
||||
if element.ConnectedTo or element.ConnectedFrom:
|
||||
objs.append(obj)
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATSTART")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATEND")
|
||||
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element, connection_type="ATPATH")
|
||||
rotate_matrix = Matrix.Rotation(pi / 2, 4, self.axis)
|
||||
obj.matrix_world @= rotate_matrix
|
||||
bpy.context.view_layer.update()
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
DumbProfileRecalculator().recalculate(profile_objs)
|
||||
DumbWallRecalculator().recalculate(layer2_objs)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ class BIMArrayProperties(PropertyGroup):
|
||||
is_editing: bpy.props.IntProperty(
|
||||
default=-1, description="Currently edited array index. -1 if not in array editing mode."
|
||||
)
|
||||
count: bpy.props.IntProperty(name="Count", default=0)
|
||||
count: bpy.props.IntProperty(name="Count", default=0, min=0)
|
||||
x: bpy.props.FloatProperty(name="X", default=0)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0)
|
||||
z: bpy.props.FloatProperty(name="Z", default=0)
|
||||
@@ -261,6 +261,19 @@ class BIMArrayProperties(PropertyGroup):
|
||||
description="Use local space for array items offset instead of world space",
|
||||
default=True,
|
||||
)
|
||||
dimension_input_type: bpy.props.EnumProperty(
|
||||
items=(
|
||||
("Increment", "Increment", ""),
|
||||
("Total", "Total", ""),
|
||||
),
|
||||
name="Type of input dimension",
|
||||
default="Increment",
|
||||
)
|
||||
sync_children: bpy.props.BoolProperty(
|
||||
name="Sync Children",
|
||||
description="Keep children objects in sync with parent object",
|
||||
default = False,
|
||||
)
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
@@ -455,7 +468,9 @@ class BIMDoorProperties(PropertyGroup):
|
||||
("DOUBLE_SWING_LEFT", "DOUBLE_SWING_LEFT", ""),
|
||||
("DOUBLE_SWING_RIGHT", "DOUBLE_SWING_RIGHT", ""),
|
||||
("DOUBLE_DOOR_SINGLE_SWING", "DOUBLE_DOOR_SINGLE_SWING", ""),
|
||||
("DOUBLE_DOOR_DOUBLE_SWING", "DOUBLE_DOOR_DOUBLE_SWING", ""),
|
||||
("SLIDING_TO_LEFT", "SLIDING_TO_LEFT", ""),
|
||||
("SLIDING_TO_RIGHT", "SLIDING_TO_RIGHT", ""),
|
||||
("DOUBLE_DOOR_SLIDING", "DOUBLE_DOOR_SLIDING", ""),
|
||||
)
|
||||
|
||||
door_added_previously: bpy.props.BoolProperty(default=False)
|
||||
@@ -525,10 +540,12 @@ class BIMDoorProperties(PropertyGroup):
|
||||
"lining_depth": self.lining_depth,
|
||||
"lining_thickness": self.lining_thickness,
|
||||
"lining_offset": self.lining_offset,
|
||||
"lining_to_panel_offset_x": self.lining_to_panel_offset_x,
|
||||
"lining_to_panel_offset_y": self.lining_to_panel_offset_y,
|
||||
}
|
||||
|
||||
if "SLIDING" not in self.door_type:
|
||||
kwargs["lining_to_panel_offset_x"] = self.lining_to_panel_offset_x
|
||||
kwargs["lining_to_panel_offset_y"] = self.lining_to_panel_offset_y
|
||||
|
||||
kwargs["transom_thickness"] = self.transom_thickness
|
||||
if self.transom_thickness:
|
||||
kwargs["transom_offset"] = self.transom_offset
|
||||
|
||||
@@ -299,7 +299,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_railing"
|
||||
bl_label = "Cancel editing Railing"
|
||||
bl_label = "Cancel Editing Railing"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -328,7 +328,7 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_railing"
|
||||
bl_label = "Finish editing railing"
|
||||
bl_label = "Finish Editing Railing"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -469,7 +469,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_roof"
|
||||
bl_label = "Cancel editing Roof"
|
||||
bl_label = "Cancel Editing Roof"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -498,7 +498,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_roof"
|
||||
bl_label = "Finish editing roof"
|
||||
bl_label = "Finish Editing Roof"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -633,7 +633,7 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class SetGableRoofEdgeAngle(bpy.types.Operator):
|
||||
bl_idname = "bim.set_gable_roof_edge_angle"
|
||||
bl_label = "Set gable roof edge angle"
|
||||
bl_label = "Set Gable Roof Edge Angle"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
angle: bpy.props.FloatProperty(name="Angle", default=90)
|
||||
|
||||
|
||||
@@ -604,6 +604,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
|
||||
if extrusion.Position:
|
||||
@@ -636,8 +637,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
body = ifcopenshell.util.representation.resolve_representation(body)
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if extrusion.Position:
|
||||
position = Matrix(ifcopenshell.util.placement.get_axis2placement(extrusion.Position).tolist())
|
||||
position[0][3] *= self.unit_scale
|
||||
@@ -670,13 +672,17 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
representation=body,
|
||||
should_reload=True,
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
bpy.data.meshes.remove(profile_mesh)
|
||||
|
||||
# Only certain classes should have a footprint
|
||||
if element.is_a() not in ("IfcSlab", "IfcRamp"):
|
||||
return {"FINISHED"}
|
||||
|
||||
footprint_context = ifcopenshell.util.representation.get_context(
|
||||
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
|
||||
)
|
||||
|
||||
@@ -287,6 +287,22 @@ def update_ifc_stair_props(obj):
|
||||
)
|
||||
tool.Ifc.edit(obj)
|
||||
|
||||
# update related annotation objects
|
||||
def get_elements_from_product(product):
|
||||
elements = []
|
||||
for rel in product.ReferencedBy:
|
||||
if not rel.is_a("IfcRelAssignsToProduct"):
|
||||
continue
|
||||
elements.extend(rel.RelatedObjects)
|
||||
return elements
|
||||
|
||||
stair_obj = obj
|
||||
for rel_element in get_elements_from_product(element):
|
||||
if not rel_element.is_a("IfcAnnotation") or rel_element.ObjectType != "STAIR_ARROW":
|
||||
continue
|
||||
if annotation_obj := tool.Ifc.get_object(rel_element):
|
||||
tool.Drawing.setup_annotation_object(annotation_obj, "STAIR_ARROW", stair_obj)
|
||||
|
||||
|
||||
class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "mesh.add_clever_stair"
|
||||
@@ -368,7 +384,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_stair"
|
||||
bl_label = "Cancel editing Stair"
|
||||
bl_label = "Cancel Editing Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -388,7 +404,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_stair"
|
||||
bl_label = "Finish editing stair"
|
||||
bl_label = "Finish Editing Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -182,7 +182,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# removed the part that was relying on node graph to be opened at execution
|
||||
class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.import_sverchok_graph"
|
||||
bl_label = "Import sverchok graph"
|
||||
bl_label = "Import Sverchok Graph"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
@@ -224,7 +224,7 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# removed the part that was relying on node graph to be opened at execution
|
||||
class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.export_sverchok_graph"
|
||||
bl_label = "Export sverchok graph"
|
||||
bl_label = "Export Sverchok Graph"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
|
||||
@@ -224,11 +224,20 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
|
||||
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "use_local_space")
|
||||
row.prop(props, "dimension_input_type")
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "use_local_space")
|
||||
row.prop(props, "sync_children")
|
||||
col = box.column()
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "x")
|
||||
row.operator("bim.input_cursor_x_array", icon="CURSOR", text="")
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "y")
|
||||
row.operator("bim.input_cursor_y_array", icon="CURSOR", text="")
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "z")
|
||||
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
|
||||
|
||||
else:
|
||||
row = box.row(align=True)
|
||||
@@ -236,6 +245,8 @@ class BIM_PT_array(bpy.types.Panel):
|
||||
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
|
||||
row.operator("bim.remove_array", icon="X", text="").item = i
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Dimension input type: {array.get('dimension_input_type', 'Increment')}")
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Use Local Space: {array.get('use_local_space', False)}")
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"X: {array['x']}")
|
||||
@@ -273,7 +284,7 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
stair_data = StairData.data["parameters"]["data_dict"]
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name in props.get_props_kwargs():
|
||||
@@ -373,7 +384,7 @@ class BIM_PT_window(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_window", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_window", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -488,7 +499,7 @@ class BIM_PT_door(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_door", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_door", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -572,7 +583,7 @@ class BIM_PT_railing(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_railing", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
@@ -631,7 +642,7 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.finish_editing_roof", icon="CHECKMARK", text="Finish Editing")
|
||||
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
|
||||
|
||||
general_props = props.get_general_kwargs()
|
||||
|
||||
@@ -37,38 +37,6 @@ from mathutils import Vector, Matrix
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
|
||||
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.DumbLayer2":
|
||||
return
|
||||
if obj.mode == "EDIT":
|
||||
tool.Ifc.edit(obj)
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
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
|
||||
|
||||
|
||||
class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.join_wall"
|
||||
bl_label = "Join Wall"
|
||||
|
||||
@@ -69,10 +69,12 @@ def update_simple_openings(element, opening_width, opening_height):
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
extrusion = shape_builder.extrude(
|
||||
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height])),
|
||||
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
|
||||
magnitude=thickness / unit_scale,
|
||||
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
|
||||
extrusion_vector=Vector([0.0, 1.0, 0.0]),
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
)
|
||||
|
||||
new_representation = shape_builder.get_representation(context, extrusion)
|
||||
@@ -480,7 +482,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_window"
|
||||
bl_label = "Cancel editing Window"
|
||||
bl_label = "Cancel Editing Window"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -508,7 +510,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_window"
|
||||
bl_label = "Finish editing window"
|
||||
bl_label = "Finish Editing Window"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
|
||||
@@ -68,7 +68,17 @@ class BimTool(WorkSpaceTool):
|
||||
|
||||
|
||||
def add_layout_hotkey_operator(layout, text, hotkey, description):
|
||||
op = layout.operator("bim.hotkey", text=text)
|
||||
modifiers = {
|
||||
"A": "EVENT_ALT",
|
||||
"S": "EVENT_SHIFT",
|
||||
}
|
||||
modifier, key = hotkey.split("_")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon=modifiers[modifier])
|
||||
row.label(text="", icon=f"EVENT_{key}")
|
||||
|
||||
op = row.operator("bim.hotkey", text=text)
|
||||
op.hotkey = hotkey
|
||||
op.description = description
|
||||
return op
|
||||
@@ -132,10 +142,7 @@ class BimToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="rl2", text="RL")
|
||||
elif cls.props.ifc_class in ("IfcSpaceType"):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Generate", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
|
||||
@classmethod
|
||||
def draw_edit_object_interface(cls, context):
|
||||
@@ -155,47 +162,19 @@ class BimToolUI:
|
||||
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
|
||||
op.x_angle = cls.props.x_angle
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_T")
|
||||
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Y")
|
||||
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_M")
|
||||
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_F")
|
||||
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_K")
|
||||
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
|
||||
row.operator("bim.join_wall", icon="X", text="").join_type = ""
|
||||
|
||||
elif AuthoringData.data["active_material_usage"] == "LAYER3":
|
||||
# unnecessary check because BIM Tool is not available in EDIT mode?
|
||||
if context.active_object.mode == "OBJECT":
|
||||
row = cls.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"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="x_angle", text="X Angle")
|
||||
@@ -215,32 +194,17 @@ class BimToolUI:
|
||||
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
|
||||
op.depth = cls.props.extrusion_depth
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Extend").hotkey = "S_E"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Axis").hotkey = "A_E"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_T")
|
||||
row.operator("bim.hotkey", text="Butt").hotkey = "S_T"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Y")
|
||||
row.operator("bim.hotkey", text="Mitre").hotkey = "S_Y"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_R")
|
||||
row.operator("bim.hotkey", text="Rotate 90").hotkey = "S_R"
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
|
||||
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
|
||||
|
||||
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
|
||||
|
||||
elif AuthoringData.data["active_class"] in (
|
||||
"IfcWindow",
|
||||
"IfcWindowStandardCase",
|
||||
@@ -254,20 +218,11 @@ class BimToolUI:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.props, property="rl1", text="RL")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_F")
|
||||
row.operator("bim.hotkey", text="Flip").hotkey = "S_F"
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "")
|
||||
|
||||
elif AuthoringData.data["active_class"] in ("IfcSpace",):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_G")
|
||||
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
|
||||
|
||||
elif AuthoringData.data["active_class"] in (
|
||||
"IfcCableCarrierSegmentType",
|
||||
@@ -275,39 +230,29 @@ class BimToolUI:
|
||||
"IfcDuctSegmentType",
|
||||
"IfcPipeSegmentType",
|
||||
):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Extend", icon="EVENT_E")
|
||||
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
|
||||
|
||||
elif (
|
||||
(RailingData.is_loaded or not RailingData.load())
|
||||
and RailingData.data["parameters"]
|
||||
and not context.active_object.BIMRailingProperties.is_editing_path
|
||||
):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Railing Path").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Railing Path", "S_E", "")
|
||||
|
||||
elif (
|
||||
(RoofData.is_loaded or not RoofData.load())
|
||||
and RoofData.data["parameters"]
|
||||
and not context.active_object.BIMRoofProperties.is_editing_path
|
||||
):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit Roof Path").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Roof Path", "S_E", "")
|
||||
|
||||
elif DecoratorData.get_ifc_text_data(bpy.context.object):
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_E")
|
||||
row.operator("bim.hotkey", text="Edit text").hotkey = "S_E"
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
|
||||
if len(context.selected_objects) == 2:
|
||||
row.operator("bim.add_opening", text="Apply Void")
|
||||
else:
|
||||
@@ -320,27 +265,14 @@ class BimToolUI:
|
||||
else:
|
||||
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="Align")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Align Exterior", icon="EVENT_X")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Align Centerline", icon="EVENT_C")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Align Interior", icon="EVENT_V")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Mirror", icon="EVENT_M")
|
||||
cls.layout.row(align=True).label(text="Align")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
|
||||
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="Mode")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_O")
|
||||
add_layout_hotkey_operator(row, "Void", "A_O", "Show / edit openings")
|
||||
cls.layout.row(align=True).label(text="Mode")
|
||||
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Show / edit openings")
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_ALT")
|
||||
row.label(text="", icon="EVENT_D")
|
||||
@@ -483,8 +415,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
usage = tool.Model.get_usage_type(element)
|
||||
if not usage:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
representation = tool.Geometry.get_active_representation(obj)
|
||||
if representation and representation.RepresentationType == "SweptSolid":
|
||||
usage = "SWEPTSOLID"
|
||||
else:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected_usages.setdefault(usage, []).append(obj)
|
||||
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
@@ -498,7 +434,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif self.active_material_usage == "PROFILE":
|
||||
# Extend PROFILE to cursor
|
||||
bpy.ops.bim.extend_profile(join_type="T")
|
||||
|
||||
else:
|
||||
# Edit SWEPTSOLID profile (assuming single profile for now)
|
||||
bpy.ops.bim.enable_editing_extrusion_profile()
|
||||
elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []):
|
||||
# Extend PROFILEs to LAYER2
|
||||
[o.select_set(False) for o in selected_usages.get("LAYER3", [])]
|
||||
@@ -573,7 +511,13 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.active_material_usage == "LAYER2":
|
||||
bpy.ops.bim.merge_wall()
|
||||
else:
|
||||
bpy.ops.bim.mirror_elements()
|
||||
if len(bpy.context.selected_objects) == 1:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.",
|
||||
)
|
||||
else:
|
||||
bpy.ops.bim.mirror_elements()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if not bpy.context.selected_objects:
|
||||
|
||||
@@ -102,7 +102,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
|
||||
|
||||
class UpdateIfcPatchArguments(bpy.types.Operator):
|
||||
bl_idname = "bim.update_ifc_patch_arguments"
|
||||
bl_label = "Update IFC Patch arguments"
|
||||
bl_label = "Update IFC Patch Arguments"
|
||||
recipe: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -96,7 +96,10 @@ class MaterialPsetsData(Data):
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material))}
|
||||
cls.data = {
|
||||
"ifc_definition_id": bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id,
|
||||
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object.active_material)),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
|
||||
|
||||
@@ -298,6 +298,7 @@ class EditPset(bpy.types.Operator, Operator):
|
||||
bpy.ops.bim.load_cost_item_quantities()
|
||||
|
||||
bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
|
||||
class RemovePset(bpy.types.Operator, Operator):
|
||||
@@ -494,7 +495,7 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_remove_property_to_edit(bpy.types.Operator):
|
||||
bl_label = "Remove property to be renamed"
|
||||
bl_label = "Remove Property to Be Renamed"
|
||||
bl_idname = "bim.remove_property_to_edit"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
@@ -510,7 +511,7 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_clear_list(bpy.types.Operator):
|
||||
bl_label = "Clear list of properties"
|
||||
bl_label = "Clear List of Properties"
|
||||
bl_idname = "bim.clear_list"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
option: bpy.props.StringProperty()
|
||||
@@ -558,7 +559,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_add_edit_custom_property(bpy.types.Operator):
|
||||
bl_label = "Add or edit a custom property"
|
||||
bl_label = "Add or Edit a Custom Property"
|
||||
bl_idname = "bim.add_edit_custom_property"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
@@ -615,7 +616,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator):
|
||||
|
||||
|
||||
class BIM_OT_bulk_remove_psets(bpy.types.Operator):
|
||||
bl_label = "Bulk remove psets from selected objects"
|
||||
bl_label = "Bulk Remove Psets from Selected Objects"
|
||||
bl_idname = "bim.bulk_remove_psets"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Bulk remove psets from selected objects"
|
||||
|
||||
@@ -268,6 +268,11 @@ class BIM_PT_material_psets(Panel):
|
||||
def draw(self, context):
|
||||
if not MaterialPsetsData.is_loaded:
|
||||
MaterialPsetsData.load()
|
||||
elif (
|
||||
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
|
||||
!= MaterialPsetsData.data["ifc_definition_id"]
|
||||
):
|
||||
MaterialPsetsData.load()
|
||||
|
||||
props = context.active_object.active_material.PsetProperties
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -161,7 +161,7 @@ class EnableEditingPropTemplate(bpy.types.Operator):
|
||||
|
||||
class DeletePropEnum(bpy.types.Operator):
|
||||
bl_idname = "bim.delete_prop_enum"
|
||||
bl_label = "delete property enumeration"
|
||||
bl_label = "Delete Property Enumeration"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
@@ -173,7 +173,7 @@ class DeletePropEnum(bpy.types.Operator):
|
||||
|
||||
class AddPropEnum(bpy.types.Operator):
|
||||
bl_idname = "bim.add_prop_enum"
|
||||
bl_label = "add property enumeration"
|
||||
bl_label = "Add Property Enumeration"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
index: bpy.props.IntProperty()
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ class AssignBaseQto(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class CalculateAllQuantities(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.calculate_all_quantities"
|
||||
bl_label = "Calculate all quantities"
|
||||
bl_label = "Calculate All Quantities"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Calculate all possible quantities and assign them to selected object"
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class LoadResourceProperties(bpy.types.Operator):
|
||||
|
||||
class AddResource(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_resource"
|
||||
bl_label = "Add resource"
|
||||
bl_label = "Add Resource"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
parent_resource: bpy.props.IntProperty()
|
||||
|
||||
@@ -168,9 +168,14 @@ class UnlinkObject(bpy.types.Operator):
|
||||
objects = context.selected_objects
|
||||
for obj in objects:
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
if obj in IfcStore.edited_objs:
|
||||
IfcStore.edited_objs.remove(obj)
|
||||
IfcStore.unlink_element(obj=obj)
|
||||
if obj.data:
|
||||
obj.data = obj.data.copy()
|
||||
for material_slot in obj.material_slots:
|
||||
if material_slot.material:
|
||||
material_slot.material = material_slot.material.copy()
|
||||
blenderbim.core.style.unlink_style(tool.Ifc, tool.Style, obj=material_slot.material)
|
||||
blenderbim.core.material.unlink_material(tool.Ifc, obj=material_slot.material)
|
||||
if "Ifc" in obj.name and "/" in obj.name:
|
||||
|
||||
@@ -20,7 +20,6 @@ import bpy
|
||||
from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.HighlightProductRelatedTask,
|
||||
operator.ExpandAllTasks,
|
||||
operator.ContractAllTasks,
|
||||
operator.AddSummaryTask,
|
||||
@@ -113,6 +112,8 @@ classes = (
|
||||
operator.VisualiseWorkScheduleDateRange,
|
||||
operator.LoadTaskAnimationColors,
|
||||
operator.DisableEditingTaskAnimationColors,
|
||||
operator.LoadProductTasks,
|
||||
operator.HighlightTask,
|
||||
prop.WorkPlan,
|
||||
prop.BIMWorkPlanProperties,
|
||||
prop.Task,
|
||||
@@ -140,6 +141,8 @@ classes = (
|
||||
ui.BIM_PT_4D_Tools,
|
||||
ui.BIM_PT_Task_Bar_Creator,
|
||||
ui.BIM_UL_animation_colors,
|
||||
ui.BIM_UL_product_input_tasks,
|
||||
ui.BIM_UL_product_output_tasks,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def parse_duration_as_blender_props(dt, simplify=True):
|
||||
seconds = getattr(dt, "seconds", 0)
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
days = getattr(dt, "days",0)
|
||||
days = getattr(dt, "days", 0)
|
||||
months = int(getattr(dt, "months", 0))
|
||||
years = int(getattr(dt, "years", 0))
|
||||
return {
|
||||
|
||||
@@ -454,11 +454,8 @@ class UnassignProcess(bpy.types.Operator):
|
||||
)
|
||||
else:
|
||||
core.unassign_input_products(
|
||||
tool.Ifc,
|
||||
tool.Sequence,
|
||||
tool.Spatial,
|
||||
task=tool.Ifc.get().by_id(self.task)
|
||||
)
|
||||
tool.Ifc, tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task)
|
||||
)
|
||||
elif self.related_object_type == "CONTROL":
|
||||
pass # TODO
|
||||
return {"FINISHED"}
|
||||
@@ -606,7 +603,7 @@ class ImportP6XER(bpy.types.Operator, ImportHelper):
|
||||
|
||||
class ImportPP(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "import_pp.bim"
|
||||
bl_label = "Import Powerproject pp"
|
||||
bl_label = "Import Powerproject .pp"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
filename_ext = ".pp"
|
||||
filter_glob: bpy.props.StringProperty(default="*.pp", options={"HIDDEN"})
|
||||
@@ -1006,6 +1003,7 @@ class VisualiseWorkScheduleDate(bpy.types.Operator):
|
||||
core.visualise_work_schedule_date(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class GuessDateRange(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.guess_date_range"
|
||||
bl_label = "Guess Work Schedule Date Range"
|
||||
@@ -1024,7 +1022,10 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
has_start, has_finish = bpy.context.scene.BIMWorkScheduleProperties.visualisation_start, bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish
|
||||
has_start, has_finish = (
|
||||
bpy.context.scene.BIMWorkScheduleProperties.visualisation_start,
|
||||
bpy.context.scene.BIMWorkScheduleProperties.visualisation_finish,
|
||||
)
|
||||
return bool(has_start and has_finish) and not "-" in (has_start, has_finish)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -1106,7 +1107,7 @@ class BlenderBIM_DatePicker(bpy.types.Operator):
|
||||
|
||||
|
||||
class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
|
||||
bl_label = "set date"
|
||||
bl_label = "Set Date"
|
||||
bl_idname = "bim.datepicker_setdate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
selected_date: bpy.props.StringProperty()
|
||||
@@ -1117,7 +1118,7 @@ class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
|
||||
|
||||
|
||||
class BlenderBIM_RedrawDatePicker(bpy.types.Operator):
|
||||
bl_label = "redraw datepicker window"
|
||||
bl_label = "Redraw Datepicker Window"
|
||||
bl_idname = "bim.redraw_datepicker"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
action: bpy.props.StringProperty()
|
||||
@@ -1230,20 +1231,9 @@ class CalculateTaskDuration(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.calculate_task_duration(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
|
||||
|
||||
class HighlightProductRelatedTask(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.highlight_product_related_task"
|
||||
bl_label = "Highlights the related task"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Finds the related Task"
|
||||
product_type: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.highlight_product_related_task(tool.Sequence, tool.Spatial, product_type=self.product_type)
|
||||
|
||||
|
||||
class ExpandAllTasks(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.expand_all_tasks"
|
||||
bl_label = "Expands all tasks"
|
||||
bl_label = "Expands All Tasks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Finds the related Task"
|
||||
product_type: bpy.props.StringProperty()
|
||||
@@ -1254,7 +1244,7 @@ class ExpandAllTasks(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
class ContractAllTasks(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.contract_all_tasks"
|
||||
bl_label = "Expands all tasks"
|
||||
bl_label = "Expands All Tasks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Finds the related Task"
|
||||
product_type: bpy.props.StringProperty()
|
||||
@@ -1293,6 +1283,7 @@ class DisableEditingTaskAnimationColors(bpy.types.Operator):
|
||||
core.disable_editing_task_animation_colors(tool.Sequence)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_task"
|
||||
bl_label = "Copy Task"
|
||||
@@ -1300,4 +1291,39 @@ class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
|
||||
task: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
core.duplicate_task(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
core.duplicate_task(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
|
||||
|
||||
class LoadProductTasks(bpy.types.Operator):
|
||||
bl_idname = "bim.load_product_tasks"
|
||||
bl_label = "Load Product Tasks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
if not context.active_object:
|
||||
return False
|
||||
if not context.active_object.BIMObjectProperties.ifc_definition_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
core.load_product_tasks(
|
||||
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class HighlightTask(bpy.types.Operator):
|
||||
bl_idname = "bim.highlight_task"
|
||||
bl_label = "Highlight Task"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
task: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
r = core.highlight_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
|
||||
if isinstance(r, str):
|
||||
self.report({"WARNING"}, r)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -96,6 +96,14 @@ def update_active_task_outputs(self, context):
|
||||
bpy.ops.bim.load_task_outputs()
|
||||
|
||||
|
||||
def update_active_task_resources(self, context):
|
||||
bpy.ops.bim.load_task_resources()
|
||||
|
||||
|
||||
def update_active_task_inputs(self, context):
|
||||
bpy.ops.bim.load_task_inputs()
|
||||
|
||||
|
||||
def updateTaskName(self, context):
|
||||
props = context.scene.BIMWorkScheduleProperties
|
||||
if not props.is_task_update_enabled or self.name == "Unnamed":
|
||||
@@ -389,9 +397,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
active_task_input_index: IntProperty(name="Active Task Input Index")
|
||||
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
|
||||
active_task_output_index: IntProperty(name="Active Task Output Index")
|
||||
is_nested_task_outputs: BoolProperty(
|
||||
name="Is Nested Task Outputs", default=False, update=update_active_task_outputs
|
||||
)
|
||||
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
|
||||
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
|
||||
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
|
||||
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
|
||||
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
|
||||
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
|
||||
active_product_input_task_index: IntProperty(name="Active Product Input Task Index")
|
||||
|
||||
|
||||
class BIMTaskTreeProperties(PropertyGroup):
|
||||
|
||||
@@ -433,6 +433,8 @@ class BIM_PT_task_icom(Panel):
|
||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.task = task.ifc_definition_id
|
||||
|
||||
row2 = col.row()
|
||||
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
|
||||
row2 = col.row()
|
||||
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
|
||||
|
||||
@@ -456,6 +458,9 @@ class BIM_PT_task_icom(Panel):
|
||||
op.related_object_type = "RESOURCE"
|
||||
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
|
||||
|
||||
row2 = col.row()
|
||||
row2.prop(self.props, "show_nested_resources", text="Show Nested")
|
||||
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_task_resources", "", self.props, "task_resources", self.props, "active_task_resource_index"
|
||||
@@ -481,7 +486,7 @@ class BIM_PT_task_icom(Panel):
|
||||
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
|
||||
op.task = task.ifc_definition_id
|
||||
row2 = col.row()
|
||||
row2.prop(self.props, "is_nested_task_outputs", text="Show Nested")
|
||||
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
|
||||
row2 = col.row()
|
||||
row2.template_list(
|
||||
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
|
||||
@@ -534,6 +539,26 @@ class BIM_UL_task_outputs(UIList):
|
||||
row.prop(item, "name", emboss=False, text="")
|
||||
|
||||
|
||||
class BIM_UL_product_input_tasks(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE")
|
||||
op.task = item.ifc_definition_id
|
||||
row.split(factor=0.8)
|
||||
row.label(text=item.name)
|
||||
|
||||
|
||||
class BIM_UL_product_output_tasks(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
row = layout.row(align=True)
|
||||
op = row.operator("bim.highlight_task", text="", icon="STYLUS_PRESSURE")
|
||||
op.task = item.ifc_definition_id
|
||||
row.split(factor=0.8)
|
||||
row.label(text=item.name)
|
||||
|
||||
|
||||
class BIM_UL_tasks(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
@@ -798,14 +823,32 @@ class BIM_PT_4D_Tools(Panel):
|
||||
bl_category = "4D/5D Toolkit"
|
||||
|
||||
def draw(self, context):
|
||||
self.props = context.scene.BIMWorkScheduleProperties
|
||||
row = self.layout.row()
|
||||
row.operator(
|
||||
"bim.highlight_product_related_task", text="Find Input-related Task ", icon="STYLUS_PRESSURE"
|
||||
).product_type = "Input"
|
||||
row = self.layout.row()
|
||||
row.operator(
|
||||
"bim.highlight_product_related_task", text="Go to Output-related Task ", icon="STYLUS_PRESSURE"
|
||||
).product_type = "Output"
|
||||
row.operator("bim.load_product_tasks", text="Load Tasks", icon="FILE_REFRESH")
|
||||
|
||||
grid = self.layout.grid_flow(columns=2, even_columns=True)
|
||||
col1 = grid.column()
|
||||
col1.label(text="Product Input Tasks")
|
||||
col1.template_list(
|
||||
"BIM_UL_product_input_tasks",
|
||||
"",
|
||||
self.props,
|
||||
"product_input_tasks",
|
||||
self.props,
|
||||
"active_product_input_task_index",
|
||||
)
|
||||
|
||||
col2 = grid.column()
|
||||
col2.label(text="Product Output Tasks")
|
||||
col2.template_list(
|
||||
"BIM_UL_product_output_tasks",
|
||||
"",
|
||||
self.props,
|
||||
"product_output_tasks",
|
||||
self.props,
|
||||
"active_product_output_task_index",
|
||||
)
|
||||
|
||||
|
||||
class BIM_PT_Task_Bar_Creator(Panel):
|
||||
|
||||
@@ -34,6 +34,7 @@ class StylesData:
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"style_types": cls.style_types(), "total_styles": cls.total_styles()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def style_types(cls):
|
||||
@@ -55,7 +56,12 @@ class StyleAttributesData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {"attributes": cls.get_attributes()}
|
||||
cls.data = {"ifc_style_id": cls.ifc_style_id(), "attributes": cls.get_attributes()}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def ifc_style_id(cls):
|
||||
return bpy.context.active_object.active_material.BIMMaterialProperties.ifc_style_id
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
|
||||
@@ -23,8 +23,6 @@ from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
|
||||
from bl_ui.properties_material import MaterialButtonsPanel
|
||||
|
||||
|
||||
|
||||
|
||||
class BIM_PT_styles(Panel):
|
||||
bl_label = "IFC Styles"
|
||||
bl_idname = "BIM_PT_styles"
|
||||
@@ -93,7 +91,7 @@ class BIM_PT_style(MaterialButtonsPanel, Panel):
|
||||
row.operator("bim.remove_style", icon="X", text="").style = props.ifc_style_id
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(mat, "diffuse_color", text="Color")
|
||||
|
||||
|
||||
else:
|
||||
row.operator("bim.add_style", icon="ADD")
|
||||
|
||||
@@ -117,6 +115,11 @@ class BIM_PT_style_attributes(Panel):
|
||||
def draw(self, context):
|
||||
if not StyleAttributesData.is_loaded:
|
||||
StyleAttributesData.load()
|
||||
elif (
|
||||
context.active_object.active_material.BIMMaterialProperties.ifc_style_id
|
||||
!= StyleAttributesData.data["ifc_style_id"]
|
||||
):
|
||||
StyleAttributesData.load()
|
||||
|
||||
obj = context.active_object.active_material
|
||||
mprops = obj.BIMMaterialProperties
|
||||
|
||||
@@ -64,19 +64,49 @@ class UnassignType(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
def exclude_callback(attribute):
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
self.file = IfcStore.get_file()
|
||||
related_objects = (
|
||||
objs = (
|
||||
[bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
|
||||
)
|
||||
for related_object in related_objects:
|
||||
oprops = related_object.BIMObjectProperties
|
||||
ifcopenshell.api.run(
|
||||
"type.unassign_type",
|
||||
self.file,
|
||||
**{
|
||||
"related_object": self.file.by_id(oprops.ifc_definition_id),
|
||||
},
|
||||
)
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or element.is_a("IfcElementType"):
|
||||
continue
|
||||
ifcopenshell.api.run("type.unassign_type", self.file, related_object=element)
|
||||
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
new_active_representation = None
|
||||
|
||||
if element.Representation:
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations.
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(), resolved_representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback
|
||||
)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
should_reload=False,
|
||||
is_global=False,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
from blenderbim.bim.module.type.data import TypeData
|
||||
|
||||
|
||||
@@ -69,7 +70,7 @@ class BIM_PT_type(Panel):
|
||||
|
||||
row.prop(props, "relating_type_class", text="")
|
||||
if type_prop.get_relating_type(None, context):
|
||||
row.prop(props, "relating_type", text="")
|
||||
prop_with_search(row, props, "relating_type", text="")
|
||||
row.operator("bim.assign_type", icon="CHECKMARK", text="")
|
||||
else:
|
||||
row.label(text="No Types Found")
|
||||
|
||||
@@ -101,6 +101,7 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
|
||||
bl_idname = "bim.reload_selected_ifc_file"
|
||||
bl_label = "Reload selected IFC File"
|
||||
@@ -114,6 +115,7 @@ class ReloadSelectedIfcFile(bpy.types.Operator, IFCFileSelector):
|
||||
context.scene.BIMProperties.ifc_file = context.scene.BIMProperties.ifc_file
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectDataDir(bpy.types.Operator):
|
||||
bl_idname = "bim.select_data_dir"
|
||||
bl_label = "Select Data Directory"
|
||||
@@ -211,6 +213,11 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
|
||||
|
||||
def create_section_compare_node(self):
|
||||
group = bpy.data.node_groups.new("Section Compare", type="ShaderNodeTree")
|
||||
group.inputs.new("NodeSocketFloat", "Value")
|
||||
group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group
|
||||
group.inputs.new("NodeSocketVector", "Vector")
|
||||
group.outputs.new("NodeSocketFloat", "Value")
|
||||
group.outputs.new("NodeSocketFloat", "Line Decorator")
|
||||
group_input = group.nodes.new(type="NodeGroupInput")
|
||||
group_input.location = 0, 50
|
||||
|
||||
@@ -222,6 +229,12 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
|
||||
greater.inputs[1].default_value = 0
|
||||
greater.location = 400, 0
|
||||
|
||||
compare = group.nodes.new(type="ShaderNodeMath")
|
||||
compare.operation = "COMPARE"
|
||||
compare.inputs[1].default_value = 0
|
||||
compare.inputs[2].default_value = 0.04
|
||||
compare.location = 400, -200
|
||||
|
||||
multiply = group.nodes.new(type="ShaderNodeMath")
|
||||
multiply.operation = "MULTIPLY"
|
||||
multiply.inputs[0].default_value = 1
|
||||
@@ -230,57 +243,68 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
|
||||
group_output = group.nodes.new(type="NodeGroupOutput")
|
||||
group_output.location = 800, 0
|
||||
|
||||
group.links.new(group_input.outputs[""], multiply.inputs[0])
|
||||
group.links.new(group_input.outputs[""], separate_xyz.inputs[0])
|
||||
group.links.new(group_input.outputs["Value"], multiply.inputs[0])
|
||||
group.links.new(group_input.outputs["Vector"], separate_xyz.inputs[0])
|
||||
group.links.new(separate_xyz.outputs[2], greater.inputs[0])
|
||||
group.links.new(greater.outputs[0], multiply.inputs[1])
|
||||
group.links.new(multiply.outputs[0], group_output.inputs[""])
|
||||
group.links.new(multiply.outputs[0], group_output.inputs["Value"])
|
||||
group.links.new(separate_xyz.outputs[2], compare.inputs[0])
|
||||
group.links.new(compare.outputs[0], group_output.inputs["Line Decorator"])
|
||||
|
||||
def create_section_override_node(self, obj, context):
|
||||
group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree")
|
||||
group.inputs.new("NodeSocketShader", "Shader")
|
||||
group.outputs.new("NodeSocketShader", "Shader")
|
||||
links = group.links
|
||||
nodes = group.nodes
|
||||
|
||||
group_input = nodes.new(type="NodeGroupInput")
|
||||
group_output = nodes.new(type="NodeGroupOutput")
|
||||
group_output.location = 600, 250
|
||||
group_output.location = 800, 250
|
||||
|
||||
backfacing_mix = nodes.new(type="ShaderNodeMixShader")
|
||||
backfacing_mix.location = group_output.location - Vector((400, 350))
|
||||
mix_decorator = group.nodes.new(type="ShaderNodeMixShader")
|
||||
mix_decorator.name = "Line Decorator Mix"
|
||||
mix_decorator.location = group_output.location - Vector((200, 0))
|
||||
|
||||
mix_section = group.nodes.new(type="ShaderNodeMixShader")
|
||||
mix_section.name = "Section Mix"
|
||||
mix_section.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
|
||||
mix_section.location = mix_decorator.location - Vector((200, 200))
|
||||
|
||||
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
|
||||
transparent.location = mix_section.location - Vector((200, 100))
|
||||
|
||||
mix_backfacing = nodes.new(type="ShaderNodeMixShader")
|
||||
mix_backfacing.location = mix_section.location - Vector((200, 0))
|
||||
|
||||
group_input.location = mix_backfacing.location - Vector((200, 50))
|
||||
|
||||
backfacing = nodes.new(type="ShaderNodeNewGeometry")
|
||||
backfacing.location = backfacing_mix.location + Vector((-200, 200))
|
||||
group_input.location = backfacing_mix.location - Vector((200, 50))
|
||||
backfacing.location = mix_backfacing.location + Vector((-200, 200))
|
||||
|
||||
emission = nodes.new(type="ShaderNodeEmission")
|
||||
emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1]
|
||||
emission.location = backfacing_mix.location - Vector((200, 150))
|
||||
|
||||
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
|
||||
transparent.location = group_output.location - Vector((400, 100))
|
||||
|
||||
section_mix = group.nodes.new(type="ShaderNodeMixShader")
|
||||
section_mix.name = "Section Mix"
|
||||
section_mix.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
|
||||
section_mix.location = group_output.location - Vector((200, 0))
|
||||
emission.location = mix_backfacing.location - Vector((200, 150))
|
||||
|
||||
cut_obj = nodes.new(type="ShaderNodeTexCoord")
|
||||
cut_obj.object = obj
|
||||
cut_obj.location = group_output.location - Vector((800, 150))
|
||||
cut_obj.location = backfacing.location - Vector((200, 200))
|
||||
|
||||
section_compare = nodes.new(type="ShaderNodeGroup")
|
||||
section_compare.node_tree = bpy.data.node_groups.get("Section Compare")
|
||||
section_compare.name = "Last Section Compare"
|
||||
section_compare.location = group_output.location - Vector((600, 0))
|
||||
section_compare.location = backfacing.location + Vector((0, 200))
|
||||
|
||||
links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
|
||||
links.new(backfacing.outputs["Backfacing"], backfacing_mix.inputs[0])
|
||||
links.new(group_input.outputs[""], backfacing_mix.inputs[1])
|
||||
links.new(emission.outputs["Emission"], backfacing_mix.inputs[2])
|
||||
links.new(section_compare.outputs[0], section_mix.inputs[0])
|
||||
links.new(transparent.outputs["BSDF"], section_mix.inputs[1])
|
||||
links.new(backfacing_mix.outputs["Shader"], section_mix.inputs[2])
|
||||
links.new(section_mix.outputs["Shader"], group_output.inputs[""])
|
||||
links.new(backfacing.outputs["Backfacing"], mix_backfacing.inputs[0])
|
||||
links.new(group_input.outputs["Shader"], mix_backfacing.inputs[1])
|
||||
links.new(emission.outputs["Emission"], mix_backfacing.inputs[2])
|
||||
links.new(section_compare.outputs["Value"], mix_section.inputs[0])
|
||||
links.new(transparent.outputs[0], mix_section.inputs[1])
|
||||
links.new(mix_backfacing.outputs["Shader"], mix_section.inputs[2])
|
||||
links.new(section_compare.outputs["Line Decorator"], mix_decorator.inputs[0])
|
||||
links.new(mix_section.outputs["Shader"], mix_decorator.inputs[1])
|
||||
links.new(mix_decorator.outputs["Shader"], group_output.inputs["Shader"])
|
||||
|
||||
def append_obj_to_section_override_node(self, obj):
|
||||
group = bpy.data.node_groups.get("Section Override")
|
||||
@@ -695,7 +719,7 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
|
||||
|
||||
class EditBlenderCollection(bpy.types.Operator):
|
||||
bl_idname = "bim.edit_blender_collection"
|
||||
bl_label = "Add or Remove blender collection item"
|
||||
bl_label = "Add or Remove Blender Collection Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
option: bpy.props.StringProperty(description="add or remove item from collection")
|
||||
collection: bpy.props.StringProperty(description="collection to be edited")
|
||||
|
||||
@@ -173,6 +173,18 @@ def update_section_color(self, context):
|
||||
pass
|
||||
|
||||
|
||||
def update_section_line_decorator(self, context):
|
||||
compare_node_group = bpy.data.node_groups.get("Section Compare")
|
||||
if compare_node_group is None:
|
||||
return
|
||||
for node in compare_node_group.nodes:
|
||||
if not hasattr(node, "operation"):
|
||||
continue
|
||||
if node.operation == "COMPARE":
|
||||
node.inputs[2].default_value = self.section_line_decorator_width
|
||||
break
|
||||
|
||||
|
||||
class StrProperty(PropertyGroup):
|
||||
pass
|
||||
|
||||
@@ -313,13 +325,20 @@ class BIMProperties(PropertyGroup):
|
||||
last_transaction: StringProperty(name="Last Transaction")
|
||||
should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False)
|
||||
section_plane_colour: FloatVectorProperty(
|
||||
name="Temporary Section Cutaway Colour",
|
||||
name="Cutaway Colour",
|
||||
subtype="COLOR",
|
||||
default=(1, 0, 0),
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
update=update_section_color,
|
||||
)
|
||||
section_line_decorator_width: FloatProperty(
|
||||
name="Line Decorator Width",
|
||||
default=0.04,
|
||||
min=0.0,
|
||||
soft_max=1.0,
|
||||
update=update_section_line_decorator,
|
||||
)
|
||||
area_unit: EnumProperty(
|
||||
default="SQUARE_METRE",
|
||||
items=[
|
||||
|
||||
@@ -78,11 +78,9 @@ class BIM_PT_section_plane(Panel):
|
||||
layout.use_property_split = True
|
||||
props = context.scene.BIMProperties
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "should_section_selected_objects")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(props, "section_plane_colour")
|
||||
layout.prop(props, "should_section_selected_objects")
|
||||
layout.prop(props, "section_plane_colour")
|
||||
layout.prop(props, "section_line_decorator_width")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.add_section_plane")
|
||||
@@ -184,6 +182,23 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
row.prop(context.scene.BIMProperties, "data_dir")
|
||||
row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "docs_dir")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "sheets_dir")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "titleblocks_dir")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "drawings_dir")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "stylesheet_path")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "markers_path")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "symbols_path")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.DocProperties, "patterns_path")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.configure_visibility")
|
||||
|
||||
|
||||
@@ -45,13 +45,13 @@ def disable_editing_assigned_product(drawing, obj=None):
|
||||
def edit_assigned_product(ifc, drawing, obj=None, product=None):
|
||||
element = ifc.get_entity(obj)
|
||||
existing_product = drawing.get_assigned_product(element)
|
||||
if existing_product == product:
|
||||
return
|
||||
if existing_product:
|
||||
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
|
||||
if product:
|
||||
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
|
||||
drawing.update_text_value(obj)
|
||||
if existing_product != product:
|
||||
if existing_product:
|
||||
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
|
||||
if product:
|
||||
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
|
||||
drawing.update_text_value(obj)
|
||||
|
||||
drawing.disable_editing_assigned_product(obj)
|
||||
|
||||
|
||||
@@ -66,19 +66,31 @@ def disable_editing_sheets(drawing):
|
||||
|
||||
def add_sheet(ifc, drawing, titleblock=None):
|
||||
sheet = ifc.run("document.add_information")
|
||||
layout = ifc.run("document.add_reference", information=sheet)
|
||||
titleblock_reference = ifc.run("document.add_reference", information=sheet)
|
||||
identification = drawing.generate_sheet_identification()
|
||||
identification = drawing.ensure_unique_identification(identification)
|
||||
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "DOCUMENTATION"}
|
||||
if ifc.get_schema() == "IFC2X3":
|
||||
attributes["DocumentId"] = attributes["Identification"]
|
||||
del attributes["Identification"]
|
||||
attributes = {"DocumentId": identification, "Name": "UNTITLED", "Scope": "SHEET"}
|
||||
else:
|
||||
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"}
|
||||
ifc.run("document.edit_information", information=sheet, attributes=attributes)
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=layout,
|
||||
attributes={"Location": drawing.get_default_sheet_path(identification, "UNTITLED"), "Description": "LAYOUT"},
|
||||
)
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=titleblock_reference,
|
||||
attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"},
|
||||
)
|
||||
drawing.create_svg_sheet(sheet, titleblock)
|
||||
drawing.import_sheets()
|
||||
|
||||
|
||||
def open_sheet(drawing, sheet=None):
|
||||
drawing.open_svg(drawing.get_document_uri(sheet))
|
||||
drawing.open_svg(drawing.get_document_uri(sheet, "LAYOUT"))
|
||||
|
||||
|
||||
def remove_sheet(ifc, drawing, sheet=None):
|
||||
@@ -86,6 +98,11 @@ def remove_sheet(ifc, drawing, sheet=None):
|
||||
drawing.import_sheets()
|
||||
|
||||
|
||||
def update_sheet_name(ifc, drawing, sheet=None, name=None):
|
||||
if drawing.get_name(sheet) != name:
|
||||
ifc.run("document.edit_information", information=sheet, attributes={"Name": name})
|
||||
|
||||
|
||||
def load_schedules(drawing):
|
||||
drawing.import_schedules()
|
||||
drawing.enable_editing_schedules()
|
||||
@@ -98,13 +115,13 @@ def disable_editing_schedules(drawing):
|
||||
def add_schedule(ifc, drawing, uri=None):
|
||||
schedule = ifc.run("document.add_information")
|
||||
reference = ifc.run("document.add_reference", information=schedule)
|
||||
name = drawing.get_path_filename(uri)
|
||||
if ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"}
|
||||
ifc.run("document.edit_information", information=schedule, attributes=attributes)
|
||||
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
|
||||
attributes = {"DocumentId": "X", "Name": name, "Scope": "SCHEDULE"}
|
||||
else:
|
||||
attributes = {"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": uri}
|
||||
ifc.run("document.edit_information", information=schedule, attributes=attributes)
|
||||
attributes = {"Identification": "X", "Name": name, "Scope": "SCHEDULE"}
|
||||
ifc.run("document.edit_information", information=schedule, attributes=attributes)
|
||||
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
|
||||
drawing.import_schedules()
|
||||
|
||||
|
||||
@@ -114,7 +131,7 @@ def remove_schedule(ifc, drawing, schedule=None):
|
||||
|
||||
|
||||
def open_schedule(drawing, schedule=None):
|
||||
drawing.open_spreadsheet(drawing.get_schedule_location(schedule))
|
||||
drawing.open_spreadsheet(drawing.get_document_uri(schedule))
|
||||
|
||||
|
||||
def update_schedule_name(ifc, drawing, schedule=None, name=None):
|
||||
@@ -159,8 +176,22 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
|
||||
"HasLinework": True,
|
||||
"HasAnnotation": True,
|
||||
"GlobalReferencing": True,
|
||||
"Stylesheet": drawing.get_default_drawing_resource_path("Stylesheet"),
|
||||
"Markers": drawing.get_default_drawing_resource_path("Markers"),
|
||||
"Symbols": drawing.get_default_drawing_resource_path("Symbols"),
|
||||
"Patterns": drawing.get_default_drawing_resource_path("Patterns"),
|
||||
},
|
||||
)
|
||||
information = ifc.run("document.add_information")
|
||||
uri = drawing.get_default_drawing_path(drawing_name)
|
||||
reference = ifc.run("document.add_reference", information=information)
|
||||
if ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"DocumentId": "X", "Name": drawing_name, "Scope": "DRAWING"}
|
||||
else:
|
||||
attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"}
|
||||
ifc.run("document.edit_information", information=information, attributes=attributes)
|
||||
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
|
||||
ifc.run("document.assign_document", product=element, document=reference)
|
||||
drawing.import_drawings()
|
||||
|
||||
|
||||
@@ -199,6 +230,7 @@ def remove_drawing(ifc, drawing_tool, drawing=None):
|
||||
if reference_obj:
|
||||
drawing_tool.delete_object(reference_obj)
|
||||
ifc.run("root.remove_product", product=reference)
|
||||
ifc.run("document.remove_information", information=drawing_tool.get_drawing_document(drawing))
|
||||
ifc.run("root.remove_product", product=drawing)
|
||||
drawing_tool.import_drawings()
|
||||
|
||||
@@ -232,12 +264,13 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
|
||||
)
|
||||
ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), products=[element])
|
||||
collector.assign(obj)
|
||||
drawing_tool.setup_annotation_object(obj, object_type)
|
||||
drawing_tool.enable_editing(obj)
|
||||
|
||||
|
||||
def build_schedule(drawing, schedule=None):
|
||||
drawing.create_svg_schedule(schedule)
|
||||
drawing.open_svg(drawing.get_document_uri(schedule))
|
||||
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))
|
||||
|
||||
|
||||
def sync_references(ifc, collector, drawing_tool, drawing=None):
|
||||
|
||||
@@ -454,6 +454,13 @@ def calculate_task_duration(ifc, sequence, task=None):
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
def highlight_task(sequence, task=None):
|
||||
work_schedule = sequence.get_work_schedule(task)
|
||||
is_work_schedule_active = sequence.is_work_schedule_active(work_schedule)
|
||||
if is_work_schedule_active:
|
||||
sequence.highlight_task(task)
|
||||
|
||||
|
||||
def highlight_product_related_task(sequence, spatial, product_type=None):
|
||||
products = spatial.get_selected_products()
|
||||
if products:
|
||||
@@ -514,3 +521,6 @@ def visualise_work_schedule_date(sequence, work_schedule=None):
|
||||
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)
|
||||
@@ -71,16 +71,21 @@ class Aggregate:
|
||||
|
||||
|
||||
@interface
|
||||
class Blender: pass
|
||||
def create_ifc_object(cls, ifc_class, name, data): pass
|
||||
def get_name(cls, ifc_class, name): pass
|
||||
def get_obj_ifc_definition_id(cls, obj, obj_type): pass
|
||||
def get_selected_objects(cls): pass
|
||||
def set_active_object(cls, obj): pass
|
||||
def apply_bmesh(cls, mesh, bm): pass
|
||||
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
|
||||
def get_viewport_context(cls): pass
|
||||
def update_viewport(cls): pass
|
||||
class Blender:
|
||||
def set_active_object(cls, obj): pass
|
||||
def get_name(cls, ifc_class, name): pass
|
||||
def get_selected_objects(cls): pass
|
||||
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass
|
||||
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass
|
||||
def is_ifc_object(cls, obj): pass
|
||||
def is_ifc_class_active(cls, ifc_class): pass
|
||||
def get_viewport_context(cls): pass
|
||||
def update_viewport(cls): pass
|
||||
def get_default_selection_keypmap(cls): pass
|
||||
def get_object_bounding_box(cls, obj): pass
|
||||
def apply_bmesh(cls, mesh, bm, obj=None): pass
|
||||
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
|
||||
def bmesh_join(cls, bm_a, bm_b, callback=None): pass
|
||||
|
||||
|
||||
@interface
|
||||
@@ -223,6 +228,7 @@ class Drawing:
|
||||
def activate_view(cls, camera): pass
|
||||
def copy_representation(cls, source, dest): pass
|
||||
def create_annotation_object(cls, drawing, object_type): pass
|
||||
def setup_annotation_object(cls, obj, object_type): pass
|
||||
def create_camera(cls, name, matrix): pass
|
||||
def create_svg_schedule(cls, schedule): pass
|
||||
def create_svg_sheet(cls, document, titleblock): pass
|
||||
@@ -249,15 +255,19 @@ class Drawing:
|
||||
def get_annotation_context(cls, target_view): pass
|
||||
def get_assigned_product(cls, element): pass
|
||||
def get_body_context(cls): pass
|
||||
def get_default_drawing_path(cls, name): pass
|
||||
def get_default_drawing_resource_path(cls, resource): pass
|
||||
def get_default_sheet_path(cls, identification, name): pass
|
||||
def get_document_uri(cls, document): pass
|
||||
def get_drawing_collection(cls, drawing): pass
|
||||
def get_drawing_document(cls, drawing): pass
|
||||
def get_drawing_group(cls, drawing): pass
|
||||
def get_path_filename(cls, uri): pass
|
||||
def get_drawing_references(cls, drawing): pass
|
||||
def get_drawing_target_view(cls, drawing): pass
|
||||
def get_group_elements(cls, group): pass
|
||||
def get_ifc_representation_class(cls, object_type): pass
|
||||
def get_name(cls, element): pass
|
||||
def get_schedule_location(cls, schedule): pass
|
||||
def get_text_literal(cls, obj): pass
|
||||
def remove_literal_from_annotation(cls, obj, literal): pass
|
||||
def synchronise_ifc_and_text_attributes(cls, obj): pass
|
||||
|
||||
@@ -160,12 +160,32 @@ class Blender:
|
||||
)
|
||||
return keymap
|
||||
|
||||
@classmethod
|
||||
def get_object_bounding_box(cls, obj):
|
||||
"""Returns dict with local min and max x, y, z values for the object.
|
||||
|
||||
Careful with using this method for objects in EDIT mode because
|
||||
it requires all EDIT mode changes to be applied.
|
||||
"""
|
||||
bound_box = obj.bound_box
|
||||
bbox_dict = {
|
||||
"min_x": bound_box[0][0],
|
||||
"max_x": bound_box[6][0],
|
||||
"min_y": bound_box[0][1],
|
||||
"max_y": bound_box[6][1],
|
||||
"min_z": bound_box[0][2],
|
||||
"max_z": bound_box[6][2],
|
||||
}
|
||||
return bbox_dict
|
||||
|
||||
## BMESH UTILS ##
|
||||
@classmethod
|
||||
def apply_bmesh(cls, mesh, bm):
|
||||
def apply_bmesh(cls, mesh, bm, obj=None):
|
||||
"""`obj` argument is not optional if you plan to update mesh in EDIT mode
|
||||
and it's possible that that mesh object won't be currenly active."""
|
||||
import bmesh
|
||||
|
||||
if bpy.context.object and bpy.context.object.mode == "EDIT":
|
||||
if mesh.is_editmode:
|
||||
# better to be safe because otherwise mesh won't be updated
|
||||
# and you won't get any errors
|
||||
if not bm.is_wrapped or hash(bmesh.from_edit_mesh(mesh)) != hash(bm):
|
||||
@@ -174,7 +194,13 @@ class Blender:
|
||||
"For applying bmesh in edit mode bmesh should be acquired with `bmesh.from_edit_mesh(me)`."
|
||||
)
|
||||
bmesh.update_edit_mesh(mesh)
|
||||
bpy.context.object.update_from_editmode()
|
||||
if not obj:
|
||||
if not bpy.context.object and bpy.context.object.data != mesh:
|
||||
raise Exception(
|
||||
"Error applying bmesh in EDIT object - object is "
|
||||
"not provided and can't be acquired from the context. "
|
||||
)
|
||||
obj.update_from_editmode()
|
||||
else:
|
||||
bm.to_mesh(mesh)
|
||||
# only freeing bmesh if object is in OBJECT mode
|
||||
@@ -190,7 +216,7 @@ class Blender:
|
||||
def get_bmesh_for_mesh(cls, mesh, clean=False):
|
||||
import bmesh
|
||||
|
||||
if bpy.context.object.mode == "EDIT":
|
||||
if mesh.is_editmode:
|
||||
bm = bmesh.from_edit_mesh(mesh)
|
||||
if clean:
|
||||
bm.clear()
|
||||
|
||||
@@ -92,14 +92,16 @@ class Document(blenderbim.core.tool.Document):
|
||||
for element in document.DocumentReferences or []:
|
||||
new = props.documents.add()
|
||||
new.ifc_definition_id = element.id()
|
||||
new.name = element.Name or "Unnamed"
|
||||
name = " - ".join([x for x in [element.Description, element.Location] if x])
|
||||
new.name = name or "Unnamed"
|
||||
new.identification = element.ItemReference or "*"
|
||||
new.is_information = False
|
||||
else:
|
||||
for element in document.HasDocumentReferences:
|
||||
new = props.documents.add()
|
||||
new.ifc_definition_id = element.id()
|
||||
new.name = element.Name or "Unnamed"
|
||||
name = " - ".join([x for x in [element.Description, element.Location] if x])
|
||||
new.name = name or "Unnamed"
|
||||
new.identification = element.Identification or "*"
|
||||
new.is_information = False
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ import blenderbim.bim.module.drawing.helper as helper
|
||||
from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData
|
||||
from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS
|
||||
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class Drawing(blenderbim.core.tool.Drawing):
|
||||
@classmethod
|
||||
@@ -74,8 +76,40 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
obj = annotation.Annotator.add_line_to_annotation(obj, co2, co1)
|
||||
elif object_type != "TEXT":
|
||||
obj = annotation.Annotator.add_line_to_annotation(obj)
|
||||
|
||||
return obj
|
||||
|
||||
# TODO: add a way for users to run this method to readjust the annotations
|
||||
# based on non-modifier stairs
|
||||
@classmethod
|
||||
def setup_annotation_object(cls, obj, object_type, related_object=None):
|
||||
"""Finish object's adjustments after both object and entity are created"""
|
||||
|
||||
if not related_object:
|
||||
related_object = bpy.context.active_object
|
||||
related_entity = tool.Ifc.get_entity(related_object)
|
||||
|
||||
if object_type == "STAIR_ARROW":
|
||||
if related_entity and related_entity.is_a("IfcStairFlight"):
|
||||
stair, stair_entity = related_object, related_entity
|
||||
arrow = obj
|
||||
arrow_element = tool.Ifc.get_entity(arrow)
|
||||
arrow.parent = stair
|
||||
|
||||
# place the arrow
|
||||
# NOTE: may not work correctly in EDIT mode
|
||||
bbox = tool.Blender.get_object_bounding_box(stair)
|
||||
float_is_zero = lambda f: 0.0001 >= f >= -0.0001
|
||||
arrow.location = Vector((bbox["min_x"], (bbox["max_y"] - bbox["min_y"]) / 2, bbox["max_z"]))
|
||||
last_step_x = max(v.co.x for v in stair.data.vertices if float_is_zero(v.co.z - bbox["max_z"]))
|
||||
|
||||
arrow.data.splines[0].points[0].co = Vector((0, 0, 0, 1))
|
||||
arrow.data.splines[0].points[1].co = Vector((last_step_x, 0, 0, 1))
|
||||
|
||||
if not cls.get_assigned_product(arrow_element):
|
||||
tool.Ifc.run("drawing.assign_product", relating_product=stair_entity, related_object=arrow_element)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def create_camera(cls, name, matrix):
|
||||
camera = bpy.data.objects.new(name, bpy.data.cameras.new(name))
|
||||
@@ -94,13 +128,17 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
@classmethod
|
||||
def create_svg_schedule(cls, schedule):
|
||||
schedule_creator = scheduler.Scheduler()
|
||||
schedule_creator.schedule(cls.get_schedule_location(schedule), cls.get_document_uri(schedule))
|
||||
schedule_creator.schedule(
|
||||
cls.get_document_uri(schedule), cls.get_path_with_ext(cls.get_document_uri(schedule), "svg")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_svg_sheet(cls, document, titleblock):
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
|
||||
sheet_builder.create(cls.get_document_uri(document), titleblock)
|
||||
uri = cls.get_document_uri(document)
|
||||
sheet_builder.create(uri, titleblock)
|
||||
return uri
|
||||
|
||||
@classmethod
|
||||
def delete_collection(cls, collection):
|
||||
@@ -112,7 +150,20 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
tool.Ifc.delete(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
obj_data = obj.data
|
||||
bpy.data.objects.remove(obj)
|
||||
if obj_data.users == 0: # in case we have drawing element types
|
||||
cls.remove_object_data(obj_data)
|
||||
|
||||
@classmethod
|
||||
def remove_object_data(cls, data):
|
||||
"""also removes all related objects"""
|
||||
if isinstance(data, bpy.types.Camera):
|
||||
bpy.data.cameras.remove(data)
|
||||
elif isinstance(data, bpy.types.Mesh):
|
||||
bpy.data.meshes.remove(data)
|
||||
elif isinstance(data, bpy.types.Curve):
|
||||
bpy.data.curves.remove(C.object.data)
|
||||
|
||||
@classmethod
|
||||
def delete_object(cls, obj):
|
||||
@@ -204,18 +255,33 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
def get_document_uri(cls, document):
|
||||
if hasattr(document, "Identification"):
|
||||
name = document.Identification or "X"
|
||||
else:
|
||||
name = document.DocumentId or "X"
|
||||
name += " - " + (document.Name or "Unnamed")
|
||||
if not hasattr(document, "Scope"):
|
||||
return
|
||||
if document.Scope == "DOCUMENTATION":
|
||||
return os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", name + ".svg")
|
||||
elif document.Scope == "SCHEDULE":
|
||||
return os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", name + ".svg")
|
||||
def get_document_uri(cls, document, description=None):
|
||||
if getattr(document, "Location", None):
|
||||
if os.path.isabs(document.Location):
|
||||
return document.Location
|
||||
ifc_path = tool.Ifc.get_path()
|
||||
if os.path.isfile(ifc_path):
|
||||
ifc_path = os.path.dirname(ifc_path)
|
||||
return os.path.abspath(os.path.join(ifc_path, document.Location))
|
||||
if document.is_a("IfcDocumentInformation"):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
references = document.DocumentReferences
|
||||
else:
|
||||
references = document.HasDocumentReferences
|
||||
for reference in references:
|
||||
if description and reference.Description != description:
|
||||
continue
|
||||
location = cls.get_document_uri(reference)
|
||||
if location:
|
||||
return location
|
||||
|
||||
@classmethod
|
||||
def get_path_filename(cls, path):
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
||||
|
||||
@classmethod
|
||||
def get_path_with_ext(cls, path, ext):
|
||||
return os.path.splitext(path)[0] + f".{ext}"
|
||||
|
||||
@classmethod
|
||||
def get_drawing_collection(cls, drawing):
|
||||
@@ -229,6 +295,12 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
if rel.is_a("IfcRelAssignsToGroup"):
|
||||
return rel.RelatingGroup
|
||||
|
||||
@classmethod
|
||||
def get_drawing_document(cls, drawing):
|
||||
for rel in drawing.HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesDocument"):
|
||||
return rel.RelatingDocument
|
||||
|
||||
@classmethod
|
||||
def get_drawing_references(cls, drawing):
|
||||
results = set()
|
||||
@@ -258,12 +330,6 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
def get_name(cls, element):
|
||||
return element.Name
|
||||
|
||||
@classmethod
|
||||
def get_schedule_location(cls, schedule):
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return schedule.DocumentReferences[0].Location
|
||||
return schedule.Location
|
||||
|
||||
@classmethod
|
||||
def generate_drawing_matrix(cls, target_view, location_hint):
|
||||
x = 0 if location_hint == 0 else bpy.context.scene.cursor.matrix[0][3]
|
||||
@@ -501,7 +567,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
props = bpy.context.scene.DocProperties
|
||||
expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded}
|
||||
props.sheets.clear()
|
||||
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]
|
||||
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"]
|
||||
for sheet in sheets:
|
||||
new = props.sheets.add()
|
||||
new.ifc_definition_id = sheet.id()
|
||||
@@ -517,22 +583,19 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
continue
|
||||
|
||||
for reference in cls.get_document_references(sheet):
|
||||
if reference.Description == "LAYOUT":
|
||||
continue # The layout itself is an internal detail and should not be visible to users
|
||||
new = props.sheets.add()
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.is_sheet = False
|
||||
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
new.identification = reference.ItemReference or "X"
|
||||
new.identification = reference.ItemReference or ""
|
||||
else:
|
||||
new.identification = reference.Identification or "X"
|
||||
new.identification = reference.Identification or ""
|
||||
|
||||
element = cls.get_reference_element(reference)
|
||||
if element:
|
||||
new.name = element.Name
|
||||
new.reference_type = "DRAWING"
|
||||
else:
|
||||
new.name = cls.get_reference_document(reference).Name or "Unnamed"
|
||||
new.reference_type = "SCHEDULE"
|
||||
new.name = os.path.basename(reference.Location)
|
||||
new.reference_type = reference.Description
|
||||
|
||||
@classmethod
|
||||
def import_text_attributes(cls, obj):
|
||||
@@ -674,6 +737,28 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
return location_hint + " " + target_view.split("_")[0]
|
||||
return target_view
|
||||
|
||||
@classmethod
|
||||
def get_default_sheet_path(cls, identification, name):
|
||||
return os.path.join(
|
||||
bpy.context.scene.DocProperties.sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_default_titleblock_path(cls, name):
|
||||
return os.path.join(bpy.context.scene.DocProperties.titleblocks_dir, cls.sanitise_filename(f"{name}.svg"))
|
||||
|
||||
@classmethod
|
||||
def get_default_drawing_path(cls, name):
|
||||
return os.path.join(bpy.context.scene.DocProperties.drawings_dir, cls.sanitise_filename(f"{name}.svg"))
|
||||
|
||||
@classmethod
|
||||
def sanitise_filename(cls, name):
|
||||
return "".join(x for x in name if (x.isalnum() or x in "._- "))
|
||||
|
||||
@classmethod
|
||||
def get_default_drawing_resource_path(cls, resource):
|
||||
return getattr(bpy.context.scene.DocProperties, f"{resource.lower()}_path") or None
|
||||
|
||||
@classmethod
|
||||
def get_potential_reference_elements(cls, drawing):
|
||||
elements = []
|
||||
@@ -1041,11 +1126,20 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
def replace_text_literal_variables(cls, text, product):
|
||||
if not product:
|
||||
return text
|
||||
|
||||
for command in re.findall("``.*?``", text):
|
||||
original_command = command
|
||||
for variable in re.findall("{{.*?}}", command):
|
||||
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
||||
command = command.replace(variable, repr(value))
|
||||
text = text.replace(original_command, str(eval(command[2:-2])))
|
||||
|
||||
for variable in re.findall("{{.*?}}", text):
|
||||
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = ", ".join(str(v) for v in value)
|
||||
text = text.replace(variable, str(value))
|
||||
|
||||
return text
|
||||
|
||||
@classmethod
|
||||
@@ -1197,3 +1291,13 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
project_collection.children["Views"].children[camera.users_collection[0].name].hide_viewport = False
|
||||
bpy.data.collections.get(camera.users_collection[0].name).hide_render = False
|
||||
tool.Spatial.set_active_object(camera)
|
||||
|
||||
# sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
|
||||
drawing_elements = cls.get_drawing_elements(tool.Ifc.get_entity(camera))
|
||||
for element in tool.Ifc.get().by_type("IfcElement"):
|
||||
if element.is_a() in ("IfcOpeningElement",):
|
||||
continue
|
||||
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.hide_viewport = element not in drawing_elements
|
||||
obj.hide_render = element not in drawing_elements
|
||||
|
||||
@@ -77,6 +77,11 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
if obj.data:
|
||||
return obj.data.copy()
|
||||
|
||||
@classmethod
|
||||
def get_active_representation(cls, obj):
|
||||
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
|
||||
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||
|
||||
@classmethod
|
||||
def get_cartesian_point_coordinate_offset(cls, obj):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
@@ -124,21 +129,32 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
|
||||
@classmethod
|
||||
def get_mesh_checksum(cls, mesh):
|
||||
# Get mesh data
|
||||
vertices = mesh.vertices[:]
|
||||
edges = mesh.edges[:]
|
||||
faces = mesh.polygons[:]
|
||||
data_bytes = b""
|
||||
if isinstance(mesh, bpy.types.Mesh):
|
||||
vertices = mesh.vertices[:]
|
||||
edges = mesh.edges[:]
|
||||
faces = mesh.polygons[:]
|
||||
|
||||
# Convert mesh data to bytes
|
||||
data_bytes = b''
|
||||
for v in vertices:
|
||||
data_bytes += struct.pack('3f', *v.co)
|
||||
for e in edges:
|
||||
data_bytes += struct.pack('2i', *e.vertices)
|
||||
for f in faces:
|
||||
data_bytes += struct.pack('%di' % len(f.vertices), *f.vertices)
|
||||
# Convert mesh data to bytes
|
||||
for v in vertices:
|
||||
data_bytes += struct.pack("3f", *v.co)
|
||||
for e in edges:
|
||||
data_bytes += struct.pack("2i", *e.vertices)
|
||||
for f in faces:
|
||||
data_bytes += struct.pack("%di" % len(f.vertices), *f.vertices)
|
||||
elif isinstance(mesh, bpy.types.Curve):
|
||||
splines = mesh.splines[:]
|
||||
|
||||
for spline in splines:
|
||||
if spline.type == "BEZIER":
|
||||
for bezier_point in spline.bezier_points:
|
||||
data_bytes += struct.pack("3f", *bezier_point.co)
|
||||
data_bytes += struct.pack("3f", *bezier_point.handle_left)
|
||||
data_bytes += struct.pack("3f", *bezier_point.handle_right)
|
||||
else:
|
||||
for point in spline.points:
|
||||
data_bytes += struct.pack("4f", *point.co)
|
||||
|
||||
# Generate hash of mesh data
|
||||
hasher = hashlib.sha1()
|
||||
hasher.update(data_bytes)
|
||||
return hasher.hexdigest()
|
||||
@@ -256,6 +272,30 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
def is_mapped_representation(cls, representation):
|
||||
return representation.RepresentationType == "MappedRepresentation"
|
||||
|
||||
@classmethod
|
||||
def is_meshlike(cls, representation):
|
||||
if representation.RepresentationType in (
|
||||
"AdvancedBrep",
|
||||
"Annotation2D",
|
||||
"BoundingBox",
|
||||
"Brep",
|
||||
"Curve",
|
||||
"Curve2D",
|
||||
"Curve3D",
|
||||
"FillArea",
|
||||
"GeometricCurveSet",
|
||||
"GeometricSet",
|
||||
"Point",
|
||||
"PointCloud",
|
||||
"Surface",
|
||||
"Surface2D",
|
||||
"Surface3D",
|
||||
"SurfaceModel",
|
||||
"Tessellation",
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_type_product(cls, element):
|
||||
return element.is_a("IfcTypeProduct")
|
||||
|
||||
@@ -46,6 +46,11 @@ class Ifc(blenderbim.core.tool.Ifc):
|
||||
if IfcStore.get_file():
|
||||
return IfcStore.get_file().schema
|
||||
|
||||
@classmethod
|
||||
def has_changed_shading(cls, obj):
|
||||
checksum = obj.BIMMaterialProperties.shading_checksum
|
||||
return checksum != repr(np.array(obj.diffuse_color).tobytes())
|
||||
|
||||
@classmethod
|
||||
def is_deleted(cls, element):
|
||||
return element.id() in IfcStore.deleted_ids
|
||||
@@ -101,7 +106,21 @@ class Ifc(blenderbim.core.tool.Ifc):
|
||||
|
||||
@classmethod
|
||||
def resolve_uri(cls, uri):
|
||||
return uri if not uri or os.path.isabs(uri) else os.path.join(os.path.dirname(cls.get_path()), uri)
|
||||
if os.path.isabs(uri):
|
||||
return uri
|
||||
ifc_path = cls.get_path()
|
||||
if os.path.isfile(ifc_path):
|
||||
ifc_path = os.path.dirname(ifc_path)
|
||||
return uri if not uri or os.path.isabs(uri) else os.path.join(ifc_path, uri)
|
||||
|
||||
@classmethod
|
||||
def get_relative_uri(cls, uri):
|
||||
if not os.path.isabs(uri):
|
||||
return uri
|
||||
ifc_path = cls.get_path()
|
||||
if os.path.isfile(ifc_path):
|
||||
ifc_path = os.path.dirname(ifc_path)
|
||||
return os.path.relpath(uri, ifc_path)
|
||||
|
||||
@classmethod
|
||||
def unlink(cls, element=None, obj=None):
|
||||
|
||||
@@ -414,12 +414,24 @@ class Model(blenderbim.core.tool.Model):
|
||||
obj_stack = [parent]
|
||||
|
||||
for array in data:
|
||||
if array["sync_children"]:
|
||||
removed_children = set(array["children"])
|
||||
for removed_child in removed_children:
|
||||
element = tool.Ifc.get().by_guid(removed_child)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
child_i = 0
|
||||
existing_children = set(array["children"])
|
||||
total_existing_children = len(array["children"])
|
||||
children_elements = []
|
||||
children_objs = []
|
||||
base_offset = Vector([array["x"], array["y"], array["z"]]) * unit_scale
|
||||
if array['dimension_input_type'] == "Total":
|
||||
divider = 1 if ((array["count"] - 1) == 0) else (array["count"] - 1)
|
||||
base_offset = Vector([array["x"] / divider, array["y"] / divider, array["z"] / divider]) * unit_scale
|
||||
else:
|
||||
base_offset = Vector([array["x"], array["y"], array["z"]]) * unit_scale
|
||||
for i in range(array["count"]):
|
||||
if i == 0:
|
||||
continue
|
||||
|
||||
@@ -157,10 +157,11 @@ class Qto(blenderbim.core.tool.Qto):
|
||||
print(f"RELATING COST QUANTITY NAME: {relating_cost_item["quantity_name"]}")
|
||||
...
|
||||
"""
|
||||
quantities = cls.get_base_qto(product).Quantities
|
||||
model = tool.Ifc.get()
|
||||
cost_items = model.by_type("IfcCostItem")
|
||||
result = []
|
||||
base_qto = cls.get_base_qto(product)
|
||||
quantities = base_qto.Quantities if base_qto else []
|
||||
|
||||
for cost_item in cost_items:
|
||||
cost_item_quantities = cost_item.CostQuantities if cost_item.CostQuantities is not None else []
|
||||
|
||||
@@ -422,7 +422,18 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def get_task_inputs(cls, task):
|
||||
return ifcopenshell.util.sequence.get_task_inputs(task)
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_inputs
|
||||
return ifcopenshell.util.sequence.get_task_inputs(task, is_deep)
|
||||
|
||||
@classmethod
|
||||
def get_task_outputs(cls, task):
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
|
||||
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
|
||||
|
||||
@classmethod
|
||||
def get_task_resources(cls, task):
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources
|
||||
return ifcopenshell.util.sequence.get_task_resource(task, is_deep)
|
||||
|
||||
@classmethod
|
||||
def load_task_inputs(cls, inputs):
|
||||
@@ -459,7 +470,7 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def get_task_outputs(cls, task):
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.is_nested_task_outputs
|
||||
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
|
||||
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
|
||||
|
||||
@classmethod
|
||||
@@ -777,7 +788,7 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
displayed_tasks = [item.ifc_definition_id for item in task_props.tasks]
|
||||
if not task.id() in displayed_tasks:
|
||||
expand_ancestors(task)
|
||||
task_index = [item.ifc_definition_id for item in task_props.tasks].index(task.id()) or 0
|
||||
task_index = displayed_tasks.index(task.id()) or 0
|
||||
bpy.context.scene.BIMWorkScheduleProperties.active_task_index = task_index
|
||||
|
||||
@classmethod
|
||||
@@ -1500,3 +1511,18 @@ class Sequence(blenderbim.core.tool.Sequence):
|
||||
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json)}))
|
||||
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
|
||||
|
||||
@classmethod
|
||||
def load_product_tasks(cls, product):
|
||||
props = bpy.context.scene.BIMWorkScheduleProperties
|
||||
props.is_task_update_enabled = False
|
||||
props.product_input_tasks.clear()
|
||||
props.product_output_tasks.clear()
|
||||
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
|
||||
for task in task_inputs or []:
|
||||
new = props.product_input_tasks.add()
|
||||
new.name = task.Name or "Unnamed"
|
||||
new.ifc_definition_id = task.id()
|
||||
for task in task_ouputs or []:
|
||||
new = props.product_output_tasks.add()
|
||||
new.name = task.Name or "Unnamed"
|
||||
new.ifc_definition_id = task.id()
|
||||
|
||||
@@ -3,6 +3,17 @@ Let's get started
|
||||
|
||||
The BlenderBIM Add-on lets you analyse, create, and modify OpenBIM with Blender.
|
||||
|
||||
Quickstart video tutorial
|
||||
-------------------------
|
||||
|
||||
Learn how to model a small building and create simple architectural plans, sections, 2D details, and sheet layouts in this short tutorial series.
|
||||
|
||||
`View all tutorial videos <https://www.youtube.com/playlist?list=PLMDcOjMJYxUPHHvEHqAsOuBdSPsp6or32>`__
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/videoseries?list=PLMDcOjMJYxUPHHvEHqAsOuBdSPsp6or32" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
.. note::
|
||||
|
||||
This documentation is incomplete. Would you like to help write more? `Get in touch! <https://osarch.org/chat/>`__
|
||||
|
||||
@@ -42,7 +42,7 @@ class LibraryGenerator:
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
|
||||
)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
|
||||
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
|
||||
|
||||
@@ -56,7 +56,6 @@ def get_simple_2dcurve_data(coords, fillets=[], fillet_radius=[], closed=True, i
|
||||
for co_i, co in enumerate(coords, 0):
|
||||
current_point = len(points)
|
||||
if co_i in fillets:
|
||||
|
||||
r = fillets[co_i]
|
||||
rsb = r * cos(pi / 4) # radius shift big
|
||||
rss = r - rsb # radius shift small
|
||||
@@ -230,7 +229,7 @@ class LibraryGenerator:
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
|
||||
)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
|
||||
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
|
||||
@@ -386,23 +385,40 @@ class LibraryGenerator:
|
||||
mirror_axis = V(0, 1)
|
||||
output = []
|
||||
|
||||
seat = builder.rectangle(size=V(width, depth))
|
||||
chair_back = builder.rectangle(size=V(width, thickness))
|
||||
items_to_adjust = []
|
||||
items_3d = []
|
||||
chair_back = None
|
||||
items_to_adjust.append(seat := builder.rectangle(size=V(width, depth)))
|
||||
|
||||
leg = builder.rectangle(size=V(thickness, thickness))
|
||||
legs = [leg] + builder.mirror(
|
||||
leg, mirror_axes=[V(1, 0), V(0, 1), V(1, 1)], mirror_point=V(width / 2, depth / 2), create_copy=True
|
||||
)
|
||||
builder.translate([seat, chair_back] + legs, shift_to_center)
|
||||
builder.mirror([seat, chair_back] + legs, mirror_axis)
|
||||
seat = builder.extrude(
|
||||
builder.profile(seat), thickness, position=V(0, 0, seat_level), extrusion_vector=V(0, 0, -1)
|
||||
)
|
||||
chair_back = builder.extrude(
|
||||
builder.profile(chair_back), height - seat_level, position=V(0, 0, height), extrusion_vector=V(0, 0, -1)
|
||||
)
|
||||
legs = [builder.extrude(leg, seat_level - thickness) for leg in legs]
|
||||
items_to_adjust.extend(legs)
|
||||
|
||||
items_3d = [seat, chair_back] + legs
|
||||
if height > seat_level:
|
||||
items_to_adjust.append(chair_back := builder.rectangle(size=V(width, thickness)))
|
||||
|
||||
builder.translate(items_to_adjust, shift_to_center)
|
||||
builder.mirror(items_to_adjust, mirror_axis)
|
||||
|
||||
items_3d.append(
|
||||
builder.extrude(
|
||||
builder.profile(seat), thickness, position=V(0, 0, seat_level), extrusion_vector=V(0, 0, -1)
|
||||
)
|
||||
)
|
||||
|
||||
if chair_back:
|
||||
items_3d.append(
|
||||
builder.extrude(
|
||||
builder.profile(chair_back),
|
||||
height - seat_level,
|
||||
position=V(0, 0, height),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
)
|
||||
)
|
||||
|
||||
items_3d.extend([builder.extrude(leg, seat_level - thickness) for leg in legs])
|
||||
if return_representations:
|
||||
representation_3d = builder.get_representation(
|
||||
context=self.representations["model_body"], items=items_3d
|
||||
@@ -1286,8 +1302,6 @@ class LibraryGenerator:
|
||||
shift_to_center = V(-width / 2, 0)
|
||||
mirror_axis = V(0, 1)
|
||||
|
||||
representation_3d, representation_2d = create_box_objects(width, depth, height, return_representations=True)
|
||||
|
||||
back_wall_depth = depth * 0.1
|
||||
back_wall_border_mask_height = height * 0.1
|
||||
back_wall_border_mask_depth = depth * 0.1
|
||||
@@ -1955,12 +1969,17 @@ class LibraryGenerator:
|
||||
representation_3d, representation_2d = create_box_objects(
|
||||
overall_width, overall_depth, overall_height, return_representations=True
|
||||
)
|
||||
self.create_explicit_type("IfcSpaceType", parking_lot_name, representation_3d, representation_2d)
|
||||
self.create_explicit_type(
|
||||
"IfcSpaceType", parking_lot_name, representation_3d, representation_2d, PredefinedType="PARKING"
|
||||
)
|
||||
|
||||
self.file.write(output_filename)
|
||||
|
||||
def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d):
|
||||
def create_explicit_type(self, ifc_class, name, representation_3d, representation_2d, **params):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
for param, value in params.items():
|
||||
setattr(element, param, value)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", self.file, product=element, representation=representation_3d
|
||||
)
|
||||
|
||||
@@ -152,30 +152,9 @@ class LibraryGenerator:
|
||||
ifcopenshell.api.run(
|
||||
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
|
||||
)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
|
||||
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
|
||||
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
|
||||
self.representations = {
|
||||
"body": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Model",
|
||||
context_identifier="Body",
|
||||
target_view="MODEL_VIEW",
|
||||
parent=model,
|
||||
),
|
||||
"annotation": ifcopenshell.api.run(
|
||||
"context.add_context",
|
||||
self.file,
|
||||
context_type="Plan",
|
||||
context_identifier="Annotation",
|
||||
target_view="PLAN_VIEW",
|
||||
parent=plan,
|
||||
),
|
||||
}
|
||||
|
||||
self.material = ifcopenshell.api.run("material.add_material", self.file, name="Unknown")
|
||||
|
||||
# parameters could be optional (example: welded i-beams don't have FilletRadius)
|
||||
@@ -245,29 +224,6 @@ class LibraryGenerator:
|
||||
|
||||
self.file.write(output_filename)
|
||||
|
||||
def create_layer_set_type(self, name, data):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name)
|
||||
element.Description = data["Description"]
|
||||
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
|
||||
layer_set = rel.RelatingMaterial
|
||||
for layer_data in data["Layers"]:
|
||||
layer = ifcopenshell.api.run(
|
||||
"material.add_layer", self.file, layer_set=layer_set, material=self.materials[layer_data[1]]["ifc"]
|
||||
)
|
||||
layer.Name = layer_data[0]
|
||||
layer.LayerThickness = layer_data[2]
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
return element
|
||||
|
||||
def create_layer_type(self, ifc_class, name, thickness):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
|
||||
layer_set = rel.RelatingMaterial
|
||||
layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"])
|
||||
layer.LayerThickness = thickness
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
return element
|
||||
|
||||
def create_profile_type(self, ifc_class, name, profile):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet")
|
||||
@@ -279,38 +235,6 @@ class LibraryGenerator:
|
||||
ifcopenshell.api.run("material.assign_profile", self.file, material_profile=material_profile, profile=profile)
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
|
||||
def create_type(self, ifc_class, name, representations):
|
||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
|
||||
for rep_name, obj_name in representations.items():
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
representation = ifcopenshell.api.run(
|
||||
"geometry.add_representation",
|
||||
self.file,
|
||||
context=self.representations[rep_name],
|
||||
blender_object=obj,
|
||||
geometry=obj.data,
|
||||
total_items=max(1, len(obj.material_slots)),
|
||||
)
|
||||
styles = []
|
||||
for slot in obj.material_slots:
|
||||
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
|
||||
ifcopenshell.api.run(
|
||||
"style.add_surface_style",
|
||||
self.file,
|
||||
style=style,
|
||||
ifc_class="IfcSurfaceStyleRendering",
|
||||
attributes=tool.Style.get_surface_rendering_attributes(slot.material),
|
||||
)
|
||||
styles.append(style)
|
||||
if styles:
|
||||
ifcopenshell.api.run(
|
||||
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
|
||||
)
|
||||
ifcopenshell.api.run(
|
||||
"geometry.assign_representation", self.file, product=element, representation=representation
|
||||
)
|
||||
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = Path(__file__).parents[1] / "blenderbim/bim/data/libraries"
|
||||
|
||||
@@ -4,6 +4,7 @@ from mathutils import Vector
|
||||
|
||||
V = lambda *x: Vector([float(i) for i in x])
|
||||
|
||||
|
||||
# TODO: move examples to more suitable place
|
||||
def simple_uses():
|
||||
ifc_file = ifcopenshell.file()
|
||||
@@ -91,7 +92,7 @@ def mirror_placement_test():
|
||||
"root.create_entity", ifc_file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library"
|
||||
)
|
||||
ifcopenshell.api.run("project.assign_declaration", ifc_file, definition=library, relating_context=project)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
|
||||
model = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Model")
|
||||
plan = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Plan")
|
||||
@@ -165,7 +166,7 @@ def curve_between_two_points_test():
|
||||
"root.create_entity", ifc_file, ifc_class="IfcProjectLibrary", name=f"Non-structural assets library"
|
||||
)
|
||||
ifcopenshell.api.run("project.assign_declaration", ifc_file, definition=library, relating_context=project)
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
|
||||
model = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Model")
|
||||
plan = ifcopenshell.api.run("context.add_context", ifc_file, context_type="Plan")
|
||||
|
||||
@@ -87,10 +87,11 @@ class TestAddSheet:
|
||||
drawing.generate_sheet_identification().should_be_called().will_return("identification")
|
||||
drawing.ensure_unique_identification("identification").should_be_called().will_return("u_identification")
|
||||
ifc.get_schema().should_be_called().will_return("IFC4")
|
||||
drawing.get_default_sheet_path("u_identification", "UNTITLED").should_be_called().will_return("uri")
|
||||
ifc.run(
|
||||
"document.edit_information",
|
||||
information="sheet",
|
||||
attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "DOCUMENTATION"},
|
||||
attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "DOCUMENTATION", "Location": "uri"},
|
||||
).should_be_called()
|
||||
drawing.create_svg_sheet("sheet", "titleblock").should_be_called()
|
||||
drawing.import_sheets().should_be_called()
|
||||
@@ -141,18 +142,21 @@ class TestDisableEditingSchedules:
|
||||
class TestAddSchedule:
|
||||
def test_run(self, ifc, drawing):
|
||||
ifc.run("document.add_information").should_be_called().will_return("schedule")
|
||||
drawing.get_path_filename("uri").should_be_called().will_return("UNTITLED")
|
||||
ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
|
||||
ifc.get_schema().should_be_called().will_return("IFC4")
|
||||
ifc.run(
|
||||
"document.edit_information",
|
||||
information="schedule",
|
||||
attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": "uri"},
|
||||
attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"},
|
||||
).should_be_called()
|
||||
ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
|
||||
drawing.import_schedules().should_be_called()
|
||||
subject.add_schedule(ifc, drawing, uri="uri")
|
||||
|
||||
def test_using_a_document_id_in_ifc2x3(self, ifc, drawing):
|
||||
ifc.run("document.add_information").should_be_called().will_return("schedule")
|
||||
drawing.get_path_filename("uri").should_be_called().will_return("UNTITLED")
|
||||
ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
|
||||
ifc.get_schema().should_be_called().will_return("IFC2X3")
|
||||
ifc.run(
|
||||
@@ -174,7 +178,7 @@ class TestRemoveSchedule:
|
||||
|
||||
class TestOpenSchedule:
|
||||
def test_run(self, drawing):
|
||||
drawing.get_schedule_location("schedule").should_be_called().will_return("uri")
|
||||
drawing.get_document_uri("schedule").should_be_called().will_return("uri")
|
||||
drawing.open_spreadsheet("uri").should_be_called()
|
||||
subject.open_schedule(drawing, schedule="schedule")
|
||||
|
||||
@@ -225,6 +229,10 @@ class TestAddDrawing:
|
||||
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
|
||||
collector.assign("obj").should_be_called()
|
||||
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
|
||||
drawing.get_default_drawing_resource_path("Stylesheet").should_be_called().will_return("stylesheet.css")
|
||||
drawing.get_default_drawing_resource_path("Markers").should_be_called().will_return("markers.svg")
|
||||
drawing.get_default_drawing_resource_path("Symbols").should_be_called().will_return("symbols.svg")
|
||||
drawing.get_default_drawing_resource_path("Patterns").should_be_called().will_return("patterns.svg")
|
||||
ifc.run(
|
||||
"pset.edit_pset",
|
||||
pset="pset",
|
||||
@@ -236,8 +244,19 @@ class TestAddDrawing:
|
||||
"HasLinework": True,
|
||||
"HasAnnotation": True,
|
||||
"GlobalReferencing": True,
|
||||
"Stylesheet": "stylesheet.css",
|
||||
"Markers": "markers.svg",
|
||||
"Symbols": "symbols.svg",
|
||||
"Patterns": "patterns.svg",
|
||||
},
|
||||
).should_be_called()
|
||||
drawing.get_default_drawing_path("name").should_be_called().will_return("uri")
|
||||
ifc.run("document.add_information").should_be_called().will_return("information")
|
||||
ifc.run("document.add_reference", information="information").should_be_called().will_return("reference")
|
||||
ifc.get_schema().should_be_called().will_return("IFC4")
|
||||
ifc.run("document.edit_information", information="information", attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"}).should_be_called()
|
||||
ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
|
||||
ifc.run("document.assign_document", product="element", document="reference").should_be_called()
|
||||
drawing.import_drawings().should_be_called()
|
||||
subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint")
|
||||
|
||||
@@ -277,6 +296,8 @@ class TestRemoveDrawing:
|
||||
ifc.get_object("reference").should_be_called().will_return("reference_obj")
|
||||
drawing.delete_object("reference_obj").should_be_called()
|
||||
ifc.run("root.remove_product", product="reference").should_be_called()
|
||||
drawing.get_drawing_document("drawing").should_be_called().will_return("information")
|
||||
ifc.run("document.remove_information", information="information").should_be_called()
|
||||
ifc.run("root.remove_product", product="drawing").should_be_called()
|
||||
drawing.import_drawings().should_be_called()
|
||||
subject.remove_drawing(ifc, drawing, drawing="drawing")
|
||||
@@ -321,6 +342,7 @@ class TestAddAnnotation:
|
||||
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
|
||||
ifc.run("group.assign_group", group="group", products=["element"]).should_be_called()
|
||||
collector.assign("obj").should_be_called()
|
||||
drawing.setup_annotation_object("obj", "object_type").should_be_called()
|
||||
drawing.enable_editing("obj").should_be_called()
|
||||
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
|
||||
|
||||
|
||||
@@ -61,9 +61,15 @@ class TestCreateCamera(NewFile):
|
||||
class TestCreateSvgSheet(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
|
||||
subject.create_svg_sheet(document, "A1")
|
||||
assert os.path.isfile(os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg"))
|
||||
document = ifc.createIfcDocumentInformation(
|
||||
Identification="X",
|
||||
Name="FOOBAR",
|
||||
Scope="DOCUMENTATION",
|
||||
Location=os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", "X - FOOBAR.svg"),
|
||||
)
|
||||
uri = subject.create_svg_sheet(document, "A1")
|
||||
assert uri.endswith(".svg")
|
||||
assert os.path.isfile(uri)
|
||||
|
||||
|
||||
class TestDeleteCollection(NewFile):
|
||||
@@ -204,11 +210,13 @@ class TestEnsureUniqueIdentification(NewFile):
|
||||
class TestExportTextLiteralAttributes(NewFile):
|
||||
def test_run(self):
|
||||
TestImportTextAttributes().test_run()
|
||||
assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [{
|
||||
"Literal": "Literal",
|
||||
"Path": "RIGHT",
|
||||
"BoxAlignment": "bottom-left",
|
||||
}]
|
||||
assert subject.export_text_literal_attributes(bpy.data.objects.get("Object")) == [
|
||||
{
|
||||
"Literal": "Literal",
|
||||
"Path": "RIGHT",
|
||||
"BoxAlignment": "bottom-left",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class TestGetAnnotationContext(NewFile):
|
||||
@@ -236,23 +244,29 @@ class TestGetBodyContext(NewFile):
|
||||
|
||||
|
||||
class TestGetDocumentUri(NewFile):
|
||||
def test_get_sheet_uri(self):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
|
||||
result = subject.get_document_uri(document)
|
||||
assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")
|
||||
document = ifc.createIfcDocumentInformation(
|
||||
Identification="X", Name="FOOBAR", Scope="SHEET", Location="Location"
|
||||
)
|
||||
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
|
||||
|
||||
def test_get_schedule_uri(self):
|
||||
def test_get_indirect_locations(self):
|
||||
ifc = ifcopenshell.file()
|
||||
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
|
||||
result = subject.get_document_uri(document)
|
||||
assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", "X - FOOBAR.svg")
|
||||
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET")
|
||||
reference = ifc.createIfcDocumentReference(Location="Location", ReferencedDocument=document)
|
||||
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
|
||||
assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
|
||||
|
||||
def test_run_ifc2x3(self):
|
||||
ifc = ifcopenshell.file(schema="IFC2X3")
|
||||
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION")
|
||||
result = subject.get_document_uri(document)
|
||||
assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")
|
||||
tool.Ifc.set(ifc)
|
||||
reference = ifc.createIfcDocumentReference(Location="Location")
|
||||
document = ifc.createIfcDocumentInformation(
|
||||
DocumentId="X", Name="FOOBAR", Scope="SHEET", DocumentReferences=[reference]
|
||||
)
|
||||
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
|
||||
assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
|
||||
|
||||
|
||||
class TestGetDrawingCollection(NewFile):
|
||||
@@ -311,23 +325,6 @@ class TestGetName(NewFile):
|
||||
assert subject.get_name(ifc.createIfcWall(Name="Foobar")) == "Foobar"
|
||||
|
||||
|
||||
class TestGetScheduleLocation(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
|
||||
schedule = ifcopenshell.api.run("document.add_information", ifc)
|
||||
schedule.Location = "uri"
|
||||
reference = ifcopenshell.api.run("document.add_reference", ifc, information=schedule)
|
||||
subject.get_schedule_location(schedule) == "uri"
|
||||
|
||||
def test_run_ifc2x3(self):
|
||||
ifc = ifcopenshell.file(schema="IFC2X3")
|
||||
tool.Ifc.set(ifc)
|
||||
reference = ifc.createIfcDocumentReference(Location="uri")
|
||||
schedule = ifc.createIfcDocumentInformation(DocumentReferences=[reference])
|
||||
subject.get_schedule_location(schedule) == "uri"
|
||||
|
||||
|
||||
class TestGenerateDrawingMatrix(NewFile):
|
||||
def test_returning_the_origin_as_a_fallback(self):
|
||||
assert subject.generate_drawing_matrix("PLAN_VIEW", None) == mathutils.Matrix()
|
||||
|
||||
@@ -138,7 +138,7 @@ class Csv2Ifc:
|
||||
if unit:
|
||||
return unit
|
||||
else:
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type=unit_type, name=ifcopenshell.util.unit.si_type_names.get(unit_type, None))
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type=unit_type)
|
||||
# unit = self.file.createIfcContextDependentUnit(
|
||||
# self.file.createIfcDimensionalExponents(0, 0, 0, 0, 0, 0, 0), "USERDEFINED", symbol
|
||||
# )
|
||||
|
||||
@@ -320,7 +320,7 @@ def import_ifc(filename, use_names, process_relations, blender_booleans):
|
||||
|
||||
class ImportIFC(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "import_scene.ifc"
|
||||
bl_label = "Import .ifc file"
|
||||
bl_label = "Import .ifc File"
|
||||
|
||||
filename_ext = ".ifc"
|
||||
filter_glob: StringProperty(default="*.ifc", options={"HIDDEN"})
|
||||
|
||||
@@ -20,6 +20,7 @@ import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
from ifcopenshell.api.geometry.add_window_representation import create_ifc_window
|
||||
from mathutils import Vector
|
||||
from math import cos, radians
|
||||
|
||||
import collections
|
||||
|
||||
@@ -31,6 +32,9 @@ SUPPORTED_DOOR_TYPES = (
|
||||
"DOUBLE_SWING_LEFT",
|
||||
"DOUBLE_DOOR_SINGLE_SWING",
|
||||
"DOUBLE_DOOR_DOUBLE_SWING",
|
||||
"SLIDING_TO_LEFT",
|
||||
"SLIDING_TO_RIGHT",
|
||||
"DOUBLE_DOOR_SLIDING",
|
||||
)
|
||||
|
||||
|
||||
@@ -57,8 +61,15 @@ def create_ifc_door_lining(
|
||||
V(th_side, 0.0, 0.0),
|
||||
]
|
||||
|
||||
points = [p.xz for p in points]
|
||||
door_lining = builder.polyline(points, closed=True)
|
||||
door_lining = builder.extrude(door_lining, size.y, extrusion_vector=V(0, 1, 0))
|
||||
door_lining = builder.extrude(
|
||||
door_lining,
|
||||
size.y,
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
)
|
||||
builder.translate(door_lining, position)
|
||||
|
||||
return door_lining
|
||||
@@ -146,6 +157,7 @@ class Usecase:
|
||||
door_type = self.settings["operation_type"]
|
||||
double_swing_door = "DOUBLE_SWING" in door_type
|
||||
double_door = "DOUBLE_DOOR" in door_type
|
||||
sliding_door = "SLIDING" in door_type
|
||||
|
||||
if door_type not in SUPPORTED_DOOR_TYPES:
|
||||
raise NotImplementedError(f'Door type "{door_type}" is not currently supported.')
|
||||
@@ -162,8 +174,11 @@ class Usecase:
|
||||
lining_depth = lining_props["LiningDepth"]
|
||||
lining_thickness_default = lining_props["LiningThickness"]
|
||||
lining_offset = lining_props["LiningOffset"]
|
||||
lining_to_panel_offset_x = lining_props["LiningToPanelOffsetX"]
|
||||
lining_to_panel_offset_y_full = lining_props["LiningToPanelOffsetY"]
|
||||
lining_to_panel_offset_x = (
|
||||
lining_props["LiningToPanelOffsetX"] if not sliding_door else lining_thickness_default
|
||||
)
|
||||
panel_depth = panel_props["PanelDepth"]
|
||||
lining_to_panel_offset_y_full = lining_props["LiningToPanelOffsetY"] if not sliding_door else -panel_depth
|
||||
|
||||
transom_thickness = lining_props["TransomThickness"] / 2
|
||||
transfom_offset = lining_props["TransomOffset"]
|
||||
@@ -172,10 +187,10 @@ class Usecase:
|
||||
window_lining_height = overall_height - transfom_offset - transom_thickness
|
||||
|
||||
side_lining_thickness = lining_thickness_default
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0)
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
|
||||
top_lining_thickness = transom_thickness or lining_thickness_default
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0)
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
door_opening_width = overall_width - lining_to_panel_offset_x * 2
|
||||
if double_swing_door:
|
||||
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
|
||||
@@ -190,7 +205,6 @@ class Usecase:
|
||||
casing_depth = lining_props["CasingDepth"]
|
||||
|
||||
# panel params
|
||||
panel_depth = panel_props["PanelDepth"]
|
||||
panel_width = door_opening_width * panel_props["PanelWidth"]
|
||||
frame_depth = panel_props["FrameDepth"]
|
||||
frame_thickness = panel_props["FrameThickness"]
|
||||
@@ -201,6 +215,7 @@ class Usecase:
|
||||
handle_size = self.convert_si_to_unit(V(120, 40, 20) * 0.001)
|
||||
handle_offset = self.convert_si_to_unit(V(60, 0, 1000) * 0.001) # to the handle center
|
||||
handle_center_offset = V(handle_size.y / 2, 0, handle_size.z) / 2
|
||||
slider_arrow_symbol_size = self.convert_si_to_unit(30 * 0.001)
|
||||
|
||||
if transfom_offset:
|
||||
panel_height = transfom_offset + transom_thickness - lining_to_panel_offset_x - threshold_thickness
|
||||
@@ -221,8 +236,44 @@ class Usecase:
|
||||
# create 2d representation
|
||||
if self.settings["context"].TargetView == "PLAN_VIEW":
|
||||
items_2d = []
|
||||
door_items = []
|
||||
panel_size = V(panel_width, panel_depth)
|
||||
if not sliding_door:
|
||||
panel_position = V(lining_to_panel_offset_x, lining_depth)
|
||||
else:
|
||||
panel_position = V(lining_to_panel_offset_x, -panel_size.y)
|
||||
|
||||
if self.settings["context"].ContextIdentifier == "Annotation":
|
||||
# only sliding door has annotation representation
|
||||
if not sliding_door:
|
||||
return None
|
||||
|
||||
# arrow symbol
|
||||
arrow_symbol = []
|
||||
arrow_offset = slider_arrow_symbol_size / cos(radians(15))
|
||||
arrow_symbol.append(
|
||||
builder.polyline(
|
||||
points=(V(0.35 * panel_size.x, 0), V(0.65 * panel_size.x, 0)),
|
||||
)
|
||||
)
|
||||
arrow_symbol.append(
|
||||
builder.polyline(
|
||||
points=(
|
||||
V(slider_arrow_symbol_size, arrow_offset),
|
||||
V(0, 0),
|
||||
V(slider_arrow_symbol_size, -arrow_offset),
|
||||
),
|
||||
position_offset=V(0.35 * panel_size.x, 0),
|
||||
)
|
||||
)
|
||||
|
||||
builder.translate(arrow_symbol, panel_position + V(0, -arrow_offset * 1.5))
|
||||
|
||||
items_2d.extend(arrow_symbol)
|
||||
|
||||
representation_2d = builder.get_representation(self.settings["context"], items_2d, "Curve2D")
|
||||
return representation_2d
|
||||
|
||||
door_items = []
|
||||
# create lining
|
||||
if l_shape_check([side_lining_thickness]):
|
||||
lining_points = [
|
||||
@@ -243,7 +294,10 @@ class Usecase:
|
||||
)
|
||||
|
||||
# TODO: make second swing lines dashed
|
||||
def create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type):
|
||||
def create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type, sliding=False):
|
||||
if sliding:
|
||||
return create_ifc_door_sliding_panel_2d(panel_size, panel_position, door_swing_type)
|
||||
|
||||
door_items = []
|
||||
panel_size = panel_size.yx
|
||||
# create semi-semi-circle
|
||||
@@ -274,13 +328,18 @@ class Usecase:
|
||||
builder.mirror(door_items, mirror_axes=V(1, 0), mirror_point=mirror_point)
|
||||
return door_items
|
||||
|
||||
door_items = []
|
||||
panel_size = V(panel_width, panel_depth)
|
||||
panel_position = V(lining_to_panel_offset_x, lining_depth)
|
||||
def create_ifc_door_sliding_panel_2d(panel_size, panel_position, door_swing_type):
|
||||
door = builder.rectangle(panel_size, position=panel_position - V(panel_size.x * 0.5, 0))
|
||||
|
||||
if door_swing_type == "RIGHT":
|
||||
mirror_point = panel_position + V(panel_size.x / 2, 0)
|
||||
builder.mirror(door, mirror_axes=V(1, 0), mirror_point=mirror_point)
|
||||
return [door]
|
||||
|
||||
door_items = []
|
||||
if double_door:
|
||||
panel_size.x = panel_size.x / 2
|
||||
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, "LEFT"))
|
||||
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, "LEFT", sliding_door))
|
||||
|
||||
mirror_point = panel_position + V(door_opening_width / 2, 0)
|
||||
door_items.extend(
|
||||
@@ -288,7 +347,7 @@ class Usecase:
|
||||
)
|
||||
else:
|
||||
door_swing_type = "LEFT" if door_type.endswith("LEFT") else "RIGHT"
|
||||
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type))
|
||||
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type, sliding_door))
|
||||
items_2d.extend(door_items)
|
||||
|
||||
builder.translate(items_2d, V(0, lining_offset))
|
||||
@@ -383,7 +442,7 @@ class Usecase:
|
||||
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y_full, threshold_thickness)
|
||||
|
||||
if double_door:
|
||||
# TODO: keep a little space between doors for readibility?
|
||||
# keeping a little space between doors for readibility
|
||||
double_door_offset = self.convert_si_to_unit(0.001)
|
||||
panel_size.x = panel_size.x / 2 - double_door_offset
|
||||
door_items.extend(create_ifc_door_panel(panel_size, panel_position, "LEFT"))
|
||||
|
||||
@@ -30,15 +30,15 @@ import collections
|
||||
# - order of rows is from top of the window to bottom
|
||||
|
||||
DEFAULT_PANEL_SCHEMAS = {
|
||||
"SINGLE_PANEL": [[0]],
|
||||
"DOUBLE_PANEL_HORIZONTAL": [[0], [1]],
|
||||
"DOUBLE_PANEL_VERTICAL": [[0, 1]],
|
||||
"TRIPLE_PANEL_BOTTOM": [[0, 1], [2, 2]],
|
||||
"TRIPLE_PANEL_TOP": [[0, 0], [1, 2]],
|
||||
"TRIPLE_PANEL_LEFT": [[0, 1], [0, 2]],
|
||||
"TRIPLE_PANEL_RIGHT": [[0, 1], [2, 1]],
|
||||
"TRIPLE_PANEL_HORIZONTAL": [[0], [1], [2]],
|
||||
"TRIPLE_PANEL_VERTICAL": [[0, 1, 2]],
|
||||
"SINGLE_PANEL": [[0]],
|
||||
"DOUBLE_PANEL_HORIZONTAL": [[0], [1]],
|
||||
"DOUBLE_PANEL_VERTICAL": [[0, 1]],
|
||||
"TRIPLE_PANEL_BOTTOM": [[0, 1], [2, 2]],
|
||||
"TRIPLE_PANEL_TOP": [[0, 0], [1, 2]],
|
||||
"TRIPLE_PANEL_LEFT": [[0, 1], [0, 2]],
|
||||
"TRIPLE_PANEL_RIGHT": [[0, 1], [2, 1]],
|
||||
"TRIPLE_PANEL_HORIZONTAL": [[0], [1], [2]],
|
||||
"TRIPLE_PANEL_VERTICAL": [[0, 1, 2]],
|
||||
}
|
||||
|
||||
|
||||
@@ -54,13 +54,20 @@ def create_ifc_window_frame_simple(builder, size: Vector, thickness: list, posit
|
||||
|
||||
th_left, th_up, th_right, th_bottom = thickness
|
||||
|
||||
panel_rect = builder.rectangle(size=size * V(1, 0, 1))
|
||||
panel_rect = builder.rectangle(size=size.xz)
|
||||
|
||||
inner_rect_size = size - V(th_left + th_right, 0, th_bottom + th_up)
|
||||
inner_rect = builder.rectangle(size=inner_rect_size * V(1, 0, 1), position=V(th_left, 0, th_bottom))
|
||||
inner_rect = builder.rectangle(size=inner_rect_size.xz, position=V(th_left, th_bottom))
|
||||
|
||||
panel_profile = builder.profile(panel_rect, inner_curves=inner_rect)
|
||||
panel_extruded = builder.extrude(panel_profile, size.y, extrusion_vector=V(0, 1, 0), position=position)
|
||||
panel_extruded = builder.extrude(
|
||||
panel_profile,
|
||||
size.y,
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
position=position,
|
||||
)
|
||||
return panel_extruded
|
||||
|
||||
|
||||
@@ -133,7 +140,9 @@ def create_ifc_window(
|
||||
glass = builder.extrude(
|
||||
glass_rect,
|
||||
glass_thickness,
|
||||
extrusion_vector=V(0, 1, 0),
|
||||
position_x_axis=V(1, 0, 0),
|
||||
position_z_axis=V(0, -1, 0),
|
||||
extrusion_vector=V(0, 0, -1),
|
||||
position=glass_position,
|
||||
)
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class Usecase:
|
||||
return element
|
||||
|
||||
def handle_2x3_defaults(self, element):
|
||||
if element.is_a("IfcElement") or element.is_a("IfcElementType"):
|
||||
if element.is_a("IfcElementType"):
|
||||
if hasattr(element, "PredefinedType") and not element.PredefinedType:
|
||||
element.PredefinedType = "NOTDEFINED"
|
||||
|
||||
@@ -117,7 +117,7 @@ class Usecase:
|
||||
element.Sizeable = False
|
||||
|
||||
def handle_4_defaults(self, element):
|
||||
if element.is_a("IfcElement") or element.is_a("IfcElementType"):
|
||||
if element.is_a("IfcElementType"):
|
||||
if hasattr(element, "PredefinedType") and not element.PredefinedType:
|
||||
element.PredefinedType = "NOTDEFINED"
|
||||
|
||||
|
||||
@@ -293,19 +293,25 @@ class Selector:
|
||||
results = []
|
||||
for prop_name, prop_value in value.items():
|
||||
if re.match(key, prop_name):
|
||||
results.append(prop_value)
|
||||
if isinstance(prop_value, (list, tuple)):
|
||||
results.extend(prop_value)
|
||||
else:
|
||||
results.append(prop_value)
|
||||
value = results
|
||||
else:
|
||||
value = value.get(key, None)
|
||||
elif isinstance(value, (list, tuple)): # If we use regex
|
||||
results = []
|
||||
for v in value:
|
||||
subvalue = cls.get_element_value(v, [key], is_regex=is_regex)
|
||||
if isinstance(subvalue, list):
|
||||
results.extend(subvalue)
|
||||
else:
|
||||
results.append(subvalue)
|
||||
value = results
|
||||
if key.isnumeric():
|
||||
value = value[int(key)]
|
||||
else:
|
||||
results = []
|
||||
for v in value:
|
||||
subvalue = cls.get_element_value(v, [key], is_regex=is_regex)
|
||||
if isinstance(subvalue, list):
|
||||
results.extend(subvalue)
|
||||
else:
|
||||
results.append(subvalue)
|
||||
value = results
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -326,26 +326,45 @@ def get_task_outputs(task, is_deep=False):
|
||||
if not is_deep:
|
||||
return get_direct_task_outputs(task)
|
||||
else:
|
||||
nested_tasks = get_all_nested_tasks(task)
|
||||
return [
|
||||
output
|
||||
for nested_task in nested_tasks
|
||||
for nested_task in get_all_nested_tasks(task)
|
||||
for output in get_direct_task_outputs(nested_task)
|
||||
]
|
||||
|
||||
def get_task_inputs(task, is_deep=False):
|
||||
if not is_deep:
|
||||
return [object for rel in task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcProduct")]
|
||||
else:
|
||||
return [
|
||||
output
|
||||
for nested_task in get_all_nested_tasks(task)
|
||||
for output in [object for rel in nested_task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcProduct")]
|
||||
]
|
||||
|
||||
def get_task_inputs(task):
|
||||
inputs = []
|
||||
for rel in task.OperatesOn:
|
||||
for object in rel.RelatedObjects:
|
||||
if object.is_a("IfcProduct"):
|
||||
inputs.append(object)
|
||||
return inputs
|
||||
|
||||
def get_task_resources(task, is_deep=False):
|
||||
if not is_deep:
|
||||
return [object for rel in task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcResource")]
|
||||
else:
|
||||
return [
|
||||
output
|
||||
for nested_task in get_all_nested_tasks(task)
|
||||
for output in [object for rel in nested_task.OperatesOn if rel.is_a("IfcRelAssignsToProcess") for object in rel.RelatedObjects if object.is_a("IfcResource")]
|
||||
]
|
||||
|
||||
def has_task_outputs(task):
|
||||
return len(get_task_outputs(task)) > 0
|
||||
|
||||
|
||||
def has_task_inputs(task):
|
||||
return len(get_task_inputs(task)) > 0
|
||||
|
||||
def get_tasks_for_product(product):
|
||||
inputs = []
|
||||
outputs = []
|
||||
for assignment in product.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToProcess") and assignment.RelatingProcess.is_a("IfcTask"):
|
||||
inputs.append(assignment.RelatingProcess)
|
||||
for reference in product.ReferencedBy:
|
||||
if reference.is_a("IfcRelAssignsToProduct") and reference.RelatedObjects[0].is_a("IfcTask"):
|
||||
outputs.extend([obj for obj in reference.RelatedObjects if obj.is_a("IfcTask")])
|
||||
return inputs, outputs
|
||||
@@ -55,7 +55,6 @@ class ShapeBuilder:
|
||||
return ifc_curve
|
||||
|
||||
def get_rectangle_coords(self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Vector = None):
|
||||
|
||||
dimensions = len(size)
|
||||
|
||||
if not position:
|
||||
@@ -184,9 +183,30 @@ class ShapeBuilder:
|
||||
# because of that you can't create bool edges of outer_curve this way
|
||||
|
||||
# < returns IfcArbitraryClosedProfileDef or IfcArbitraryProfileDefWithVoids
|
||||
|
||||
if outer_curve.Dim != 2:
|
||||
# TODO: replace with exception
|
||||
print(
|
||||
f"WARNING. Outer curve for IfcArbitraryClosedProfileDef/IfcIfcArbitraryProfileDefWithVoid should be 2D to be valid, currently it has {outer_curve.Dim} dimensions.\n"
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
|
||||
if inner_curves:
|
||||
if not isinstance(inner_curves, collections.abc.Iterable):
|
||||
inner_curves = [inner_curves]
|
||||
# TODO: replace with exception
|
||||
if any(curve.Dim != 2 for curve in inner_curves):
|
||||
print(
|
||||
"WARNING. InnerCurve for IfcIfcArbitraryProfileDefWithVoid sould be 2D to be valid, "
|
||||
"currently on one of the inner curves is using different amount of dimensions.\n"
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
|
||||
profile = self.file.createIfcArbitraryProfileDefWithVoids(
|
||||
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve, InnerCurves=inner_curves
|
||||
@@ -241,7 +261,6 @@ class ShapeBuilder:
|
||||
def rotate_2d_point(
|
||||
self, point_2d: Vector, angle=90, pivot_point: Vector = Vector((0.0, 0.0)).freeze(), counter_clockwise=False
|
||||
):
|
||||
|
||||
# > angle - in degrees
|
||||
# < rotated Vector
|
||||
|
||||
@@ -407,9 +426,9 @@ class ShapeBuilder:
|
||||
|
||||
if hasattr(c.SweptArea, "InnerCurves"):
|
||||
for inner_curve in c.SweptArea.InnerCurves:
|
||||
self.translate(inner_curve, base_position)
|
||||
self.translate(inner_curve, base_position.to_2d())
|
||||
self.mirror(inner_curve, mirror_axes, mirror_point, placement_matrix=placement_matrix)
|
||||
self.translate(inner_curve, -new_position)
|
||||
self.translate(inner_curve, -new_position.to_2d())
|
||||
|
||||
# extrusion converted to world space
|
||||
base_extruded_direction = Vector(c.ExtrudedDirection.DirectionRatios)
|
||||
@@ -471,6 +490,12 @@ class ShapeBuilder:
|
||||
# > position_y_axis - optional, could be used to calculate Z-axis based on Y-axis
|
||||
# < IfcExtrudedAreaSolid
|
||||
|
||||
if not magnitude:
|
||||
raise Exception(
|
||||
"Extrusion magnitude must be greater than 0 to be valid.\n"
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation"
|
||||
)
|
||||
|
||||
if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"):
|
||||
profile_or_curve = self.profile(profile_or_curve)
|
||||
|
||||
@@ -488,15 +513,24 @@ class ShapeBuilder:
|
||||
)
|
||||
return extruded_area
|
||||
|
||||
def get_representation(self, context, items):
|
||||
def get_representation(self, context, items, representation_type=None):
|
||||
# > items - could be a list or single curve/IfcExtrudedAreaSolid
|
||||
# < IfcShapeRepresentation
|
||||
if not isinstance(items, collections.abc.Iterable):
|
||||
items = [items]
|
||||
|
||||
if not representation_type:
|
||||
if items[0].is_a("IfcExtrudedAreaSolid"):
|
||||
representation_type = "SweptSolid"
|
||||
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
|
||||
representation_type = "Curve3D"
|
||||
else:
|
||||
representation_type = "Curve2D"
|
||||
|
||||
representation = self.file.createIfcShapeRepresentation(
|
||||
ContextOfItems=context,
|
||||
RepresentationIdentifier=context.ContextIdentifier,
|
||||
RepresentationType="SweptSolid" if items[0].is_a("IfcExtrudedAreaSolid") else "Curve2D",
|
||||
RepresentationType=representation_type,
|
||||
Items=items,
|
||||
)
|
||||
return representation
|
||||
|
||||
@@ -83,18 +83,34 @@ def get_connected_port(port):
|
||||
|
||||
|
||||
def get_connected_to(element):
|
||||
# Note: this code is for IFC2X3. IFC4 has a different approach.
|
||||
results = []
|
||||
for rel in element.HasPorts:
|
||||
for rel2 in rel.RelatingPort.ConnectedTo:
|
||||
results.extend([r.RelatedElement for r in rel2.RelatedPort.ContainedIn if r.RelatedElement != element])
|
||||
return results
|
||||
for port in ifcopenshell.util.system.get_ports(element):
|
||||
for relConnectsPort in port.ConnectedTo:
|
||||
for disPort in [relConnectsPort.RelatedPort,relConnectsPort.RelatingPort]:
|
||||
if hasattr(disPort,"Nests"):
|
||||
for relNest in disPort.Nests:
|
||||
if relNest.RelatingObject != element:
|
||||
results.append(relNest.RelatingObject)
|
||||
# IFC2X3 only, deprecated in IFC4
|
||||
elif hasattr(disPort,"ContainedIn"):
|
||||
for relConPortToElement in disPort.ContainedIn:
|
||||
if relConPortToElement.RelatedElement != element:
|
||||
results.append(relConPortToElement.RelatedElement)
|
||||
return(results)
|
||||
|
||||
|
||||
def get_connected_from(element):
|
||||
# Note: this code is for IFC2X3. IFC4 has a different approach.
|
||||
results = []
|
||||
for rel in element.HasPorts:
|
||||
for rel2 in rel.RelatingPort.ConnectedFrom:
|
||||
results.extend([r.RelatedElement for r in rel2.RelatingPort.ContainedIn if r.RelatedElement != element])
|
||||
return results
|
||||
for port in ifcopenshell.util.system.get_ports(element):
|
||||
for relConnectsPort in port.ConnectedFrom:
|
||||
for disPort in [relConnectsPort.RelatedPort,relConnectsPort.RelatingPort]:
|
||||
if hasattr(disPort,"Nests"):
|
||||
for relNest in disPort.Nests:
|
||||
if relNest.RelatingObject != element:
|
||||
results.append(relNest.RelatingObject)
|
||||
# IFC2X3 only, deprecated in IFC4
|
||||
elif hasattr(disPort,"ContainedIn"):
|
||||
for relConPortToElement in disPort.ContainedIn:
|
||||
if relConPortToElement.RelatedElement != element:
|
||||
results.append(relConPortToElement.RelatedElement)
|
||||
return(results)
|
||||