mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Basic reimplementation of the 2D representation support without PythonOCC. See #1153.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
* { stroke-linecap: round; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; }
|
||||
.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
* { stroke-linecap: round; stroke-linejoin: round; }
|
||||
*[id] { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; }
|
||||
.projection { fill: white; 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; }
|
||||
.solid {}
|
||||
|
||||
@@ -88,15 +88,6 @@ def parse_diagram_scale(camera):
|
||||
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):
|
||||
"""Calculates 2d bounding box of camera view area.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import multiprocessing
|
||||
import zipfile
|
||||
import tempfile
|
||||
import numpy as np
|
||||
import blenderbim.bim.prop
|
||||
from blenderbim.bim.module.drawing.prop import getDiagramScales
|
||||
from pathlib import Path
|
||||
from itertools import cycle
|
||||
from datetime import datetime
|
||||
@@ -1218,7 +1218,8 @@ class IfcImporter:
|
||||
if not view_collection:
|
||||
view_collection = bpy.data.collections.new("Views")
|
||||
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)
|
||||
drawing_collection.objects.link(obj)
|
||||
else:
|
||||
@@ -1342,7 +1343,7 @@ class IfcImporter:
|
||||
camera.BIMCameraProperties.target_view = pset["TargetView"]
|
||||
if "Scale" in pset:
|
||||
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:
|
||||
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
|
||||
|
||||
@@ -1,5 +1,102 @@
|
||||
import bpy
|
||||
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
|
||||
@@ -151,3 +248,12 @@ def get_active_drawing(scene):
|
||||
return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera
|
||||
except (KeyError, IndexError):
|
||||
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 re
|
||||
import bpy
|
||||
import json
|
||||
import time
|
||||
import bmesh
|
||||
import subprocess
|
||||
import webbrowser
|
||||
@@ -8,9 +10,10 @@ import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.bim.module.drawing.svgwriter as svgwriter
|
||||
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.scheduler as scheduler
|
||||
import blenderbim.bim.module.drawing.helper as helper
|
||||
from mathutils import Vector, Matrix, Euler, geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.group.data import Data as GroupData
|
||||
@@ -33,6 +36,7 @@ class AddDrawing(bpy.types.Operator):
|
||||
bl_label = "Add Drawing"
|
||||
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
new = context.scene.DocProperties.drawings.add()
|
||||
new.name = "DRAWING {}".format(len(context.scene.DocProperties.drawings))
|
||||
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.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")
|
||||
pset_id = sorted(PsetData.products[camera.BIMObjectProperties.ifc_definition_id]["psets"])[-1]
|
||||
bpy.ops.bim.edit_pset(
|
||||
@@ -81,22 +87,45 @@ class CreateDrawing(bpy.types.Operator):
|
||||
or not self.camera.BIMObjectProperties.ifc_definition_id
|
||||
):
|
||||
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.drawing_name = IfcStore.get_file().by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name
|
||||
base_svg = self.ifc_to_svg(context)
|
||||
self.profile_code("Generate base layer")
|
||||
annotation_svg = self.annotation_to_svg(context)
|
||||
self.profile_code("Generate annotation layer")
|
||||
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)
|
||||
print("Total Time: {:.2f}".format(time.time() - start))
|
||||
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):
|
||||
# Hacky :)
|
||||
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.drawing_name + ".svg")
|
||||
with open(svg_path, "w") as outfile:
|
||||
with open(base) as infile:
|
||||
should_skip = False
|
||||
for line in infile:
|
||||
if "</svg>" in line:
|
||||
continue
|
||||
elif "<defs>" in line:
|
||||
should_skip = True
|
||||
continue
|
||||
elif "</style>" in line:
|
||||
should_skip = False
|
||||
continue
|
||||
elif should_skip:
|
||||
continue
|
||||
outfile.write(line)
|
||||
with open(annotation) as infile:
|
||||
for i, line in enumerate(infile):
|
||||
@@ -127,6 +156,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
"entities",
|
||||
"IfcSpace",
|
||||
"IfcOpeningElement",
|
||||
"IfcDoor",
|
||||
"IfcWindow",
|
||||
]
|
||||
)
|
||||
return svg_path
|
||||
@@ -205,6 +236,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
svg_writer.annotations.setdefault("misc_objs", []).append(obj)
|
||||
|
||||
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()
|
||||
return svg_writer.output
|
||||
@@ -212,6 +244,75 @@ class CreateDrawing(bpy.types.Operator):
|
||||
def is_landscape(self):
|
||||
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):
|
||||
bl_idname = "bim.add_annotation"
|
||||
|
||||
@@ -38,10 +38,10 @@ def getDiagramScales(self, context):
|
||||
global diagram_scales_enum
|
||||
if (
|
||||
len(diagram_scales_enum) < 1
|
||||
or (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 == "IMPERIAL" and len(diagram_scales_enum) == 13)
|
||||
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 = [
|
||||
("CUSTOM", "Custom", ""),
|
||||
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
|
||||
|
||||
@@ -106,8 +106,7 @@ class BIM_PT_camera(Panel):
|
||||
row.operator("bim.activate_drawing_style")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.cut_section", text="Create Drawing")
|
||||
row.operator("bim.create_drawing", text="Create Drawing 2.0")
|
||||
row.operator("bim.create_drawing", text="Create Drawing")
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
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")
|
||||
if self.props.is_editing:
|
||||
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:
|
||||
row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user