diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index 8e8435baed..1fe59e2b03 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -92,6 +92,7 @@ class entity_instance(object): def is_a(self, *args): return self.wrapped_data.is_a(*args) def id(self): return self.wrapped_data.id() def __eq__(self, other): + if type(self) != type(other): return False return self.wrapped_data == other.wrapped_data def __hash__(self): return hash((self.id(), self.wrapped_data.file_pointer())) @@ -151,3 +152,4 @@ def create_entity(type,*args,**kwargs): version = ifcopenshell_wrapper.version() schema_identifier = ifcopenshell_wrapper.schema_identifier() +get_supertype = ifcopenshell_wrapper.get_supertype diff --git a/src/ifcopenshell-python/ifcopenshell/geom/__init__.py b/src/ifcopenshell-python/ifcopenshell/geom/__init__.py index f4d05f86dc..347851aa07 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/__init__.py @@ -1,88 +1,2 @@ -############################################################################### -# # -# This file is part of IfcOpenShell. # -# # -# IfcOpenShell is free software: you can redistribute it and/or modify # -# it under the terms of the Lesser GNU General Public License as published by # -# the Free Software Foundation, either version 3.0 of the License, or # -# (at your option) any later version. # -# # -# IfcOpenShell is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# Lesser GNU General Public License for more details. # -# # -# You should have received a copy of the Lesser GNU General Public License # -# along with this program. If not, see . # -# # -############################################################################### - -import os -import sys - -from .. import ifcopenshell_wrapper - -def has_occ(): - try: import OCC.BRepTools - except: return False - return True - - -has_occ = has_occ() -wrap_shape_creation = lambda settings, shape: shape -if has_occ: - from . import occ_utils as utils - wrap_shape_creation = lambda settings, shape: utils.create_shape_from_serialization(shape) if getattr(settings, 'use_python_opencascade', False) else shape - - -# Subclass the settings module to provide an additional -# setting to enable pythonOCC when available -class settings(ifcopenshell_wrapper.settings): - if has_occ: - USE_PYTHON_OPENCASCADE = -1 - def set(self, *args): - setting, value = args - if setting == settings.USE_PYTHON_OPENCASCADE: - self.set(settings.USE_BREP_DATA, value) - self.set(settings.USE_WORLD_COORDS, value) - self.set(settings.DISABLE_TRIANGULATION, value) - self.use_python_opencascade = value - else: - ifcopenshell_wrapper.settings.set(self, *args) - - -# Hide templating precision to the user by choosing based on Python's -# internal float type. This is probably always going to be a double. -for ty in (ifcopenshell_wrapper.iterator_single_precision, ifcopenshell_wrapper.iterator_double_precision): - if ty.mantissa_size() == sys.float_info.mant_dig: - _iterator = ty - - -# Make sure people are able to use python's platform agnostic paths -class iterator(_iterator): - def __init__(self, settings, filename): - self.settings = settings - _iterator.__init__(self, settings, os.path.abspath(filename)) - if has_occ: - def get(self): - return wrap_shape_creation(self.settings, _iterator.get(self)) - - -def create_shape(settings, inst, repr=None): - return wrap_shape_creation( - settings, - ifcopenshell_wrapper.create_shape( - settings, - inst.wrapped_data, - repr.wrapped_data if repr is not None else None - )) - - -def iterate(settings, filename): - it = iterator(settings, filename) - if it.initialize(): - while True: - yield it.get() - if not it.next(): break - - +from . import occ_utils as utils +from .main import * diff --git a/src/ifcopenshell-python/ifcopenshell/geom/app.py b/src/ifcopenshell-python/ifcopenshell/geom/app.py new file mode 100644 index 0000000000..37f2c09f72 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/geom/app.py @@ -0,0 +1,370 @@ +import sys +import time +import operator +import functools + +import OCC.AIS + +import ifcopenshell + +from collections import defaultdict, Iterable + +from PyQt4 import QtGui, QtCore + +try: from OCC.Display.pyqt4Display import qtViewer3d +except: + import OCC.Display + OCC.Display.backend.get_backend("qt-pyqt4") + from OCC.Display.qtDisplay import qtViewer3d + +from .main import create_shape, settings +from .occ_utils import display_shape + +# Depending on Python version and what not there may or may not be a QString +try: + from PyQt4.QtCore import QString +except ImportError: + QString = str + +class application(QtGui.QApplication): + + """A pythonOCC, PyQt based IfcOpenShell application + with two tree views and a graphical 3d view""" + + class abstract_treeview(QtGui.QTreeWidget): + + """Base class for the two treeview controls""" + + instanceSelected = QtCore.pyqtSignal([object]) + instanceVisibilityChanged = QtCore.pyqtSignal([object, int]) + instanceDisplayModeChanged = QtCore.pyqtSignal([object, int]) + + def __init__(self): + QtGui.QTreeView.__init__(self) + self.setColumnCount(len(self.ATTRIBUTES)) + self.setHeaderLabels(self.ATTRIBUTES) + self.children = defaultdict(list) + + def get_children(self, inst): + c = [inst] + i = 0 + while i < len(c): + c.extend(self.children[c[i]]) + i += 1 + return c + + def contextMenuEvent(self, event): + menu = QtGui.QMenu(self) + visibility = [menu.addAction("Show"), menu.addAction("Hide")] + displaymode = [menu.addAction("Solid"), menu.addAction("Wireframe")] + action = menu.exec_(self.mapToGlobal(event.pos())) + index = self.selectionModel().currentIndex() + inst = index.data(QtCore.Qt.UserRole) + if hasattr(inst, 'toPyObject'): + inst = inst.toPyObject() + if action in visibility: + self.instanceVisibilityChanged.emit(inst, visibility.index(action)) + elif action in displaymode: + self.instanceDisplayModeChanged.emit(inst, displaymode.index(action)) + + def clicked(self, index): + inst = index.data(QtCore.Qt.UserRole) + if hasattr(inst, 'toPyObject'): + inst = inst.toPyObject() + if inst: + self.instanceSelected.emit(inst) + + def select(self, product): + itm = self.product_to_item.get(product) + if itm is None: return + self.selectionModel().setCurrentIndex(itm, QtGui.QItemSelectionModel.SelectCurrent | QtGui.QItemSelectionModel.Rows); + + class decomposition_treeview(abstract_treeview): + + """Treeview with typical IFC decomposition relationships""" + + ATTRIBUTES = ['Entity', 'GlobalId', 'Name'] + + def parent(self, instance): + if instance.is_a("IfcOpeningElement"): + return instance.VoidsElements[0].RelatingBuildingElement + if instance.is_a("IfcElement"): + fills = instance.FillsVoids + if len(fills): + return fills[0].RelatingOpeningElement + containments = instance.ContainedInStructure + if len(containments): + return containments[0].RelatingStructure + if instance.is_a("IfcObjectDefinition"): + decompositions = instance.Decomposes + if len(decompositions): + return decompositions[0].RelatingObject + + def load_file(self, f): + products = list(f.by_type("IfcProduct")) + list(f.by_type("IfcProject")) + parents = list(map(self.parent, products)) + items = {} + skipped = 0 + ATTRS = self.ATTRIBUTES + while len(items) + skipped < len(products): + for product, parent in zip(products, parents): + if parent is None and not product.is_a("IfcProject"): + skipped += 1 + continue + if (parent is None or parent in items) and product not in items: + sl = [] + for attr in ATTRS: + if attr == 'Entity': + sl.append(product.is_a()) + else: + sl.append(getattr(product, attr) or '') + itm = items[product] = QtGui.QTreeWidgetItem(items.get(parent, self), sl) + itm.setData(0, QtCore.Qt.UserRole, product) + self.children[parent].append(product) + self.product_to_item = dict(zip(items.keys(), map(self.indexFromItem, items.values()))) + self.connect(self, QtCore.SIGNAL("clicked(const QModelIndex &)"), self.clicked) + self.expandAll() + + class type_treeview(abstract_treeview): + + """Treeview with typical IFC decomposition relationships""" + + ATTRIBUTES = ['Name'] + + def load_file(self, f): + products = list(f.by_type("IfcProduct")) + types = set(map(lambda i: i.is_a(), products)) + items = {} + for t in types: + def add(t): + s = ifcopenshell.get_supertype(t) + if s: add(s) + s2, t2 = map(QString, (s,t)) + if t2 not in items: + itm = items[t2] = QtGui.QTreeWidgetItem(items.get(s2, self), [t2]) + itm.setData(0, QtCore.Qt.UserRole, t2) + self.children[s2].append(t2) + add(t) + + for p in products: + t = QString(p.is_a()) + itm = items[p] = QtGui.QTreeWidgetItem(items.get(t, self), [p.Name or '']) + itm.setData(0, QtCore.Qt.UserRole, t) + self.children[t].append(p) + + self.product_to_item = dict(zip(items.keys(), map(self.indexFromItem, items.values()))) + self.connect(self, QtCore.SIGNAL("clicked(const QModelIndex &)"), self.clicked) + self.expandAll() + + class viewer(qtViewer3d): + + instanceSelected = QtCore.pyqtSignal([object]) + + @staticmethod + def ais_to_key(ais_handle): + def yield_shapes(): + ais = ais_handle.GetObject() + if hasattr(ais, 'Shape'): + yield ais.Shape() + return + shp = OCC.AIS.Handle_AIS_Shape.DownCast(ais_handle) + if not shp.IsNull(): yield shp.Shape() + return + mult = ais_handle + if mult.IsNull(): + shp = OCC.AIS.Handle_AIS_Shape.DownCast(ais_handle) + if not shp.IsNull(): yield shp + else: + li = mult.GetObject().ConnectedTo() + for i in range(li.Length()): + shp = OCC.AIS.Handle_AIS_Shape.DownCast(li.Value(i+1)) + if not shp.IsNull(): yield shp + return tuple(shp.HashCode(1 << 24) for shp in yield_shapes()) + + def __init__(self, widget): + qtViewer3d.__init__(self, widget) + self.ais_to_product = {} + self.product_to_ais = {} + self.counter = 0 + self.window = widget + + def initialize(self): + self.InitDriver() + self._display.Select = self.HandleSelection + + def load_file(self, f): + + s = settings() + s.set(s.USE_PYTHON_OPENCASCADE, True) + + v = self._display + + t = {0: time.time()} + def update(dt = None): + t1 = time.time() + if t1 - t[0] > (dt or -1): + v.FitAll() + v.Repaint() + t[0] = t1 + + terminate = [False] + self.window.window_closed.connect(lambda *args: operator.setitem(terminate, 0, True)) + + for p in f.by_type("IfcProduct"): + if terminate[0]: break + if p.Representation is None: continue + shape = create_shape(s, p) + ais = display_shape(shape, viewer_handle=v) + ais.GetObject().SetSelectionPriority(self.counter) + self.ais_to_product[self.counter] = p + self.product_to_ais[p] = ais + self.counter += 1 + QtGui.QApplication.processEvents() + if p.is_a() in {'IfcSpace', 'IfcOpeningElement'}: + v.Context.Erase(ais, True) + update(0.1) + update() + + def select(self, product): + ais = self.product_to_ais.get(product) + if ais is None: return + v = self._display.Context + v.ClearSelected(False) + v.SetSelected(ais, True) + + def toggle(self, product_or_products, fn): + if not isinstance(product_or_products, Iterable): + product_or_products = [product_or_products] + aiss = list(filter(None, map(self.product_to_ais.get, product_or_products))) + last = len(aiss) - 1 + for i, ais in enumerate(aiss): + fn(ais, i == last) + + def toggle_visibility(self, product_or_products, flag): + v = self._display.Context + if flag: + def visibility(ais, last): + v.Erase(ais, last) + else: + def visibility(ais, last): + v.Display(ais, last) + self.toggle(product_or_products, visibility) + + def toggle_wireframe(self, product_or_products, flag): + v = self._display.Context + if flag: + def wireframe(ais, last): + if v.IsDisplayed(ais): + v.SetDisplayMode(ais, 0, last) + else: + def wireframe(ais, last): + if v.IsDisplayed(ais): + v.SetDisplayMode(ais, 1, last) + self.toggle(product_or_products, wireframe) + + def HandleSelection(self, X, Y): + v = self._display.Context + v.Select() + v.InitSelected() + if v.MoreSelected(): + ais = v.SelectedInteractive() + inst = self.ais_to_product[ais.GetObject().SelectionPriority()] + self.instanceSelected.emit(inst) + + class window(QtGui.QMainWindow): + + TITLE = "IfcOpenShell IFC viewer" + + window_closed = QtCore.pyqtSignal([]) + + def __init__(self): + QtGui.QMainWindow.__init__(self) + self.setWindowTitle(self.TITLE) + self.menu = self.menuBar() + self.menus = {} + + def closeEvent(self, *args): + self.window_closed.emit() + + def add_menu_item(self, menu, label, callback, icon=None, shortcut=None): + m = self.menus.get(menu) + if m is None: + m = self.menu.addMenu(menu) + self.menus[menu] = m + + if icon: + a = QtGui.QAction(QtGui.QIcon(icon), label, self) + else: + a = QtGui.QAction(label, self) + + if shortcut: + a.setShortcut(shortcut) + + a.triggered.connect(callback) + m.addAction(a) + + + def makeSelectionHandler(self, component): + def handler(inst): + for c in self.components: + if c != component: + c.select(inst) + return handler + + def __init__(self): + QtGui.QApplication.__init__(self, sys.argv) + self.window = application.window() + self.tree = application.decomposition_treeview() + self.tree2 = application.type_treeview() + self.canvas = application.viewer(self.window) + self.tabs = QtGui.QTabWidget() + self.window.resize(800, 600) + splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) + splitter.addWidget(self.tabs) + self.tabs.addTab(self.tree, 'Decomposition') + self.tabs.addTab(self.tree2, 'Types') + splitter.addWidget(self.canvas) + splitter.setSizes([200,600]) + self.window.setCentralWidget(splitter) + self.canvas.initialize() + self.components = [self.tree, self.tree2, self.canvas] + self.files = {} + + self.window.add_menu_item('File', '&Open', self.browse, shortcut='CTRL+O') + self.window.add_menu_item('File', '&Close', self.clear, shortcut='CTRL+W') + self.window.add_menu_item('File', '&Exit', self.window.close, shortcut='ALT+F4') + + self.tree.instanceSelected.connect(self.makeSelectionHandler(self.tree)) + self.tree2.instanceSelected.connect(self.makeSelectionHandler(self.tree2)) + self.canvas.instanceSelected.connect(self.makeSelectionHandler(self.canvas)) + for t in [self.tree, self.tree2]: + t.instanceVisibilityChanged.connect(functools.partial(self.change_visibility, t)) + t.instanceDisplayModeChanged.connect(functools.partial(self.change_displaymode, t)) + + def change_visibility(self, tree, inst, flag): + insts = tree.get_children(inst) + self.canvas.toggle_visibility(insts, flag) + + def change_displaymode(self, tree, inst, flag): + insts = tree.get_children(inst) + self.canvas.toggle_wireframe(insts, flag) + + def start(self): + self.window.show() + sys.exit(self.exec_()) + + def browse(self): + filename = QtGui.QFileDialog.getOpenFileName(self.window, 'Open file',".","Industry Foundation Classes (*.ifc)") + self.load(filename) + + def clear(self): + self.canvas._display.Context.RemoveAll() + self.tree.clear() + self.files.clear() + + def load(self, fn): + if fn in self.files: return + f = ifcopenshell.open(fn) + self.files[fn] = f + for c in self.components: + c.load_file(f) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py new file mode 100644 index 0000000000..b325793df2 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -0,0 +1,86 @@ +############################################################################### +# # +# This file is part of IfcOpenShell. # +# # +# IfcOpenShell is free software: you can redistribute it and/or modify # +# it under the terms of the Lesser GNU General Public License as published by # +# the Free Software Foundation, either version 3.0 of the License, or # +# (at your option) any later version. # +# # +# IfcOpenShell is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# Lesser GNU General Public License for more details. # +# # +# You should have received a copy of the Lesser GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +import os +import sys + +from .. import ifcopenshell_wrapper + +def has_occ(): + try: import OCC.BRepTools + except: return False + return True + + +has_occ = has_occ() +wrap_shape_creation = lambda settings, shape: shape +if has_occ: + from . import occ_utils as utils + wrap_shape_creation = lambda settings, shape: utils.create_shape_from_serialization(shape) if getattr(settings, 'use_python_opencascade', False) else shape + +# Subclass the settings module to provide an additional +# setting to enable pythonOCC when available +class settings(ifcopenshell_wrapper.settings): + if has_occ: + USE_PYTHON_OPENCASCADE = -1 + def set(self, *args): + setting, value = args + if setting == settings.USE_PYTHON_OPENCASCADE: + self.set(settings.USE_BREP_DATA, value) + self.set(settings.USE_WORLD_COORDS, value) + self.set(settings.DISABLE_TRIANGULATION, value) + self.use_python_opencascade = value + else: + ifcopenshell_wrapper.settings.set(self, *args) + +# Hide templating precision to the user by choosing based on Python's +# internal float type. This is probably always going to be a double. +for ty in (ifcopenshell_wrapper.iterator_single_precision, ifcopenshell_wrapper.iterator_double_precision): + if ty.mantissa_size() == sys.float_info.mant_dig: + _iterator = ty + + +# Make sure people are able to use python's platform agnostic paths +class iterator(_iterator): + def __init__(self, settings, filename): + self.settings = settings + _iterator.__init__(self, settings, os.path.abspath(filename)) + if has_occ: + def get(self): + return wrap_shape_creation(self.settings, _iterator.get(self)) + + +def create_shape(settings, inst, repr=None): + return wrap_shape_creation( + settings, + ifcopenshell_wrapper.create_shape( + settings, + inst.wrapped_data, + repr.wrapped_data if repr is not None else None + )) + + +def iterate(settings, filename): + it = iterator(settings, filename) + if it.initialize(): + while True: + yield it.get() + if not it.next(): break + + diff --git a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py index e98bad0544..aa596143d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py @@ -81,7 +81,9 @@ def yield_subshapes(shape): yield it.Value() it.Next() -def display_shape(shape, clr=None): +def display_shape(shape, clr=None, viewer_handle=None): + if viewer_handle is None: viewer_handle = handle + if isinstance(shape, shape_tuple): shape, representation = shape.geometry, shape else: representation is None @@ -145,9 +147,11 @@ def display_shape(shape, clr=None): if len(default_style_applied) == 3: default_style_applied += (1.,) applied_styles += (default_style_applied,) - min_transp = min(map(operator.itemgetter(3), applied_styles)) - if min_transp < 1.: - ais.SetTransparency(1.) + if len(applied_styles): + # The only way for this not to be true if is the entire shape is NULL + min_transp = min(map(operator.itemgetter(3), applied_styles)) + if min_transp < 1.: + ais.SetTransparency(1.) else: ais = OCC.AIS.AIS_Shape(shape) @@ -158,7 +162,7 @@ def display_shape(shape, clr=None): ais.SetColor(clr) ais_handle = ais.GetHandle() - handle.Context.Display(ais_handle, False) + viewer_handle.Context.Display(ais_handle, False) return ais_handle diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i index 24ace4cf10..d7c47767c2 100644 --- a/src/ifcwrap/IfcParseWrapper.i +++ b/src/ifcwrap/IfcParseWrapper.i @@ -220,4 +220,15 @@ namespace IfcUtil { const char* const version() { return IFCOPENSHELL_VERSION; } + + std::string get_supertype(std::string n) { + boost::to_upper(n); + /// @todo: Redo without copy once Parent() function from performance_improvements branch is in. + IfcSchema::Type::Enum t = IfcSchema::Type::FromString(n); + if (IfcSchema::Type::Parent(t) != -1) { + return IfcSchema::Type::ToString(IfcSchema::Type::Parent(t)); + } else { + return ""; + } + } %} \ No newline at end of file