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