mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 11:02:44 +00:00
1021 lines
42 KiB
Python
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
|