Basic reimplementation of the 2D representation support without PythonOCC. See #1153.

This commit is contained in:
Dion Moult
2021-05-17 21:52:23 +10:00
parent d3bb895c9d
commit 2d42846fa3
8 changed files with 221 additions and 23 deletions
@@ -1,6 +1,6 @@
* { stroke-linecap: round; } * { stroke-linecap: round; stroke-linejoin: round; }
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; } *[id] { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; }
.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.hidden { stroke-dasharray: 3, 2; } .hidden { stroke-dasharray: 3, 2; }
.solid {} .solid {}
-9
View File
@@ -88,15 +88,6 @@ def parse_diagram_scale(camera):
return float(numerator) / float(denominator) return float(numerator) / float(denominator)
def get_project_collection(scene):
"""Get main project collection"""
colls = [c for c in scene.collection.children if c.name.startswith("IfcProject")]
if len(colls) != 1:
raise RuntimeError("project collection missing or not unique")
return colls[0]
def ortho_view_frame(camera, margin=0.015): def ortho_view_frame(camera, margin=0.015):
"""Calculates 2d bounding box of camera view area. """Calculates 2d bounding box of camera view area.
+4 -3
View File
@@ -18,7 +18,7 @@ import multiprocessing
import zipfile import zipfile
import tempfile import tempfile
import numpy as np import numpy as np
import blenderbim.bim.prop from blenderbim.bim.module.drawing.prop import getDiagramScales
from pathlib import Path from pathlib import Path
from itertools import cycle from itertools import cycle
from datetime import datetime from datetime import datetime
@@ -1218,7 +1218,8 @@ class IfcImporter:
if not view_collection: if not view_collection:
view_collection = bpy.data.collections.new("Views") view_collection = bpy.data.collections.new("Views")
bpy.context.scene.collection.children.link(view_collection) bpy.context.scene.collection.children.link(view_collection)
drawing_collection = bpy.data.collections.new(obj.name) group = [r for r in element.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup
drawing_collection = bpy.data.collections.new("IfcGroup/" + group.Name)
view_collection.children.link(drawing_collection) view_collection.children.link(drawing_collection)
drawing_collection.objects.link(obj) drawing_collection.objects.link(obj)
else: else:
@@ -1342,7 +1343,7 @@ class IfcImporter:
camera.BIMCameraProperties.target_view = pset["TargetView"] camera.BIMCameraProperties.target_view = pset["TargetView"]
if "Scale" in pset: if "Scale" in pset:
valid_scales = [ valid_scales = [
i[0] for i in blenderbim.bim.prop.getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1] i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]
] ]
if valid_scales: if valid_scales:
camera.BIMCameraProperties.diagram_scale = valid_scales[0] camera.BIMCameraProperties.diagram_scale = valid_scales[0]
@@ -1,5 +1,102 @@
import bpy import bpy
import math import math
import mathutils.geometry
from mathutils import Vector
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
class BoundingEdge:
def __init__(self, v0, v1):
self.vertex = (v0, v1)
self.vector = v1 - v0
class BoundingFace:
def __init__(self, v0, v1, v2):
self.vertex = (v0, v1, v2)
self.normal = mathutils.geometry.normal(v0, v1, v2)
class BoundingBox:
def __init__(self, ob, vertex=None):
self.vertex = vertex or [ob.matrix_world @ Vector(v) for v in ob.bound_box]
if self.vertex != None:
self.edge = [
BoundingEdge(self.vertex[0], self.vertex[1]),
BoundingEdge(self.vertex[1], self.vertex[2]),
BoundingEdge(self.vertex[2], self.vertex[3]),
BoundingEdge(self.vertex[3], self.vertex[0]),
BoundingEdge(self.vertex[4], self.vertex[5]),
BoundingEdge(self.vertex[5], self.vertex[6]),
BoundingEdge(self.vertex[6], self.vertex[7]),
BoundingEdge(self.vertex[7], self.vertex[4]),
BoundingEdge(self.vertex[0], self.vertex[4]),
BoundingEdge(self.vertex[1], self.vertex[5]),
BoundingEdge(self.vertex[2], self.vertex[6]),
BoundingEdge(self.vertex[3], self.vertex[7]),
]
self.face = [
BoundingFace(self.vertex[0], self.vertex[1], self.vertex[3]),
BoundingFace(self.vertex[0], self.vertex[4], self.vertex[1]),
BoundingFace(self.vertex[0], self.vertex[3], self.vertex[4]),
BoundingFace(self.vertex[6], self.vertex[5], self.vertex[7]),
BoundingFace(self.vertex[6], self.vertex[7], self.vertex[2]),
BoundingFace(self.vertex[6], self.vertex[2], self.vertex[5]),
]
def whichSide(self, vtxs, normal, faceVtx):
retVal = 0
positive = 0
negative = 0
for v in vtxs:
t = normal.dot(v - faceVtx)
if t > 0:
positive = positive + 1
elif t < 0:
negative = negative + 1
if positive != 0 and negative != 0:
return 0
if positive != 0:
retVal = 1
else:
retVal = -1
return retVal
# Taken from: http://www.geometrictools.com/Documentation/MethodOfSeparatingAxes.pdf
def intersect(self, bb):
retVal = False
if self.vertex != None and bb.vertex != None:
# check all the faces of this object for a seperation axis
for i, f in enumerate(self.face):
d = f.normal
if self.whichSide(bb.vertex, d, f.vertex[0]) > 0:
return False # all the vertexes are on the +ve side of the face
# now do it again for the other objects faces
for i, f in enumerate(bb.face):
d = f.normal
if self.whichSide(self.vertex, d, f.vertex[0]) > 0:
return False # all the vertexes are on the +ve side of the face
# do edge checks
for e1 in self.edge:
for e2 in bb.edge:
d = e1.vector.cross(e2.vector)
side0 = self.whichSide(self.vertex, d, e1.vertex[0])
if side0 == 0:
continue
side1 = self.whichSide(bb.vertex, d, e1.vertex[0])
if side1 == 0:
continue
if (side0 * side1) < 0:
return False
retVal = True
return retVal
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py # This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
@@ -151,3 +248,12 @@ def get_active_drawing(scene):
return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera
except (KeyError, IndexError): except (KeyError, IndexError):
raise RuntimeError("missing drawing collection") raise RuntimeError("missing drawing collection")
def get_project_collection(scene):
"""Get main project collection"""
colls = [c for c in scene.collection.children if c.name.startswith("IfcProject")]
if len(colls) != 1:
raise RuntimeError("project collection missing or not unique")
return colls[0]
@@ -1,6 +1,8 @@
import os import os
import re
import bpy import bpy
import json import json
import time
import bmesh import bmesh
import subprocess import subprocess
import webbrowser import webbrowser
@@ -8,9 +10,10 @@ import ifcopenshell.util.selector
import ifcopenshell.util.representation import ifcopenshell.util.representation
import blenderbim.bim.module.drawing.svgwriter as svgwriter import blenderbim.bim.module.drawing.svgwriter as svgwriter
import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.cut_ifc as cut_ifc # TODO: deprecate import blenderbim.bim.module.drawing.cut_ifc as cut_ifc # TODO: deprecate
import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.sheeter as sheeter
import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.helper as helper
from mathutils import Vector, Matrix, Euler, geometry from mathutils import Vector, Matrix, Euler, geometry
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.group.data import Data as GroupData from ifcopenshell.api.group.data import Data as GroupData
@@ -33,6 +36,7 @@ class AddDrawing(bpy.types.Operator):
bl_label = "Add Drawing" bl_label = "Add Drawing"
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file()
new = context.scene.DocProperties.drawings.add() new = context.scene.DocProperties.drawings.add()
new.name = "DRAWING {}".format(len(context.scene.DocProperties.drawings)) new.name = "DRAWING {}".format(len(context.scene.DocProperties.drawings))
if not bpy.data.collections.get("Views"): if not bpy.data.collections.get("Views"):
@@ -58,7 +62,9 @@ class AddDrawing(bpy.types.Operator):
bpy.ops.bim.activate_drawing_style() bpy.ops.bim.activate_drawing_style()
bpy.ops.bim.add_group() bpy.ops.bim.add_group()
bpy.ops.bim.assign_group(product=camera.name, group=sorted(GroupData.groups.keys())[-1]) group = self.file.by_id(sorted(GroupData.groups.keys())[-1])
ifcopenshell.api.run("group.edit_group", self.file, **{"group": group, "attributes": {"Name": new.name}})
bpy.ops.bim.assign_group(product=camera.name, group=group.id())
bpy.ops.bim.add_pset(obj=camera.name, obj_type="Object", pset_name="EPset_Drawing") bpy.ops.bim.add_pset(obj=camera.name, obj_type="Object", pset_name="EPset_Drawing")
pset_id = sorted(PsetData.products[camera.BIMObjectProperties.ifc_definition_id]["psets"])[-1] pset_id = sorted(PsetData.products[camera.BIMObjectProperties.ifc_definition_id]["psets"])[-1]
bpy.ops.bim.edit_pset( bpy.ops.bim.edit_pset(
@@ -81,22 +87,45 @@ class CreateDrawing(bpy.types.Operator):
or not self.camera.BIMObjectProperties.ifc_definition_id or not self.camera.BIMObjectProperties.ifc_definition_id
): ):
return return
self.file = IfcStore.get_file()
self.time = None
start = time.time()
self.profile_code("Start drawing generation process")
self.props = context.scene.DocProperties self.props = context.scene.DocProperties
self.drawing_name = IfcStore.get_file().by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name self.drawing_name = IfcStore.get_file().by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
base_svg = self.ifc_to_svg(context) base_svg = self.ifc_to_svg(context)
self.profile_code("Generate base layer")
annotation_svg = self.annotation_to_svg(context) annotation_svg = self.annotation_to_svg(context)
self.profile_code("Generate annotation layer")
svg_path = self.combine_svgs(context, base_svg, annotation_svg) svg_path = self.combine_svgs(context, base_svg, annotation_svg)
self.profile_code("Combine SVG layers")
open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.svg_command, svg_path) open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
print("Total Time: {:.2f}".format(time.time() - start))
return {"FINISHED"} return {"FINISHED"}
def profile_code(self, message):
if not self.time:
self.time = time.time()
print("{} :: {:.2f}".format(message, time.time() - self.time))
self.time = time.time()
def combine_svgs(self, context, base, annotation): def combine_svgs(self, context, base, annotation):
# Hacky :) # Hacky :)
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg") svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
with open(svg_path, "w") as outfile: with open(svg_path, "w") as outfile:
with open(base) as infile: with open(base) as infile:
should_skip = False
for line in infile: for line in infile:
if "</svg>" in line: if "</svg>" in line:
continue continue
elif "<defs>" in line:
should_skip = True
continue
elif "</style>" in line:
should_skip = False
continue
elif should_skip:
continue
outfile.write(line) outfile.write(line)
with open(annotation) as infile: with open(annotation) as infile:
for i, line in enumerate(infile): for i, line in enumerate(infile):
@@ -127,6 +156,8 @@ class CreateDrawing(bpy.types.Operator):
"entities", "entities",
"IfcSpace", "IfcSpace",
"IfcOpeningElement", "IfcOpeningElement",
"IfcDoor",
"IfcWindow",
] ]
) )
return svg_path return svg_path
@@ -205,6 +236,7 @@ class CreateDrawing(bpy.types.Operator):
svg_writer.annotations.setdefault("misc_objs", []).append(obj) svg_writer.annotations.setdefault("misc_objs", []).append(obj)
svg_writer.annotations["attributes"] = [a.name for a in drawing_style.attributes] svg_writer.annotations["attributes"] = [a.name for a in drawing_style.attributes]
svg_writer.annotations["annotation_objs"] = self.get_annotation(svg_writer)
svg_writer.write() svg_writer.write()
return svg_writer.output return svg_writer.output
@@ -212,6 +244,75 @@ class CreateDrawing(bpy.types.Operator):
def is_landscape(self): def is_landscape(self):
return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y
def get_annotation(self, svg_writer):
results = []
x = svg_writer.camera_width / 2
y = svg_writer.camera_height / 2
z = 0.01
camera_box = helper.BoundingBox(
self.camera,
[
self.camera.matrix_world @ Vector((-x, -y, -z)),
self.camera.matrix_world @ Vector((-x, -y, 0)),
self.camera.matrix_world @ Vector((-x, y, 0)),
self.camera.matrix_world @ Vector((-x, y, -z)),
self.camera.matrix_world @ Vector((x, -y, -z)),
self.camera.matrix_world @ Vector((x, -y, 0)),
self.camera.matrix_world @ Vector((x, y, 0)),
self.camera.matrix_world @ Vector((x, y, -z)),
],
)
# This should probably be also part of IfcConvert in the future, here is a Python prototype
settings_2d = ifcopenshell.geom.settings()
settings_2d.set(settings_2d.INCLUDE_CURVES, True)
for obj in bpy.data.objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
if obj == self.camera:
continue
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
representation = ifcopenshell.util.representation.get_representation(
element, "Plan", "Annotation", self.camera.data.BIMCameraProperties.target_view
)
if not representation:
continue
if not camera_box.intersect(helper.BoundingBox(obj)):
continue
shape = ifcopenshell.geom.create_shape(settings_2d, representation)
geometry = shape
e = geometry.edges
v = geometry.verts
results.append(
{
"raw": element,
"classes": self.get_classes(element, "annotation", svg_writer),
"edges": [[e[i], e[i + 1]] for i in range(0, len(e), 2)],
"vertices": [obj.matrix_world @ Vector((v[i], v[i + 1], v[i + 2])) for i in range(0, len(v), 3)],
}
)
return results
def get_classes(self, element, position, svg_writer):
classes = [position, element.is_a()]
material = ifcopenshell.util.element.get_material(element)
if material:
classes.append("material-{}".format(re.sub("[^0-9a-zA-Z]+", "", self.get_material_name(material))))
classes.append("globalid-{}".format(element.GlobalId))
for attribute in svg_writer.annotations["attributes"]:
result = self.selector.get_element_value(element, attribute)
if result:
classes.append(
"{}-{}".format(re.sub("[^0-9a-zA-Z]+", "", attribute), re.sub("[^0-9a-zA-Z]+", "", result))
)
return classes
def get_material_name(self, element):
if hasattr(element, "Name") and element.Name:
return element.Name
elif hasattr(element, "LayerSetName") and element.LayerSetName:
return element.LayerSetName
return "mat-" + str(element.id())
class AddAnnotation(bpy.types.Operator): class AddAnnotation(bpy.types.Operator):
bl_idname = "bim.add_annotation" bl_idname = "bim.add_annotation"
@@ -38,10 +38,10 @@ def getDiagramScales(self, context):
global diagram_scales_enum global diagram_scales_enum
if ( if (
len(diagram_scales_enum) < 1 len(diagram_scales_enum) < 1
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13) or (bpy.context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
or (context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31) or (bpy.context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31)
): ):
if context.scene.unit_settings.system == "IMPERIAL": if bpy.context.scene.unit_settings.system == "IMPERIAL":
diagram_scales_enum = [ diagram_scales_enum = [
("CUSTOM", "Custom", ""), ("CUSTOM", "Custom", ""),
("1'=1'-0\"|1/1", "1'=1'-0\"", ""), ("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
@@ -106,8 +106,7 @@ class BIM_PT_camera(Panel):
row.operator("bim.activate_drawing_style") row.operator("bim.activate_drawing_style")
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.cut_section", text="Create Drawing") row.operator("bim.create_drawing", text="Create Drawing")
row.operator("bim.create_drawing", text="Create Drawing 2.0")
op = row.operator("bim.open_view", icon="URL", text="") op = row.operator("bim.open_view", icon="URL", text="")
op.view = context.active_object.name.split("/")[1] op.view = context.active_object.name.split("/")[1]
@@ -24,7 +24,7 @@ class BIM_PT_groups(Panel):
row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER") row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER")
if self.props.is_editing: if self.props.is_editing:
row.operator("bim.add_group", text="", icon="ADD") row.operator("bim.add_group", text="", icon="ADD")
row.operator("bim.disable_group_editing_ui", text="", icon="CHECKMARK") row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
else: else:
row.operator("bim.load_groups", text="", icon="GREASEPENCIL") row.operator("bim.load_groups", text="", icon="GREASEPENCIL")