Files
IfcOpenShell/src/ifcblenderexport/blenderbim/cut_ifc.py
T

1021 lines
42 KiB
Python

import os
import math
import time
import numpy
import pickle
import sys
from pathlib import Path
mathutils = sys.modules.get('mathutils')
if mathutils is not None:
from mathutils import Vector
from mathutils import geometry
from math import degrees
import xml.etree.ElementTree as ET
import svgwrite
import OCC.gp
import OCC.Geom
import OCC.Bnd
import OCC.BRepBndLib
import OCC.BRep
import OCC.BRepPrimAPI
import OCC.BRepAlgoAPI
import OCC.BRepBuilderAPI
import OCC.TopOpeBRepTool
import OCC.TopOpeBRepBuild
import OCC.ShapeExtend
import OCC.GProp
import OCC.BRepGProp
import OCC.GC
import OCC.ShapeAnalysis
import OCC.TopTools
import OCC.TopExp
import OCC.HLRAlgo
import OCC.HLRBRep
import OCC.TopLoc
import OCC.Bnd
import OCC.BRepBndLib
import OCC.BRepTools
import OCC.TopoDS
import OCC.GeomLProp
import OCC.IntCurvesFace
from OCC.TopoDS import topods
import ifcopenshell
import ifcopenshell.geom
cwd = os.path.dirname(os.path.realpath(__file__))
this_file = os.path.join(cwd, 'cut_ifc.py')
def get_booleaned_edges(shape):
edges = []
exp = OCC.TopExp.TopExp_Explorer(shape, OCC.TopAbs.TopAbs_EDGE)
while exp.More():
edges.append(topods.Edge(exp.Current()))
exp.Next()
return edges
def connect_edges_into_wires(unconnected_edges):
edges = OCC.TopTools.TopTools_HSequenceOfShape()
edges_handle = OCC.TopTools.Handle_TopTools_HSequenceOfShape(edges)
wires = OCC.TopTools.TopTools_HSequenceOfShape()
wires_handle = OCC.TopTools.Handle_TopTools_HSequenceOfShape(wires)
for edge in unconnected_edges:
edges.Append(edge)
OCC.ShapeAnalysis.ShapeAnalysis_FreeBounds.ConnectEdgesToWires(edges_handle, 1e-5, True, wires_handle)
return wires_handle.GetObject()
def do_cut(process_data):
global_id, shape, section, trsf_data = process_data
axis = OCC.gp.gp_Ax2(
OCC.gp.gp_Pnt(
trsf_data['top_left_corner'][0],
trsf_data['top_left_corner'][1],
trsf_data['top_left_corner'][2]),
OCC.gp.gp_Dir(
trsf_data['projection'][0],
trsf_data['projection'][1],
trsf_data['projection'][2]),
OCC.gp.gp_Dir(
trsf_data['x_axis'][0],
trsf_data['x_axis'][1],
trsf_data['x_axis'][2])
)
source = OCC.gp.gp_Ax3(axis)
destination = OCC.gp.gp_Ax3(
OCC.gp.gp_Pnt(0, 0, 0),
OCC.gp.gp_Dir(0, 0, -1),
OCC.gp.gp_Dir(1, 0, 0))
transformation = OCC.gp.gp_Trsf()
transformation.SetDisplacement(source, destination)
cut_polygons = []
section = OCC.BRepAlgoAPI.BRepAlgoAPI_Section(section, shape).Shape()
section_edges = get_booleaned_edges(section)
if len(section_edges) <= 0:
return cut_polygons
wires = connect_edges_into_wires(section_edges)
for i in range(wires.Length()):
wire_shape = wires.Value(i+1)
transformed_wire = OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(
wire_shape, transformation)
wire_shape = transformed_wire.Shape()
wire = topods.Wire(wire_shape)
face = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeFace(wire).Face()
points = []
exp = OCC.BRepTools.BRepTools_WireExplorer(wire)
while exp.More():
point = OCC.BRep.BRep_Tool.Pnt(exp.CurrentVertex())
points.append((point.X(), -point.Y()))
exp.Next()
cut_polygons.append({ 'global_id': global_id, 'points': points })
return cut_polygons
class IfcCutter:
def __init__(self):
self.product_shapes = []
self.background_elements = []
self.cut_polygons = []
self.data_dir = ''
self.ifc_files = []
self.unit = None
self.resolved_pixels = set()
self.should_get_background = False
self.shapes_pickle_file = 'shapes.pickle'
self.cut_pickle_file = 'cut.pickle'
self.should_recut = True
self.diagram_name = None
self.background_image = None
self.section_box = {
'projection': (0, 1, 0),
'x_axis': (1, 0, 0),
'y_axis': (0, 0, -1),
'top_left_corner': (-2, 2, 8),
'x': 14,
'y': 9,
'z': 2,
'shape': None,
'face': None
}
def cut(self):
start_time = time.time()
print('# Load files')
self.load_ifc_files()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get units')
self.get_units()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get product shapes')
self.get_product_shapes()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Create section box')
self.create_section_box()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get cut polygons')
self.get_cut_polygons()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
if not self.should_get_background:
return
start_time = time.time()
print('# Get background elements')
self.get_background_elements()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Sort background elements')
self.sort_background_elements(reverse=True)
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Merge background_elements')
self.merge_background_elements()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Sort background elements')
self.sort_background_elements()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
def load_ifc_files(self):
for filename in Path(self.data_dir).glob('*.ifc'):
print('Loading file {} ...'.format(filename))
self.ifc_files.append(ifcopenshell.open(filename))
def get_units(self):
unit_assignment = self.ifc_files[0].by_type('IfcUnitAssignment')[0]
for unit in unit_assignment.Units:
if unit.UnitType == 'LENGTHUNIT':
self.unit = unit
break
def get_product_shapes(self):
if not self.should_recut:
return
shape_map = {}
if os.path.isfile(self.shapes_pickle_file):
with open(self.shapes_pickle_file, 'rb') as shape_file:
shape_map = pickle.load(shape_file)
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_PYTHON_OPENCASCADE, True)
products = []
for ifc_file in self.ifc_files:
products.extend(ifc_file.by_type('IfcProduct'))
total_products = len(products)
for i, product in enumerate(products):
print('{}/{} geometry processed ...'.format(i, total_products), end='\r', flush=True)
if product.is_a('IfcOpeningElement') or product.is_a('IfcSite'):
continue
if product.Representation is not None:
try:
if product.GlobalId in shape_map:
shape = shape_map[product.GlobalId]
else:
shape = ifcopenshell.geom.create_shape(settings, product).geometry
shape_map[product.GlobalId] = shape
self.product_shapes.append((product, shape))
except:
print('Failed to create shape for {}'.format(product))
if not os.path.isfile(self.shapes_pickle_file):
with open(self.shapes_pickle_file, 'wb') as shape_file:
pickle.dump(shape_map, shape_file, protocol=pickle.HIGHEST_PROTOCOL)
def sort_background_elements(self, reverse=None):
if reverse:
new_list = sorted(self.background_elements, key=lambda k: -k['z'])
else:
new_list = sorted(self.background_elements, key=lambda k: k['z'])
self.background_elements = new_list
def process_grid(self, face, resolution):
try:
bbox = self.get_bbox(face)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
except:
return
current_x = 0
current_y = 0
is_visible = False
while current_x < self.section_box['x']:
current_y = 0
while current_y > -self.section_box['y']:
if current_x < xmin \
or current_x > xmax \
or current_y < ymin \
or current_y > ymax:
current_y -= resolution
continue
if (current_x, current_y) in self.resolved_pixels:
current_y -= resolution
continue
point = numpy.array((current_x, current_y, 0))
hit = self.raycast(face, point)
if hit:
is_visible = True
self.resolved_pixels.add((current_x, current_y))
current_y -= resolution
current_x += resolution
return is_visible
def merge_background_elements(self):
background_elements = []
resolution = 0.1 # 10cm
# DO CUT
total_product_shapes = len(self.cut_polygons)
n = 0
for element in self.cut_polygons:
#print('{}/{} background elements processed ...'.format(n, total_product_shapes), end='\r', flush=True)
print('{}/{} cut polygons processed ...'.format(n, total_product_shapes))
print('{} resolved pixels'.format(len(self.resolved_pixels)))
n += 1
self.process_grid(element['geometry_face'], resolution)
# DO BACKGROUND
total_product_shapes = len(self.background_elements)
n = 0
for element in self.background_elements:
#print('{}/{} background elements processed ...'.format(n, total_product_shapes), end='\r', flush=True)
print('{}/{} background elements processed ...'.format(n, total_product_shapes))
print('{} resolved pixels'.format(len(self.resolved_pixels)))
n += 1
if element['type'] != 'polygon':
background_elements.append(element)
continue
is_visible = self.process_grid(element['geometry_face'], resolution)
if is_visible:
background_elements.append(element)
print('##### BEFORE it had {} and after it had {}'.format(
len(self.background_elements), len(background_elements)))
self.background_elements = background_elements
return
def create_section_box(self):
top_left_corner = OCC.gp.gp_Pnt(
self.section_box['top_left_corner'][0],
self.section_box['top_left_corner'][1],
self.section_box['top_left_corner'][2])
axis = OCC.gp.gp_Ax2(
top_left_corner,
OCC.gp.gp_Dir(
self.section_box['projection'][0],
self.section_box['projection'][1],
self.section_box['projection'][2]),
OCC.gp.gp_Dir(
self.section_box['x_axis'][0],
self.section_box['x_axis'][1],
self.section_box['x_axis'][2])
)
section_box = OCC.BRepPrimAPI.BRepPrimAPI_MakeBox(
axis, self.section_box['x'], self.section_box['y'], self.section_box['z']
)
self.section_box['shape'] = section_box.Shape()
self.section_box['face'] = section_box.BottomFace()
source = OCC.gp.gp_Ax3(axis)
self.transformation_data = {
'top_left_corner': self.section_box['top_left_corner'],
'projection': self.section_box['projection'],
'x_axis': self.section_box['x_axis']
}
destination = OCC.gp.gp_Ax3(
OCC.gp.gp_Pnt(0, 0, 0),
OCC.gp.gp_Dir(0, 0, -1),
OCC.gp.gp_Dir(1, 0, 0))
self.transformation_dest = destination
self.transformation = OCC.gp.gp_Trsf()
self.transformation.SetDisplacement(source, destination)
def get_background_elements(self):
total_product_shapes = len(self.product_shapes)
n = 0
intersections = []
compound = OCC.TopoDS.TopoDS_Compound()
builder = OCC.BRep.BRep_Builder()
builder.MakeCompound(compound)
for product, shape in self.product_shapes:
builder.Add(compound, shape)
print('{}/{} background elements processed ...'.format(n, total_product_shapes), end='\r', flush=True)
#print('Processing product {} '.format(product.Name))
n += 1
intersection = OCC.BRepAlgoAPI.BRepAlgoAPI_Common(self.section_box['shape'], shape).Shape()
intersection_edges = self.get_booleaned_edges(intersection)
if len(intersection_edges) <= 0:
continue
intersections.append(intersection)
transformed_intersection = OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(
intersection, self.transformation)
intersection = transformed_intersection.Shape()
edge_face_map = OCC.TopTools.TopTools_IndexedDataMapOfShapeListOfShape()
OCC.TopExp.topexp.MapShapesAndAncestors(
intersection, OCC.TopAbs.TopAbs_EDGE,
OCC.TopAbs.TopAbs_FACE, edge_face_map)
exp = OCC.TopExp.TopExp_Explorer(intersection, OCC.TopAbs.TopAbs_FACE)
while exp.More():
face = topods.Face(exp.Current())
normal = self.get_normal(face)
# Cull back-faces
if normal.Z() <= 0:
exp.Next()
continue
zpos, zmax = self.calculate_face_zpos(face)
self.build_new_face(face, zpos, product)
self.get_split_edges(edge_face_map, face, zmax, product)
exp.Next()
def get_raycast_hits(self, shape):
resolution = 0.1 # 5cm
hits = []
current_x = 0
current_y = 0
while current_x < self.section_box['x'] /2:
current_y = 0
while current_y < self.section_box['y']/4:
point = numpy.array(self.section_box['top_left_corner'])
point = numpy.add(point, current_x * numpy.array(self.section_box['x_axis']))
point = numpy.add(point, current_y * numpy.array(self.section_box['y_axis']))
hit = self.raycast(shape, point)
if hit:
hits.append(hit)
current_y += resolution
current_x += resolution
print('row down')
return hits
def raycast(self, shape, point):
raycast = OCC.IntCurvesFace.IntCurvesFace_ShapeIntersector()
raycast.Load(shape, 0.01)
line = OCC.gp.gp_Lin(
OCC.gp.gp_Pnt(float(point[0]), float(point[1]), float(point[2])),
OCC.gp.gp_Dir( 0, 0, -1))
raycast.Perform(line, 0, self.section_box['z'])
return raycast.NbPnt() != 0
def raycast_at_projection_dir(self, shape, point):
raycast = OCC.IntCurvesFace.IntCurvesFace_ShapeIntersector()
raycast.Load(shape, 0.01)
line = OCC.gp.gp_Lin(
OCC.gp.gp_Pnt(float(point[0]), float(point[1]), float(point[2])),
OCC.gp.gp_Dir(
self.section_box['projection'][0],
self.section_box['projection'][1],
self.section_box['projection'][2]))
raycast.Perform(line, 0, self.section_box['z'])
if raycast.NbPnt() != 0:
# The smaller WParameter is the closer z-index
# Should be the first
return { 'face': raycast.Face(1), 'z': raycast.WParameter(1) }
def get_bbox(self, shape):
bbox = OCC.Bnd.Bnd_Box()
OCC.BRepBndLib.brepbndlib_Add(shape, bbox)
return bbox
def calculate_face_zpos(self, face):
bbox = self.get_bbox(face)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
zpos = zmin + ((zmax - zmin)/2)
return zpos, zmax
def get_split_edges(self, edge_face_map, face, zmax, product):
exp2 = OCC.TopExp.TopExp_Explorer(face, OCC.TopAbs.TopAbs_EDGE)
while exp2.More():
edge = topods.Edge(exp2.Current())
adjface = OCC.TopoDS.TopoDS_Face()
getadj = OCC.TopOpeBRepBuild.TopOpeBRepBuild_Tools.GetAdjacentFace(face, edge, edge_face_map, adjface)
if getadj:
try:
edge_angle = math.degrees(self.get_angle_between_faces(face, adjface))
except:
# TODO: Figure out when a math domain error might occur,
# because it does, sometimes.
edge_angle = 0
if edge_angle > 30 and edge_angle < 160:
newedge = self.build_new_edge(edge, zmax+0.01)
if newedge:
self.background_elements.append({
'raw': product,
'geometry': newedge,
'type': 'line',
'z': zmax+0.01
})
exp2.Next()
def get_angle_between_faces(self, f1, f2):
return self.convert_dot_product_to_angle(
self.get_dot_product_of_normals(
self.get_normal(f1), self.get_normal(f2)))
def get_normal(self, face):
surface = OCC.Geom.Handle_Geom_Surface(OCC.BRep.BRep_Tool.Surface(face))
props = OCC.GeomLProp.GeomLProp_SLProps(surface, 0, 0, 1, .001)
return props.Normal()
def get_dot_product_of_normals(self, n1, n2):
return n1.X() * n2.X() + n1.Y() * n2.Y() + n1.Z() * n2.Z()
def convert_dot_product_to_angle(self, dp):
return math.acos(dp)
def is_same_point(self, p1, p2):
return p1.X() == p2.X() \
and p1.Y() == p2.Y() \
and p1.Z() == p2.Z()
def build_new_edge(self, edge, zpos):
exp = OCC.TopExp.TopExp_Explorer(edge, OCC.TopAbs.TopAbs_VERTEX)
new_vertices = []
while exp.More():
current_vertex = topods.Vertex(exp.Current())
current_point = OCC.BRep.BRep_Tool.Pnt(current_vertex)
current_point.SetZ(zpos)
new_vertices.append(OCC.BRepBuilderAPI.BRepBuilderAPI_MakeVertex(current_point).Vertex())
exp.Next()
try:
return OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(
new_vertices[0], new_vertices[1]
).Edge()
except:
return None
def build_new_face(self, face, zpos, product):
exp = OCC.TopExp.TopExp_Explorer(face, OCC.TopAbs.TopAbs_WIRE)
while exp.More():
wireexp = OCC.BRepTools.BRepTools_WireExplorer(topods.Wire(exp.Current()))
new_wire_builder = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeWire()
first_vertex = None
previous_vertex = None
while wireexp.More():
current_vertex = wireexp.CurrentVertex()
current_point = OCC.BRep.BRep_Tool.Pnt(current_vertex)
# Dodgy technique to squash in Z axis
current_point.SetZ(zpos)
current_vertex = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeVertex(current_point).Vertex()
if not first_vertex:
first_vertex = current_vertex
if not previous_vertex:
previous_vertex = current_vertex
else:
try:
new_wire_builder.Add(topods.Edge(
OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(
previous_vertex, current_vertex
).Edge()))
previous_vertex = current_vertex
except:
pass
wireexp.Next()
# make last edge
if not wireexp.More():
try:
new_wire_builder.Add(topods.Edge(
OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(
current_vertex, first_vertex
).Edge()))
except:
pass
try:
new_wire = new_wire_builder.Wire()
new_face = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeFace(new_wire).Face()
self.background_elements.append({
'raw': product,
'geometry': new_wire,
'geometry_face': new_face,
'type': 'polygon',
'z': zpos
})
except:
#print('Could not build face')
pass
exp.Next()
def get_area(self, shape):
gprops = OCC.GProp.GProp_GProps()
OCC.BRepGProp.brepgprop.SurfaceProperties(shape, gprops)
return gprops.Mass()
def get_booleaned_edges(self, shape):
edges = []
exp = OCC.TopExp.TopExp_Explorer(shape, OCC.TopAbs.TopAbs_EDGE)
while exp.More():
edges.append(topods.Edge(exp.Current()))
exp.Next()
return edges
def get_cut_polygons(self):
if self.should_recut:
self.get_fresh_cut_polygons()
self.pickle_cut_polygons()
else:
self.get_pickled_cut_polygons()
def pickle_cut_polygons(self):
with open(self.cut_pickle_file, 'wb') as pickle_file:
pickle.dump(self.cut_polygons, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
def get_fresh_cut_polygons(self):
total_product_shapes = len(self.product_shapes)
process_data = [(p.GlobalId, s, self.section_box['face'], self.transformation_data) for p, s in self.product_shapes]
import multiprocessing
import bpy
multiprocessing.set_executable(bpy.app.binary_path_python)
with multiprocessing.Pool(9) as p:
results = p.map(do_cut, process_data)
for result in results:
polygons = [p for p in result if p['points']]
self.cut_polygons.extend(polygons)
def get_pickled_cut_polygons(self):
if os.path.isfile(self.cut_pickle_file):
with open(self.cut_pickle_file, 'rb') as pickle_file:
self.cut_polygons = pickle.load(pickle_file)
class IfcCutterDebug(IfcCutter):
def cut(self):
self.occ_display = ifcopenshell.geom.utils.initialize_display()
super().cut()
def create_section_box(self):
super().create_section_box()
self.display_everything_with_section_plane()
def get_cut_polygons(self):
super().get_cut_polygons()
self.display_cut_polygons()
def get_background_elements(self):
super().get_background_elements()
self.display_background_elements()
def display_everything_with_section_plane(self):
section_face_display = ifcopenshell.geom.utils.display_shape(self.section_box['face'])
ifcopenshell.geom.utils.set_shape_transparency(section_face_display, 0.8)
section_box_display = ifcopenshell.geom.utils.display_shape(self.section_box['shape'])
ifcopenshell.geom.utils.set_shape_transparency(section_box_display, 0.5)
transformed_box = OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(
self.section_box['shape'], self.transformation)
box_display = ifcopenshell.geom.utils.display_shape(transformed_box.Shape())
ifcopenshell.geom.utils.set_shape_transparency(box_display, 0.2)
for shape in self.product_shapes:
ifcopenshell.geom.utils.display_shape(shape[1])
input('Debug: showing everything with section plane.')
def display_cut_polygons(self):
self.occ_display.EraseAll()
for polygon in self.cut_polygons:
ifcopenshell.geom.utils.display_shape(polygon['geometry'], clr='BLACK')
face = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeFace(polygon['geometry']).Face()
face_display = ifcopenshell.geom.utils.display_shape(face)
ifcopenshell.geom.utils.set_shape_transparency(face_display, 0.5)
input('Debug: showing cut polygons.')
def display_background_elements(self):
self.occ_display.EraseAll()
for element in self.background_elements:
if element['type'] == 'line':
ifcopenshell.geom.utils.display_shape(element['geometry'], clr='PURPLE')
elif element['type'] == 'polyline':
ifcopenshell.geom.utils.display_shape(element['geometry_face'], clr='RED')
elif element['type'] == 'polygon':
ifcopenshell.geom.utils.display_shape(element['geometry_face'])
input('Debug: showing background elements.')
class External(svgwrite.container.Group):
def __init__(self, xml, **extra):
self.xml = xml
# Remove namespace
ns = u'{http://www.w3.org/2000/svg}'
nsl = len(ns)
for elem in self.xml.getiterator():
if elem.tag.startswith(ns):
elem.tag = elem.tag[nsl:]
super(External, self).__init__(**extra)
def get_xml(self):
return self.xml
class SvgWriter():
def __init__(self, ifc_cutter):
self.ifc_cutter = ifc_cutter
self.human_scale = 'NTS'
self.scale = 1 / 100 # 1:100
def write(self):
self.calculate_scale()
self.output = os.path.join(
self.ifc_cutter.data_dir,
'diagrams',
self.ifc_cutter.diagram_name + '.svg'
)
self.svg = svgwrite.Drawing(
self.output,
debug=False,
size=('{}mm'.format(self.width), '{}mm'.format(self.height)),
viewBox=('0 0 {} {}'.format(self.width, self.height)),
id='root',
data_scale=self.human_scale
)
self.add_stylesheet()
self.add_markers()
self.add_patterns()
self.draw_background_image()
self.draw_background_elements()
self.draw_cut_polygons()
self.draw_annotations()
self.svg.save(pretty=True)
def calculate_scale(self):
# TODO: properly handle units
if self.ifc_cutter.unit.Name == 'METRE':
self.scale *= 1000
self.raw_width = self.ifc_cutter.section_box['x']
self.raw_height = self.ifc_cutter.section_box['y']
self.width = self.raw_width * self.scale
self.height = self.raw_height * self.scale
def add_stylesheet(self):
with open('{}styles/default.css'.format(self.ifc_cutter.data_dir), 'r') as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self):
tree = ET.parse('{}templates/markers.svg'.format(self.ifc_cutter.data_dir))
root = tree.getroot()
for child in root.getchildren():
self.svg.defs.add(External(child))
def add_patterns(self):
return
tree = ET.parse('{}templates/patterns.svg'.format(self.ifc_cutter.data_dir))
root = tree.getroot()
for child in root.getchildren():
self.svg.defs.add(External(child))
def draw_background_image(self):
self.svg.add(self.svg.image(
os.path.join('..', 'diagrams', os.path.basename(self.ifc_cutter.background_image)), **{
'width': self.width,
'height': self.height
}
))
def draw_background_elements(self):
for element in self.ifc_cutter.background_elements:
if element['type'] == 'polygon':
self.draw_polygon(element, 'background')
elif element['type'] == 'polyline':
self.draw_polyline(element, 'background')
elif element['type'] == 'line':
self.draw_line(element, 'background')
def draw_annotations(self):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
if self.ifc_cutter.equal_obj:
self.draw_dimension_annotations(self.ifc_cutter.equal_obj, text_override='EQ')
if self.ifc_cutter.dimension_obj:
self.draw_dimension_annotations(self.ifc_cutter.dimension_obj)
for grid_obj in self.ifc_cutter.grid_objs:
for edge in grid_obj.data.edges:
classes = ['annotation', 'grid']
v0 = grid_obj.data.vertices[edge.vertices[0]].co
v1 = grid_obj.data.vertices[edge.vertices[1]].co
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
vector = end - start
line = self.svg.add(self.svg.line(start=tuple(start * self.scale),
end=tuple(end * self.scale), class_=' '.join(classes)))
line['marker-start'] = 'url(#grid-marker)'
line['marker-end'] = 'url(#grid-marker)'
line['stroke-dasharray'] = '12.5, 3, 3, 3'
self.svg.add(self.svg.text(grid_obj.name.split('/')[1], insert=tuple(start * self.scale), **{
'font-size': '8.25', # 5
'font-family': 'OpenGost Type B TT',
'text-anchor': 'middle',
'alignment-baseline': 'middle',
'dominant-baseline': 'middle'
}))
self.svg.add(self.svg.text(grid_obj.name.split('/')[1], insert=tuple(end * self.scale), **{
'font-size': '8.25', # 5
'font-family': 'OpenGost Type B TT',
'text-anchor': 'middle',
'alignment-baseline': 'middle',
'dominant-baseline': 'middle'
}))
if self.ifc_cutter.hidden_obj:
matrix_world = self.ifc_cutter.hidden_obj.matrix_world
for edge in self.ifc_cutter.hidden_obj.data.edges:
classes = ['annotation', 'hidden']
v0_global = matrix_world @ self.ifc_cutter.hidden_obj.data.vertices[edge.vertices[0]].co.xyz
v1_global = matrix_world @ self.ifc_cutter.hidden_obj.data.vertices[edge.vertices[1]].co.xyz
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
vector = end - start
line = self.svg.add(self.svg.line(start=tuple(start * self.scale),
end=tuple(end * self.scale), class_=' '.join(classes)))
line['stroke-dasharray'] = '3, 2'
if self.ifc_cutter.leader_obj:
matrix_world = self.ifc_cutter.leader_obj.matrix_world
for spline in self.ifc_cutter.leader_obj.data.splines:
classes = ['annotation', 'leader']
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = ' '.join(['L {} {}'.format(
(x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
for p in projected_points])
d = 'M{}'.format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=' '.join(classes)))
path['marker-end'] = 'url(#leader-marker)'
if self.ifc_cutter.plan_level_obj:
for spline in self.ifc_cutter.plan_level_obj.data.splines:
classes = ['annotation', 'plan-level']
points = self.get_spline_points(spline)
d = ' '.join(['L {} {}'.format((x_offset + p.co.x) * self.scale, (y_offset - p.co.y) * self.scale) for p in points])
d = 'M{}'.format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=' '.join(classes)))
path['marker-end'] = 'url(#plan-level-marker)'
text_position = Vector((
(x_offset + points[0].co.x) * self.scale,
((y_offset - points[0].co.y) * self.scale) - 2.5
))
# TODO: unhardcode m unit
rl = ((self.ifc_cutter.plan_level_obj.matrix_world @
points[0].co).xyz + self.ifc_cutter.plan_level_obj.location).z
if points[0].co.x > points[-1].co.x:
text_anchor = 'end'
else:
text_anchor = 'start'
self.svg.add(self.svg.text('RL +{:.3f}m'.format(rl), insert=tuple(text_position), **{
'font-size': '4.13', # 2.5
'font-family': 'OpenGost Type B TT',
'text-anchor': text_anchor,
'alignment-baseline': 'baseline',
'dominant-baseline': 'baseline'
}))
if self.ifc_cutter.section_level_obj:
matrix_world = self.ifc_cutter.section_level_obj.matrix_world
for spline in self.ifc_cutter.section_level_obj.data.splines:
classes = ['annotation', 'section-level']
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = ' '.join(['L {} {}'.format(
(x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
for p in projected_points])
d = 'M{}'.format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=' '.join(classes)))
path['marker-start'] = 'url(#section-level-marker)'
path['stroke-dasharray'] = '12.5, 3, 3, 3'
text_position = Vector((
(x_offset + projected_points[0].x) * self.scale,
((y_offset - projected_points[0].y) * self.scale) - 3.5
))
# TODO: unhardcode m unit
rl = (matrix_world @ points[0].co.xyz).z
self.svg.add(self.svg.text('RL +{:.3f}m'.format(rl), insert=tuple(text_position), **{
'font-size': '4.13', # 2.5
'font-family': 'OpenGost Type B TT',
'text-anchor': 'start',
'alignment-baseline': 'baseline',
'dominant-baseline': 'baseline'
}))
if self.ifc_cutter.stair_obj:
for spline in self.ifc_cutter.stair_obj.data.splines:
classes = ['annotation', 'stair']
points = self.get_spline_points(spline)
d = ' '.join(['L {} {}'.format((x_offset + p.co.x) * self.scale, (y_offset - p.co.y) * self.scale) for p in points])
d = 'M{}'.format(d[1:])
start = Vector(((x_offset + points[0].co.x), (y_offset - points[0].co.y)))
next_point = Vector(((x_offset + points[1].co.x), (y_offset - points[1].co.y)))
text_position = (start * self.scale) - ((next_point - start).normalized() * 5)
path = self.svg.add(self.svg.path(d=d, class_=' '.join(classes)))
path['marker-start'] = 'url(#stair-marker-start)'
path['marker-end'] = 'url(#stair-marker-end)'
self.svg.add(self.svg.text('UP', insert=tuple(text_position), **{
'font-size': '4.13', # 2.5
'font-family': 'OpenGost Type B TT',
'text-anchor': 'middle',
'alignment-baseline': 'middle',
'dominant-baseline': 'middle'
}))
for text_obj in self.ifc_cutter.text_objs:
loc, rot, scale = self.ifc_cutter.camera_obj.matrix_world.decompose()
pos = (text_obj.location - self.ifc_cutter.camera_obj.location) @ rot.to_matrix()
text_position = Vector(((x_offset + pos.x), (y_offset - pos.y)))
if text_obj.data.align_x == 'CENTER':
text_anchor = 'middle'
elif text_obj.data.align_x == 'RIGHT':
text_anchor = 'end'
else:
text_anchor = 'start'
if text_obj.data.align_y == 'CENTER':
alignment_baseline = 'middle'
elif text_obj.data.align_y == 'TOP':
alignment_baseline = 'hanging'
else:
alignment_baseline = 'baseline'
for line_number, text_line in enumerate(text_obj.data.body.split('\n')):
self.svg.add(self.svg.text(
text_line,
insert=tuple((text_position * self.scale) + Vector((0, 3.5*line_number))),
**{
'font-size': '4.13', # 2.5
'font-family': 'OpenGost Type B TT',
'text-anchor': text_anchor,
'alignment-baseline': alignment_baseline,
'dominant-baseline': alignment_baseline
}
))
def draw_dimension_annotations(self, dimension_obj, text_override=None):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
matrix_world = dimension_obj.matrix_world
for spline in dimension_obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
if i+1 >= len(points):
continue
classes = ['annotation', 'dimension', 'blahblah']
v0_global = matrix_world @ points[i].co.xyz
v1_global = matrix_world @ points[i+1].co.xyz
v0 = self.project_point_onto_camera(v0_global)
v1 = self.project_point_onto_camera(v1_global)
start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
mid = ((end - start) / 2) + start
# TODO: hardcoded meters to mm conversion, until I properly do units
vector = end - start
perpendicular = Vector((vector.y, -vector.x)).normalized()
dimension = (v1_global - v0_global).length * 1000
sheet_dimension = ((end*self.scale) - (start*self.scale)).length
if sheet_dimension < 5: # annotation can't fit
# offset text to right of marker
text_position = (end * self.scale) + perpendicular + (3 * vector.normalized())
else:
text_position = (mid * self.scale) + perpendicular
rotation = degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.add(self.svg.line(start=tuple(start * self.scale),
end=tuple(end * self.scale), class_=' '.join(classes)))
line['marker-start'] = 'url(#dimension-marker-start)'
line['marker-end'] = 'url(#dimension-marker-end)'
# Standard font sizes 1.8, 2.5, 3.5, 5, 7
# Equivalent for OpenGost Type B: 2.97, 4.13, 5.78, 8.25, 11.55
if text_override is not None:
text = text_override
else:
text = str(round(dimension))
self.svg.add(self.svg.text(text, insert=tuple(text_position), **{
'transform': 'rotate({} {} {})'.format(
rotation,
text_position.x,
text_position.y
),
'font-size': '4.13', # 2.5
'font-family': 'OpenGost Type B TT',
'text-anchor': 'middle'
}))
def project_point_onto_camera(self, point):
return self.ifc_cutter.camera_obj.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz,
point.xyz-Vector(self.ifc_cutter.section_box['projection']),
self.ifc_cutter.camera_obj.location,
Vector(self.ifc_cutter.section_box['projection'])
)
def get_spline_points(self, spline):
return spline.bezier_points if spline.bezier_points else spline.points
def draw_cut_polygons(self):
for polygon in self.ifc_cutter.cut_polygons:
self.draw_polygon(polygon, 'cut')
def draw_polyline(self, element, position):
classes = self.get_classes(element['raw'], position)
exp = OCC.BRepTools.BRepTools_WireExplorer(element['geometry'])
points = []
while exp.More():
point = OCC.BRep.BRep_Tool.Pnt(exp.CurrentVertex())
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.polyline(points=points, class_=' '.join(classes)))
def draw_line(self, element, position):
classes = self.get_classes(element['raw'], position)
exp = OCC.TopExp.TopExp_Explorer(element['geometry'], OCC.TopAbs.TopAbs_VERTEX)
points = []
while exp.More():
point = OCC.BRep.BRep_Tool.Pnt(topods.Vertex(exp.Current()))
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.line(start=points[0], end=points[1], class_=' '.join(classes)))
def draw_polygon(self, polygon, position):
classes = self.get_classes(self.get_ifc_element(polygon['global_id']), position)
points = [(p[0] * self.scale, p[1] * self.scale) for p in polygon['points']]
self.svg.add(self.svg.polygon(points=points, class_=' '.join(classes)))
def get_ifc_element(self, global_id):
# TODO: make this less bad
element = None
for ifc_file in self.ifc_cutter.ifc_files:
try:
element = ifc_file.by_id(global_id)
return element
except:
pass
def get_classes(self, element, position):
classes = [position, element.is_a()]
for association in element.HasAssociations:
if association.is_a('IfcRelAssociatesMaterial'):
classes.append('material-{}'.format(association.RelatingMaterial.Name))
classes.append('globalid-{}'.format(element.GlobalId))
return classes