mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 13:58:12 +00:00
WIP support for cutting a section with dimension annotation lines
This commit is contained in:
@@ -61,8 +61,10 @@ classes = (
|
|||||||
operator.FetchObjectPassport,
|
operator.FetchObjectPassport,
|
||||||
operator.AddSubcontext,
|
operator.AddSubcontext,
|
||||||
operator.RemoveSubcontext,
|
operator.RemoveSubcontext,
|
||||||
|
operator.CutSection,
|
||||||
prop.Subcontext,
|
prop.Subcontext,
|
||||||
prop.BIMProperties,
|
prop.BIMProperties,
|
||||||
|
prop.DocProperties,
|
||||||
prop.BIMLibrary,
|
prop.BIMLibrary,
|
||||||
prop.MapConversion,
|
prop.MapConversion,
|
||||||
prop.TargetCRS,
|
prop.TargetCRS,
|
||||||
@@ -75,6 +77,7 @@ classes = (
|
|||||||
prop.BIMMaterialProperties,
|
prop.BIMMaterialProperties,
|
||||||
prop.SweptSolid,
|
prop.SweptSolid,
|
||||||
prop.BIMMeshProperties,
|
prop.BIMMeshProperties,
|
||||||
|
ui.BIM_PT_documentation,
|
||||||
ui.BIM_PT_bim,
|
ui.BIM_PT_bim,
|
||||||
ui.BIM_PT_context,
|
ui.BIM_PT_context,
|
||||||
ui.BIM_PT_qa,
|
ui.BIM_PT_qa,
|
||||||
@@ -102,6 +105,7 @@ def register():
|
|||||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||||
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
|
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties)
|
||||||
|
bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
|
||||||
bpy.types.Scene.BIMLibrary = bpy.props.PointerProperty(type=prop.BIMLibrary)
|
bpy.types.Scene.BIMLibrary = bpy.props.PointerProperty(type=prop.BIMLibrary)
|
||||||
bpy.types.Scene.MapConversion = bpy.props.PointerProperty(type=prop.MapConversion)
|
bpy.types.Scene.MapConversion = bpy.props.PointerProperty(type=prop.MapConversion)
|
||||||
bpy.types.Scene.TargetCRS = bpy.props.PointerProperty(type=prop.TargetCRS)
|
bpy.types.Scene.TargetCRS = bpy.props.PointerProperty(type=prop.TargetCRS)
|
||||||
@@ -117,6 +121,7 @@ def unregister():
|
|||||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||||
del(bpy.types.Scene.BIMProperties)
|
del(bpy.types.Scene.BIMProperties)
|
||||||
|
del(bpy.types.Scene.DocProperties)
|
||||||
del(bpy.types.Scene.MapConversion)
|
del(bpy.types.Scene.MapConversion)
|
||||||
del(bpy.types.Scene.TargetCRS)
|
del(bpy.types.Scene.TargetCRS)
|
||||||
del(bpy.types.Object.BIMObjectProperties)
|
del(bpy.types.Object.BIMObjectProperties)
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
import svgwrite
|
|
||||||
import time
|
import time
|
||||||
import numpy
|
import numpy
|
||||||
import pickle
|
import pickle
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from mathutils import Vector
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
# This hack is required to get the svgwrite dependency to load
|
||||||
|
# TODO: decouple cutter into its own binary
|
||||||
|
cwd = os.path.dirname(os.path.realpath(__file__)) + os.path.sep
|
||||||
|
sys.path.append(cwd)
|
||||||
|
|
||||||
|
import svgwrite
|
||||||
import OCC.gp
|
import OCC.gp
|
||||||
import OCC.Geom
|
import OCC.Geom
|
||||||
import OCC.Bnd
|
import OCC.Bnd
|
||||||
@@ -43,11 +51,14 @@ class IfcCutter:
|
|||||||
self.product_shapes = []
|
self.product_shapes = []
|
||||||
self.background_elements = []
|
self.background_elements = []
|
||||||
self.cut_polygons = []
|
self.cut_polygons = []
|
||||||
|
self.data_dir = ''
|
||||||
self.ifc_files = []
|
self.ifc_files = []
|
||||||
self.unit = None
|
self.unit = None
|
||||||
self.resolved_pixels = set()
|
self.resolved_pixels = set()
|
||||||
self.should_get_background = False
|
self.should_get_background = False
|
||||||
self.pickle_file = 'shapes.pickle'
|
self.pickle_file = 'shapes.pickle'
|
||||||
|
self.diagram_name = None
|
||||||
|
self.background_image = None
|
||||||
self.section_box = {
|
self.section_box = {
|
||||||
'projection': (0, 1, 0),
|
'projection': (0, 1, 0),
|
||||||
'x_axis': (1, 0, 0),
|
'x_axis': (1, 0, 0),
|
||||||
@@ -103,7 +114,7 @@ class IfcCutter:
|
|||||||
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
|
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
|
||||||
|
|
||||||
def load_ifc_files(self):
|
def load_ifc_files(self):
|
||||||
for filename in Path('ifc/').glob('*.ifc'):
|
for filename in Path(self.data_dir).glob('*.ifc'):
|
||||||
print('Loading file {} ...'.format(filename))
|
print('Loading file {} ...'.format(filename))
|
||||||
self.ifc_files.append(ifcopenshell.open(filename))
|
self.ifc_files.append(ifcopenshell.open(filename))
|
||||||
|
|
||||||
@@ -558,6 +569,24 @@ class IfcCutterDebug(IfcCutter):
|
|||||||
ifcopenshell.geom.utils.display_shape(element['geometry_face'])
|
ifcopenshell.geom.utils.display_shape(element['geometry_face'])
|
||||||
input('Debug: showing background elements.')
|
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():
|
class SvgWriter():
|
||||||
def __init__(self, ifc_cutter):
|
def __init__(self, ifc_cutter):
|
||||||
self.ifc_cutter = ifc_cutter
|
self.ifc_cutter = ifc_cutter
|
||||||
@@ -565,27 +594,52 @@ class SvgWriter():
|
|||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
self.calculate_scale()
|
self.calculate_scale()
|
||||||
self.svg = svgwrite.Drawing('output.svg',
|
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,
|
debug=False,
|
||||||
size=('{}mm'.format(self.ifc_cutter.section_box['x'] * self.scale),
|
size=('{}mm'.format(self.width), '{}mm'.format(self.height)),
|
||||||
'{}mm'.format(self.ifc_cutter.section_box['y'] * self.scale)),
|
viewBox=('0 0 {} {}'.format(self.width, self.height)))
|
||||||
viewBox=('0 0 {} {}'.format(
|
|
||||||
self.ifc_cutter.section_box['x'] * self.scale,
|
|
||||||
self.ifc_cutter.section_box['y'] * self.scale)))
|
|
||||||
self.add_stylesheet()
|
self.add_stylesheet()
|
||||||
|
self.add_defs()
|
||||||
|
self.draw_background_image()
|
||||||
self.draw_background_elements()
|
self.draw_background_elements()
|
||||||
self.draw_cut_polygons()
|
self.draw_cut_polygons()
|
||||||
|
self.draw_annotations()
|
||||||
self.svg.save(pretty=True)
|
self.svg.save(pretty=True)
|
||||||
|
|
||||||
def calculate_scale(self):
|
def calculate_scale(self):
|
||||||
# TODO: properly handle units
|
# TODO: properly handle units
|
||||||
if self.ifc_cutter.unit.Name == 'METRE':
|
if self.ifc_cutter.unit.Name == 'METRE':
|
||||||
self.scale *= 1000
|
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):
|
def add_stylesheet(self):
|
||||||
with open('styles/default.css', 'r') as stylesheet:
|
with open('{}styles/default.css'.format(self.ifc_cutter.data_dir), 'r') as stylesheet:
|
||||||
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
self.svg.defs.add(self.svg.style(stylesheet.read()))
|
||||||
|
|
||||||
|
def add_defs(self):
|
||||||
|
tree = ET.parse('{}styles/defs.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.basename(self.ifc_cutter.background_image), **{
|
||||||
|
'width': self.width,
|
||||||
|
'height': self.height
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
def draw_background_elements(self):
|
def draw_background_elements(self):
|
||||||
for element in self.ifc_cutter.background_elements:
|
for element in self.ifc_cutter.background_elements:
|
||||||
if element['type'] == 'polygon':
|
if element['type'] == 'polygon':
|
||||||
@@ -595,6 +649,19 @@ class SvgWriter():
|
|||||||
elif element['type'] == 'line':
|
elif element['type'] == 'line':
|
||||||
self.draw_line(element, 'background')
|
self.draw_line(element, 'background')
|
||||||
|
|
||||||
|
def draw_annotations(self):
|
||||||
|
x_offset = self.raw_width / 2
|
||||||
|
y_offset = self.raw_height / 2
|
||||||
|
for edge in self.ifc_cutter.annotation_obj.data.edges:
|
||||||
|
classes = ['annotation', 'dimension']
|
||||||
|
v0 = self.ifc_cutter.annotation_obj.data.vertices[edge.vertices[0]].co
|
||||||
|
v1 = self.ifc_cutter.annotation_obj.data.vertices[edge.vertices[1]].co
|
||||||
|
start = ((x_offset + v0.x) * self.scale, (y_offset - v0.y) * self.scale)
|
||||||
|
end = ((x_offset + v1.x) * self.scale, (y_offset - v1.y) * self.scale)
|
||||||
|
line = self.svg.add(self.svg.line(start=start, end=end, class_=' '.join(classes)))
|
||||||
|
line['marker-start'] = 'url(#dimension-marker)'
|
||||||
|
line['marker-end'] = 'url(#dimension-marker)'
|
||||||
|
|
||||||
def draw_cut_polygons(self):
|
def draw_cut_polygons(self):
|
||||||
for polygon in self.ifc_cutter.cut_polygons:
|
for polygon in self.ifc_cutter.cut_polygons:
|
||||||
self.draw_polygon(polygon, 'cut')
|
self.draw_polygon(polygon, 'cut')
|
||||||
@@ -636,12 +703,3 @@ class SvgWriter():
|
|||||||
classes.append('material-{}'.format(association.RelatingMaterial.Name))
|
classes.append('material-{}'.format(association.RelatingMaterial.Name))
|
||||||
classes.append('globalid-{}'.format(element.GlobalId))
|
classes.append('globalid-{}'.format(element.GlobalId))
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
print('# Starting process')
|
|
||||||
ifc_cutter = IfcCutter()
|
|
||||||
#ifc_cutter = IfcCutterDebug()
|
|
||||||
svg_writer = SvgWriter(ifc_cutter)
|
|
||||||
ifc_cutter.cut()
|
|
||||||
start_time = time.time()
|
|
||||||
svg_writer.write()
|
|
||||||
print('# SVG writing finished in {:.2f} seconds'.format(time.time() - start_time))
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3px; }
|
||||||
|
.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.05px; }
|
||||||
|
.annotation { stroke: black; stroke-linecap: 'round'; stroke-width: 0.05px; }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg baseProfile="full" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<marker id="dimension-marker" markerHeight="20" markerWidth="20" refX="7.5" refY="10" orient="auto">
|
||||||
|
<path d="M 7.5 0 L 7.5 20" class="annotation" style="stroke-width:2px;" />
|
||||||
|
<path d="M 0 0 L 15 20" class="annotation" style="stroke-width:3px;" />
|
||||||
|
</marker>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 498 B |
@@ -6,8 +6,10 @@ import logging
|
|||||||
from . import ifcopenshell
|
from . import ifcopenshell
|
||||||
from . import export_ifc
|
from . import export_ifc
|
||||||
from . import import_ifc
|
from . import import_ifc
|
||||||
|
from . import cut_ifc
|
||||||
from bpy_extras.io_utils import ImportHelper
|
from bpy_extras.io_utils import ImportHelper
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
class ExportIFC(bpy.types.Operator):
|
class ExportIFC(bpy.types.Operator):
|
||||||
bl_idname = "export.ifc"
|
bl_idname = "export.ifc"
|
||||||
@@ -751,3 +753,59 @@ class RemoveSubcontext(bpy.types.Operator):
|
|||||||
subcontext_index = int(subcontext_index)
|
subcontext_index = int(subcontext_index)
|
||||||
getattr(bpy.context.scene.BIMProperties, '{}_subcontexts'.format(context)).remove(subcontext_index)
|
getattr(bpy.context.scene.BIMProperties, '{}_subcontexts'.format(context)).remove(subcontext_index)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class CutSection(bpy.types.Operator):
|
||||||
|
bl_idname = 'bim.cut_section'
|
||||||
|
bl_label = 'Cut Section'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
camera = bpy.context.active_object
|
||||||
|
if not (camera.type == 'CAMERA' and camera.data.type == 'ORTHO'):
|
||||||
|
return {'FINISHED'}
|
||||||
|
self.diagram_name = camera.name
|
||||||
|
bpy.context.scene.render.filepath = os.path.join(
|
||||||
|
bpy.context.scene.BIMProperties.data_dir,
|
||||||
|
'diagrams',
|
||||||
|
'{}.png'.format(self.diagram_name)
|
||||||
|
)
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
location = camera.location
|
||||||
|
render = bpy.context.scene.render
|
||||||
|
if self.is_landscape():
|
||||||
|
width = camera.data.ortho_scale
|
||||||
|
height = width / render.resolution_x * render.resolution_y
|
||||||
|
else:
|
||||||
|
height = camera.data.ortho_scale
|
||||||
|
width = height / render.resolution_y * render.resolution_x
|
||||||
|
depth = camera.data.clip_end
|
||||||
|
projection = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||||
|
x_axis = camera.matrix_world.to_quaternion() @ Vector((1, 0, 0))
|
||||||
|
y_axis = camera.matrix_world.to_quaternion() @ Vector((0, -1, 0))
|
||||||
|
top_left_corner = location - (width / 2 * x_axis) - (height / 2 * y_axis)
|
||||||
|
ifc_cutter = cut_ifc.IfcCutter()
|
||||||
|
ifc_cutter.data_dir = bpy.context.scene.BIMProperties.data_dir
|
||||||
|
ifc_cutter.diagram_name = self.diagram_name
|
||||||
|
ifc_cutter.background_image = bpy.context.scene.render.filepath
|
||||||
|
ifc_cutter.annotation_obj = bpy.data.objects['{}-Annotation'.format(
|
||||||
|
self.diagram_name
|
||||||
|
)]
|
||||||
|
ifc_cutter.section_box = {
|
||||||
|
'projection': tuple(projection),
|
||||||
|
'x_axis': tuple(x_axis),
|
||||||
|
'y_axis': tuple(y_axis),
|
||||||
|
'top_left_corner': tuple(top_left_corner),
|
||||||
|
'x': width,
|
||||||
|
'y': height,
|
||||||
|
'z': depth,
|
||||||
|
'shape': None,
|
||||||
|
'face': None
|
||||||
|
}
|
||||||
|
ifc_cutter.pickle_file = os.path.join(ifc_cutter.data_dir, 'shapes.pickle')
|
||||||
|
svg_writer = cut_ifc.SvgWriter(ifc_cutter)
|
||||||
|
ifc_cutter.cut()
|
||||||
|
svg_writer.write()
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def is_landscape(self):
|
||||||
|
return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y
|
||||||
|
|||||||
@@ -171,6 +171,10 @@ class Subcontext(PropertyGroup):
|
|||||||
target_view: StringProperty(name='Target View')
|
target_view: StringProperty(name='Target View')
|
||||||
|
|
||||||
|
|
||||||
|
class DocProperties(PropertyGroup):
|
||||||
|
blah: StringProperty(name="Blah")
|
||||||
|
|
||||||
|
|
||||||
class BIMProperties(PropertyGroup):
|
class BIMProperties(PropertyGroup):
|
||||||
schema_dir: StringProperty(default=os.path.join(cwd ,'schema') + os.path.sep, name="Schema Directory")
|
schema_dir: StringProperty(default=os.path.join(cwd ,'schema') + os.path.sep, name="Schema Directory")
|
||||||
data_dir: StringProperty(default=os.path.join(cwd, 'data') + os.path.sep, name="Data Directory")
|
data_dir: StringProperty(default=os.path.join(cwd, 'data') + os.path.sep, name="Data Directory")
|
||||||
|
|||||||
@@ -182,6 +182,20 @@ class BIM_PT_gis(Panel):
|
|||||||
layout.row().prop(scene.TargetCRS, 'map_unit')
|
layout.row().prop(scene.TargetCRS, 'map_unit')
|
||||||
|
|
||||||
|
|
||||||
|
class BIM_PT_documentation(Panel):
|
||||||
|
bl_label = "Diagrammatic Documentation"
|
||||||
|
bl_idname = "BIM_PT_documentation"
|
||||||
|
bl_space_type = 'PROPERTIES'
|
||||||
|
bl_region_type = 'WINDOW'
|
||||||
|
bl_context = 'output'
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
props = bpy.context.scene.DocProperties
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.operator('bim.cut_section')
|
||||||
|
|
||||||
class BIM_PT_context(Panel):
|
class BIM_PT_context(Panel):
|
||||||
bl_label = "Geometric Representation Contexts"
|
bl_label = "Geometric Representation Contexts"
|
||||||
bl_idname = "BIM_PT_context"
|
bl_idname = "BIM_PT_context"
|
||||||
|
|||||||
Reference in New Issue
Block a user