First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,57 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 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
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Geometry processing and analysis
IFC may define geometry explicitly (such as meshes) or implicitly (such as
parametric extrusions). This module provides methods to extract geometric
definitions in IFC into explicitly tessellated triangles or OpenCASCADE Breps
for further processing.
This is typically needed when writing software to visualise or analyse
geometry. See also :mod:`ifcopenshell.util.shape` for deriving quantities.
"""
def _has_occ():
# NOTE: All pythonocc versions since 7.4.0 are using OCC.Core.
# Previous versions (pythonocc<=0.17.3) are using just OCC.
try:
import OCC.Core.BRepTools # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
return True
except ImportError:
pass
try:
import OCC.BRepTools # noqa: F401 # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
return True
except ImportError:
pass
return False
has_occ = _has_occ()
if has_occ:
from . import occ_utils as utils # noqa: F401
from .main import *
@@ -0,0 +1,691 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 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
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import functools
import multiprocessing
import os
import sys
import time
import ifcopenshell.ifcopenshell_wrapper as W
try:
from OCC.Core import AIS # noqa: F401
USE_OCCT_HANDLE = False
except ImportError:
USE_OCCT_HANDLE = True
from collections import OrderedDict, defaultdict
from collections.abc import Iterable
try:
QString = unicode
except NameError:
# Python 3
QString = str
os.environ["QT_API"] = "pyqt5"
try:
from pyqode.qt import QtCore
except BaseException:
pass
from PyQt5 import QtCore, QtGui, QtWidgets
from .code_editor_pane import code_edit
try:
from OCC.Display.pyqt5Display import qtViewer3d
except BaseException:
try:
import OCC.Display.backend
except BaseException:
pass
try:
OCC.Display.backend.get_backend("qt-pyqt5")
except BaseException:
OCC.Display.backend.load_backend("qt-pyqt5")
from OCC.Display.qtDisplay import qtViewer3d
from .. import open as open_ifc_file
from .. import version as ifcopenshell_version
from .main import iterator, settings
from .occ_utils import display_shape
if ifcopenshell_version < "0.6":
# not yet ported
from .. import get_supertype
class geometry_creation_signals(QtCore.QObject):
completed = QtCore.pyqtSignal("PyQt_PyObject")
progress = QtCore.pyqtSignal("PyQt_PyObject")
class geometry_creation_thread(QtCore.QThread):
def __init__(self, signals, settings, f):
QtCore.QThread.__init__(self)
self.signals = signals
self.settings = settings
self.f = f
def run(self):
t0 = time.time()
# detect concurrency from hardware, we need to have
# at least two threads because otherwise the interface
# is different
# is different
it = iterator(self.settings, self.f, max(2, multiprocessing.cpu_count()))
if not it.initialize():
self.signals.completed.emit([])
return
def _():
old_progress = -1
while True:
shape = it.get()
if shape:
yield shape
if not it.next():
break
self.signals.completed.emit((it, self.f, list(_())))
class configuration:
def __init__(self):
try:
import ConfigParser
Cfg = ConfigParser.RawConfigParser
except BaseException:
import configparser
def Cfg():
return configparser.ConfigParser(interpolation=None)
conf_file = os.path.expanduser(os.path.join("~", ".ifcopenshell", "app", "snippets.conf"))
if conf_file.startswith("~"):
conf_file = None
return
self.config_encode = lambda s: s.replace("\\", "\\\\").replace("\n", "\n|")
self.config_decode = lambda s: s.replace("\n|", "\n").replace("\\\\", "\\")
if not os.path.exists(os.path.dirname(conf_file)):
os.makedirs(os.path.dirname(conf_file))
if not os.path.exists(conf_file):
config = Cfg()
config.add_section("snippets")
config.set(
"snippets",
"print all wall ids",
self.config_encode("""
###########################################################################
# A simple script that iterates over all walls in the current model #
# and prints their Globally unique IDs (GUIDS) to the console window #
###########################################################################
for wall in model.by_type("IfcWall"):
print ("wall with global id: "+str(wall.GlobalId))
""".lstrip()),
)
config.set(
"snippets",
"print properties of current selection",
self.config_encode("""
###########################################################################
# A simple script that iterates over all IfcPropertySets of the currently #
# selected object and prints them to the console #
###########################################################################
# check if something is selected
if selection:
#get the IfcProduct that is stored in the global variable 'selection'
obj = selection
for relDefinesByProperties in obj.IsDefinedBy:
if not relDefinesByProperties.is_a("IfcRelDefinesByProperties"):
continue
print("[{0}]".format(relDefinesByProperties.RelatingPropertyDefinition.Name))
if relDefinesByProperties.RelatingPropertyDefinition.is_a("IfcPropertySet"):
for prop in relDefinesByProperties.RelatingPropertyDefinition.HasProperties:
print ("{:<20} :{}".format(prop.Name,prop.NominalValue.wrappedValue))
print ("\\n")
""".lstrip()),
)
with open(conf_file, "w") as configfile:
config.write(configfile)
self.config = Cfg()
self.config.read(conf_file)
def options(self, s):
return OrderedDict([(k, self.config_decode(self.config.get(s, k))) for k in self.config.options(s)])
class application(QtWidgets.QApplication):
"""A pythonOCC, PyQt based IfcOpenShell application
with two tree views and a graphical 3d view"""
class abstract_treeview(QtWidgets.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):
QtWidgets.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 = QtWidgets.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
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
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, QtCore.QItemSelectionModel.SelectCurrent | QtCore.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, **kwargs):
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] = QtWidgets.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.clicked.connect(self.clicked_)
self.expandAll()
class type_treeview(abstract_treeview):
"""Treeview with typical IFC decomposition relationships"""
ATTRIBUTES = ["Name"]
def load_file(self, f, **kwargs):
products = list(f.by_type("IfcProduct"))
types = set(map(lambda i: i.is_a(), products))
items = {}
for t in types:
def add(t):
s = get_supertype(t)
if s:
add(s)
s2, t2 = map(QString, (s, t))
if t2 not in items:
itm = items[t2] = QtWidgets.QTreeWidgetItem(items.get(s2, self), [t2])
itm.setData(0, QtCore.Qt.UserRole, t2)
self.children[s2].append(t2)
if ifcopenshell_version < "0.6":
add(t)
for p in products:
t = QString(p.is_a())
itm = items[p] = QtWidgets.QTreeWidgetItem(items.get(t, self), [p.Name or "<no name>"])
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.clicked.connect(self.clicked)
self.expandAll()
class property_table(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.layout = QtWidgets.QVBoxLayout(self)
self.setLayout(self.layout)
self.scroll = QtWidgets.QScrollArea(self)
self.layout.addWidget(self.scroll)
self.scroll.setWidgetResizable(True)
self.scrollContent = QtWidgets.QWidget(self.scroll)
self.scrollLayout = QtWidgets.QVBoxLayout(self.scrollContent)
self.scrollContent.setLayout(self.scrollLayout)
self.scroll.setWidget(self.scrollContent)
self.prop_dict = {}
# triggered by selection event in either component of parent
def select(self, product):
# Clear the old contents if any
while self.scrollLayout.count():
child = self.scrollLayout.takeAt(0)
if child is not None:
if child.widget() is not None:
child.widget().deleteLater()
self.scroll = QtWidgets.QScrollArea()
self.scroll.setWidgetResizable(True)
prop_sets = self.prop_dict.get(str(product))
if prop_sets is not None:
for k, v in prop_sets:
group_box = QtWidgets.QGroupBox()
group_box.setTitle(k)
group_layout = QtWidgets.QVBoxLayout()
group_box.setLayout(group_layout)
for name, value in v.items():
prop_name = str(name)
value_str = value
if hasattr(value_str, "wrappedValue"):
value_str = value_str.wrappedValue
if isinstance(value_str, unicode):
value_str = value_str.encode("utf-8")
else:
value_str = str(value_str)
if hasattr(value, "is_a"):
type_str = " <i>(%s)</i>" % value.is_a()
else:
type_str = ""
label = QtWidgets.QLabel("<b>%s</b>: %s%s" % (prop_name, value_str, type_str))
group_layout.addWidget(label)
group_layout.addStretch()
self.scrollLayout.addWidget(group_box)
self.scrollLayout.addStretch()
else:
label = QtWidgets.QLabel("No IfcPropertySets associated with selected entity instance")
self.scrollLayout.addWidget(label)
def load_file(self, f, **kwargs):
for p in f.by_type("IfcProduct"):
propsets = []
def process_pset(prop_def):
if prop_def is not None:
prop_set_name = prop_def.Name
props = {}
if prop_def.is_a("IfcElementQuantity"):
for q in prop_def.Quantities:
if q.is_a("IfcPhysicalSimpleQuantity"):
props[q.Name] = q[3]
elif prop_def.is_a("IfcPropertySet"):
for prop in prop_def.HasProperties:
if prop.is_a("IfcPropertySingleValue"):
props[prop.Name] = prop.NominalValue
else:
# Entity introduced in IFC4
# prop_def.is_a("IfcPreDefinedPropertySet"):
for prop in range(4, len(prop_def)):
props[prop_def.attribute_name(prop)] = prop_def[prop]
return prop_set_name, props
try:
for is_def_by in p.IsDefinedBy:
if is_def_by.is_a("IfcRelDefinesByProperties"):
propsets.append(process_pset(is_def_by.RelatingPropertyDefinition))
elif is_def_by.is_a("IfcRelDefinesByType"):
type_psets = is_def_by.RelatingType.HasPropertySets
if type_psets is None:
continue
for propset in type_psets:
propsets.append(process_pset(propset))
except Exception as e:
import traceback
print("failed to load properties: {}".format(e))
traceback.print_exc()
if len(propsets):
self.prop_dict[str(p)] = propsets
print("property set dictionary has {} entries".format(len(self.prop_dict)))
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
self.thread = None
def initialize(self):
self.InitDriver()
self._display.Select = self.HandleSelection
def finished(self, file_shapes):
it, f, shapes = file_shapes
v = self._display
t = {0: time.time()}
def update(dt=None):
t1 = time.time()
if dt is None or t1 - t[0] > dt:
v.FitAll()
v.Repaint()
t[0] = t1
for shape in shapes:
ais = display_shape(shape, viewer_handle=v)
product = f[shape.data.id]
if USE_OCCT_HANDLE:
ais.GetObject().SetSelectionPriority(self.counter)
self.ais_to_product[self.counter] = product
self.product_to_ais[product] = ais
self.counter += 1
QtWidgets.QApplication.processEvents()
if product.is_a() in {"IfcSpace", "IfcOpeningElement"}:
v.Context.Erase(ais, True)
update(1.0)
update()
self.thread = None
def load_file(self, f, setting=None):
if self.thread is not None:
return
if setting is None:
setting = settings()
setting.set("dimensionality", W.CURVES_SURFACES_AND_SOLIDS)
setting.set("use-python-opencascade", True)
self.signals = geometry_creation_signals()
thread = self.thread = geometry_creation_thread(self.signals, setting, f)
self.window.window_closed.connect(lambda *args: thread.terminate())
self.signals.completed.connect(self.finished)
self.thread.start()
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(QtWidgets.QMainWindow):
TITLE = "IfcOpenShell IFC viewer"
window_closed = QtCore.pyqtSignal([])
def __init__(self):
QtWidgets.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 = QtWidgets.QAction(QtGui.QIcon(icon), label, self)
else:
a = QtWidgets.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, settings=None):
QtWidgets.QApplication.__init__(self, sys.argv)
self.window = application.window()
self.tree = application.decomposition_treeview()
self.tree2 = application.type_treeview()
self.propview = self.property_table()
self.canvas = application.viewer(self.window)
self.tabs = QtWidgets.QTabWidget()
self.window.resize(800, 600)
splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
splitter.addWidget(self.tabs)
self.tabs.addTab(self.tree, "Decomposition")
self.tabs.addTab(self.tree2, "Types")
self.tabs.addTab(self.propview, "Properties")
splitter2 = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter2.addWidget(self.canvas)
self.editor = code_edit(self.canvas, configuration().options("snippets"))
splitter2.addWidget(self.editor)
splitter.addWidget(splitter2)
splitter.setSizes([200, 600])
splitter2.setSizes([400, 200])
self.window.setCentralWidget(splitter)
self.canvas.initialize()
self.components = [self.tree, self.tree2, self.canvas, self.propview, self.editor]
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))
self.settings = settings
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 = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open file", ".", "Industry Foundation Classes (*.ifc)"
)[0]
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 = open_ifc_file(str(fn))
self.files[fn] = f
for c in self.components:
c.load_file(f, setting=self.settings)
if __name__ == "__main__":
application().start()
@@ -0,0 +1,158 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 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
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import logging
import sys
from code import InteractiveConsole
from PyQt5 import QtCore, QtGui, QtWidgets
try:
from PyQt5 import QtWidgets
except BaseException:
QtWidgets = QtGui
try:
from pyqode.core import api, modes, panels
from pyqode.core.api import CodeEdit
from pyqode.python import modes as pymodes
from pyqode.python import panels as pypanels
from pyqode.python.backend import server
from pyqode.python.modes import PythonSH
has_pyqode = True
except BaseException:
has_pyqode = False
CodeEdit = QtWidgets.QPlainTextEdit
class StdoutRedirector:
"""A class for redirecting stdout to this Text widget."""
def __init__(self, widget):
self.widget = widget
self.isError = False
def write(self, myStr):
self.widget.moveCursor(QtGui.QTextCursor.End)
if self.isError:
self.widget.setTextColor(QtCore.Qt.red)
else:
self.widget.setTextColor(QtCore.Qt.white)
self.widget.insertPlainText(myStr)
self.widget.moveCursor(QtGui.QTextCursor.End)
class code_edit(QtWidgets.QWidget):
class Console(InteractiveConsole):
def __init__(*args):
InteractiveConsole.__init__(*args)
def enter(self, source):
self.runcode(source)
def runCode(self):
sys.stdout = StdoutRedirector(self.output)
sys.stderr = StdoutRedirector(self.output)
sys.stderr.isError = True
if not self.model:
print("please load a model first", file=sys.stderr)
else:
self.c.enter(str(self.editor.toPlainText()))
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
def select(self, product):
self.c = self.Console({"model": self.model, "viewer": self.viewer, "selection": product})
def __init__(self, viewer, snippets=None):
self.model = None
self.viewer = viewer
QtWidgets.QWidget.__init__(self)
self.layout = QtWidgets.QVBoxLayout(self)
self.setLayout(self.layout)
self.c = None
self.tools = QtWidgets.QHBoxLayout(self)
self.layout.addLayout(self.tools)
self.runbutton = QtWidgets.QPushButton("Run")
width = self.runbutton.fontMetrics().boundingRect("Run").width() + 20
self.runbutton.setMaximumWidth(width)
self.tools.addWidget(self.runbutton)
self.runbutton.clicked.connect(self.runCode)
editor = CodeEdit()
if has_pyqode:
editor.backend.start(server.__file__)
editor.panels.append(panels.FoldingPanel())
editor.panels.append(panels.LineNumberPanel())
editor.panels.append(panels.SearchAndReplacePanel(), panels.SearchAndReplacePanel.Position.BOTTOM)
editor.panels.append(panels.EncodingPanel(), api.Panel.Position.TOP)
editor.add_separator()
editor.panels.append(pypanels.QuickDocPanel(), api.Panel.Position.BOTTOM)
sh = editor.modes.append(PythonSH(editor.document()))
editor.modes.append(modes.CaretLineHighlighterMode())
editor.modes.append(modes.CodeCompletionMode())
editor.modes.append(modes.ExtendedSelectionMode())
editor.modes.append(modes.FileWatcherMode())
editor.modes.append(modes.OccurrencesHighlighterMode())
editor.modes.append(modes.RightMarginMode())
editor.modes.append(modes.SmartBackSpaceMode())
editor.modes.append(modes.SymbolMatcherMode())
editor.modes.append(modes.ZoomMode())
editor.modes.append(pymodes.CommentsMode())
editor.modes.append(pymodes.CalltipsMode())
auto = pymodes.PyAutoCompleteMode()
auto.logger.setLevel(logging.CRITICAL)
editor.modes.append(auto)
editor.modes.append(pymodes.PyAutoIndentMode())
editor.modes.append(pymodes.PyIndenterMode())
editor.show()
else:
editor.setStyleSheet("font-size: 10pt; font-family: Consolas, Courier;")
self.editor = editor
self.snippets = snippets
if self.snippets:
self.list = QtWidgets.QComboBox(self)
self.replace_snippet(0)
for snip_name in self.snippets.keys():
self.list.addItem(snip_name)
self.tools.addWidget(self.list)
self.list.currentIndexChanged[int].connect(self.replace_snippet)
self.layout.addWidget(self.editor)
self.output = QtWidgets.QTextEdit()
self.output.setReadOnly(True)
self.output.setStyleSheet("font-size: 10pt; font-family: Consolas, Courier; background-color: #444;")
self.layout.addWidget(self.output)
def replace_snippet(self, number=None):
snip = list(self.snippets.values())[number]
if has_pyqode:
self.editor.setPlainText(snip, "", "")
else:
self.editor.setPlainText(snip)
def load_file(self, f, **kwargs):
output = []
sys.stdout = StdoutRedirector(self.output)
self.model = f
self.c = self.Console({"model": self.model, "selection": None, "viewer": self.viewer})
sys.stdout = sys.__stdout__
@@ -0,0 +1,718 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 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
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from collections.abc import Generator, Iterable
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast, overload
from .. import ifcopenshell_wrapper, open
from ..entity_instance import entity_instance
from ..file import file
from . import has_occ
if TYPE_CHECKING:
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"]
T = TypeVar("T")
ShapeElementType = Union[
ifcopenshell_wrapper.BRepElement, ifcopenshell_wrapper.TriangulationElement, ifcopenshell_wrapper.SerializedElement
]
ShapeType = Union[ifcopenshell_wrapper.BRep, ifcopenshell_wrapper.Triangulation, ifcopenshell_wrapper.Serialization]
def wrap_shape_creation(settings, shape):
return shape
if has_occ:
from . import occ_utils as utils
try:
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
except ImportError:
from OCC import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
def wrap_shape_creation(settings: settings, shape: ifcopenshell_wrapper.Element):
if getattr(settings, "use_python_opencascade", False):
return utils.create_shape_from_serialization(shape)
else:
return shape
SETTING = Literal[
"angle-unit",
"apply-default-materials",
"apply-offset",
"boolean-attempt-2d",
"building-local-placement",
"cache-shapes",
"cgal-original-edges",
"cgal-smooth-angle-degrees",
"circle-segments",
"compute-curvature",
"context-identifiers",
"context-ids",
"context-types",
"convert-back-units",
"debug",
"defer-processing-first-element",
"dimensionality",
"disable-boolean-result",
"disable-opening-subtractions",
"edge-arrows",
"element-hierarchy",
"enable-layerset-slicing",
"force-space-transparency",
"function-step-param",
"function-step-type",
"generate-uvs",
"iterator-output",
"keep-bounding-boxes",
"layerset-first",
"length-unit",
"make-volume",
"max-offset-deviation",
"max-offset",
"mesher-angular-deflection",
"mesher-linear-deflection",
"model-offset",
"model-rotation",
"no-clean-triangulation",
"no-normals",
"no-parallel-mapping",
"no-wire-intersection-check",
"no-wire-intersection-tolerance",
"permissive-shape-reuse",
"precision-factor",
"precision",
"reorient-shells",
"site-local-placement",
"surface-colour",
"triangulation-type",
"unify-shapes",
"use-material-names",
"use-python-opencascade",
"use-world-coords",
"validate",
"weld-vertices",
]
SERIALIZER_SETTING = Literal[
"base-uri",
"use-element-names",
"use-element-guids",
"use-element-step-ids",
"use-element-types",
"y-up",
"ecef",
"digits",
"wkt-use-section",
"separate-z-up-node",
]
# NOTE: hybrid-cgal-simple-opencascade is added just as an example
# It's possible to use any hybrid combination by the format below:
# "hybrid-library1-library2".
# List is updated from AbstractKernel.cpp.
GEOMETRY_LIBRARY = Literal["cgal", "cgal-simple", "opencascade", "hybrid-cgal-simple-opencascade"]
class missing_setting:
def __repr__(self):
return "-"
class settings_mixin:
"""
Pythonic interface mixin to the settings modules and
to provide an additional setting to enable pythonOCC
when available
"""
def __init__(self, **kwargs):
super().__init__()
for k, v in kwargs.items():
self.set(getattr(self, k), v)
def __repr__(self):
def safe_get(x):
try:
return self.get(x)
except RuntimeError:
return missing_setting()
fmt_pair = lambda x: "%s = %r" % (self.rname(x), safe_get(x))
return "%s(%s)" % (type(self).__name__, ", ".join(map(fmt_pair, self.setting_names())))
@staticmethod
def name(k: str) -> Union[SETTING, SERIALIZER_SETTING]:
return k.lower().replace("_", "-")
@staticmethod
def rname(k: Union[SETTING, SERIALIZER_SETTING]) -> str:
return k.upper().replace("-", "_")
@overload
def set(self: settings, k: SETTING, v: Any) -> None: ...
@overload
def set(self: serializer_settings, k: SERIALIZER_SETTING, v: Any) -> None: ...
def set(self, k: SETTING, v: Any) -> None:
"""
Set value of the setting named `k` to `v`.
:raises RuntimeError: If there is no setting with name `k`.
"""
k = self.name(k)
if isinstance(self, settings) and k == "use-python-opencascade":
if not has_occ:
raise AttributeError("Python OpenCASCADE is not installed")
if v:
self.set_("iterator-output", ifcopenshell_wrapper.SERIALIZED)
self.set_("use-world-coords", True)
self.use_python_opencascade = True
else:
self.set_(self.name(k), v)
@overload
def get(self: settings, k: SETTING) -> Any: ...
@overload
def get(self: serializer_settings, k: SERIALIZER_SETTING) -> Any: ...
def get(self, k: str) -> Any:
"""
Return value of the setting named `k`.
:raises RuntimeError: If there is no setting with name `k`.
"""
k = self.name(k)
if isinstance(self, settings) and k == "use-python-opencascade":
return self.use_python_opencascade
return self.get_(k)
@overload
def setting_names(self: settings) -> tuple[SETTING, ...]: ...
@overload
def setting_names(self: serializer_settings) -> tuple[SERIALIZER_SETTING, ...]: ...
def setting_names(self) -> tuple[str, ...]:
setting_names = super().setting_names()
if isinstance(self, settings):
setting_names += ("use-python-opencascade",)
return setting_names
@overload
def __getattr__(self: settings, k: str) -> SETTING: ...
@overload
def __getattr__(self: serializer_settings, k: str) -> SERIALIZER_SETTING: ...
def __getattr__(self, k: str) -> str:
# Swig wrapper will try to access "this",
# ensure we won't accidentally call any c-extension methods
# like .setting_names() until wrapper is not completely initialized.
# See #4861.
if k == "this":
raise AttributeError("Swig wrapper's 'this' is unset.")
if k in map(self.rname, self.setting_names()):
return k
else:
raise AttributeError("'Settings' object has no attribute '%s'" % k)
def build_parser(self, parser) -> None:
"""
Accepts an argparse.ArgumentParser object, enumerates the settings in this container and
adds argument parser rules for each.
"""
type_factories = {
"bool": bool,
"int": int,
"double": float,
"std::string": str,
"std::set<int>": lambda s: list(map(int, s.split(";"))),
"std::set<std::string>": lambda s: s.split(";"),
"std::vector<double>": lambda s: list(map(float, s.split(";"))),
"IteratorOutputOptions": int,
"FunctionStepMethod": int,
"OutputDimensionalityTypes": int,
"TriangulationMethod": int,
}
for nm in self.setting_names():
if nm == "use-python-opencascade":
ty == "bool"
else:
ty = self.get_type(nm)
if ty == "bool":
group = parser.add_mutually_exclusive_group()
group.add_argument(
f"--{nm}",
dest=nm.replace("-", "_"),
action="store_true",
)
group.add_argument(
f"--no-{nm}",
dest=nm.replace("-", "_"),
action="store_false",
)
parser.set_defaults(**{nm.replace("-", "_"): None})
else:
parser.add_argument(f"--{nm}", dest=nm.replace("-", "_"), type=type_factories[ty])
def apply_namespace(self, namespace) -> None:
"""
Accepts an argparse.Namespace object, enumerates over the values in this namespace and
writes them to the settings when available
"""
names = set(self.setting_names())
for k, v in namespace._get_kwargs():
if k.replace("_", "-") in names and v is not None:
self.set(k.replace("_", "-"), v)
class serializer_settings(settings_mixin, ifcopenshell_wrapper.SerializerSettings):
pass
class settings(settings_mixin, ifcopenshell_wrapper.Settings):
use_python_opencascade = False
class iterator(ifcopenshell_wrapper.Iterator):
def __init__(
self,
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
):
self.settings = settings
if isinstance(file_or_filename, file):
self.file = file
file_or_filename = file_or_filename.wrapped_data
else:
file_or_filename = self.file = open(file_or_filename)
if include is not None and exclude is not None:
raise ValueError("include and exclude cannot be specified simultaneously")
if include is not None or exclude is not None:
# Couldn't get the typemaps properly applied using %extend so we
# replicate the SWIG-generated __init__ call on the output of a
# free function.
# @todo verify this works with SWIG 4
include_or_exclude = include if exclude is None else exclude
include_or_exclude_type = set(x.__class__.__name__ for x in include_or_exclude)
if include_or_exclude_type == {"entity_instance"}:
include_or_exclude = cast(set[entity_instance], include_or_exclude)
if not all((last_inst := inst).is_a("IfcProduct") for inst in include_or_exclude):
raise ValueError(
f"include and exclude need to be an aggregate of IfcProduct. Violating element: '{last_inst}'."
)
initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude_id
include_or_exclude = [i.id() for i in include_or_exclude]
else:
initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude
self.this = initializer(
geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads
)
else:
self.this = ifcopenshell_wrapper.construct_iterator(
geometry_library, self.settings, file_or_filename, num_threads
)
if has_occ:
def get(self):
return wrap_shape_creation(self.settings, ifcopenshell_wrapper.Iterator.get(self))
def __iter__(self) -> Generator[IteratorOutput, None, None]:
if self.initialize():
while True:
yield self.get()
if not self.next():
break
def get_task_products(self):
return entity_instance.wrap_value(ifcopenshell_wrapper.Iterator.get_task_products(self), self.file)
ClashType = Literal["protrusion", "pierce", "collision", "clearance"]
CLASH_TYPE_ITEMS = ("protrusion", "pierce", "collision", "clearance")
class tree(ifcopenshell_wrapper.tree):
def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None):
args = [self]
if file is not None:
args.append(file.wrapped_data)
if settings is not None:
args.append(settings)
ifcopenshell_wrapper.tree.__init__(*args)
def add_file(self, file: file, settings: settings) -> None:
ifcopenshell_wrapper.tree.add_file(self, file.wrapped_data, settings)
def add_iterator(self, iterator: iterator) -> None:
ifcopenshell_wrapper.tree.add_file(self, iterator)
def select(
self,
value: Union[
entity_instance, ifcopenshell_wrapper.BRepElement, tuple[float, float, float], TopoDS.TopoDS_Shape
],
**kwargs,
) -> list[entity_instance]:
def unwrap(value):
if isinstance(value, entity_instance):
return value.wrapped_data
elif all(map(lambda v: hasattr(value, v), "XYZ")):
return value.X(), value.Y(), value.Z()
return value
args = [self, unwrap(value)]
if isinstance(value, (entity_instance, ifcopenshell_wrapper.BRepElement)):
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs["extend"])
elif isinstance(value, (list, tuple)) and len(value) == 3 and set(map(type, value)) == {float}:
if "extend" in kwargs:
args.append(kwargs["extend"])
elif has_occ:
if isinstance(value, TopoDS.TopoDS_Shape):
args[1] = utils.serialize_shape(value)
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs["extend"])
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)]
def select_box(self, value, **kwargs) -> list[entity_instance]:
def unwrap(value):
if isinstance(value, entity_instance):
return value.wrapped_data
elif hasattr(value, "Get"):
return value.Get()[:3], value.Get()[3:]
return value
args = [self, unwrap(value)]
if "extend" in kwargs or "completely_within" in kwargs:
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs.get("extend", -1.0e-5))
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)]
def clash_intersection_many(
self,
set_a: Iterable[entity_instance],
set_b: Iterable[entity_instance],
tolerance: float = 0.002,
check_all: bool = True,
) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all]
return ifcopenshell_wrapper.tree.clash_intersection_many(*args)
def clash_collision_many(
self, set_a: Iterable[entity_instance], set_b: Iterable[entity_instance], allow_touching=False
) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching]
return ifcopenshell_wrapper.tree.clash_collision_many(*args)
def clash_clearance_many(
self,
set_a: Iterable[entity_instance],
set_b: Iterable[entity_instance],
clearance: float = 0.05,
check_all: bool = False,
) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all]
return ifcopenshell_wrapper.tree.clash_clearance_many(*args)
@staticmethod
def get_clash_type(clash_type_i: int) -> ClashType:
"""Convert clash type index to a readable string format.
:param clash_type_i: Type index that comes from ``clash.clash_type``.
"""
return CLASH_TYPE_ITEMS[clash_type_i]
def create_shape(
settings: settings,
inst: entity_instance,
repr: Optional[entity_instance] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Union[ShapeType, ShapeElementType, ifcopenshell_wrapper.Transformation, utils.shape_tuple, TopoDS.TopoDS_Shape]:
"""
Returns a geometric interpretation of the IFC entity instance
Note that in Python, you must store a reference to the element returned by this function to prevent garbage
collection when you access its children. See #1124.
:raises RuntimeError: If failed to process shape. You can turn detailed logging to get more details.
:return:
- `inst` is IfcProduct and `repr` provided / None -> ShapeElementType\n
- `inst` is IfcRepresentation and `repr` is None -> ShapeType\n
- `inst` is IfcRepresentationItem and `repr` is None -> ShapeType\n
- `inst` is IfcProfileDef and `repr` is None -> ShapeType\n
- `inst` is IfcPlacement / IfcObjectPlacement -> Transformation\n
- `inst` is IfcTypeProduct and `repr` is None -> None\n
- `inst` is IfcTypeProduct and `repr` is provided -> RuntimeError
(for IfcTypeProducts provide just IfcRepresentation as `inst`).\n
If 'use-python-opencascade' is enabled in settings then\n
- instead of ShapeElementType it returns shape_tuple, \n
- instead of ShapeType it returns TopoDS.TopoDS_Shape.
Example:
.. code:: python
settings = ifcopenshell.geom.settings()
settings.set("use-python-opencascade", True)
ifc_file = ifcopenshell.open(file_path)
products = ifc_file.by_type("IfcProduct")
for i, product in enumerate(products):
if product.Representation is not None:
try:
created_shape = geom.create_shape(settings, inst=product)
shape = created_shape.geometry # see #1124
shape_gpXYZ = shape.Location().Transformation().TranslationPart() # These are methods of the TopoDS_Shape class from pythonOCC
print(shape_gpXYZ.X(), shape_gpXYZ.Y(), shape_gpXYZ.Z()) # These are methods of the gpXYZ class from pythonOCC
except:
print("Shape creation failed")
"""
return wrap_shape_creation(
settings,
ifcopenshell_wrapper.create_shape(
settings, inst.wrapped_data, repr.wrapped_data if repr is not None else None, geometry_library
),
)
def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper.item:
"""
Returns an interpretation of the geometry encoded as per IfcOpenShell's taxonomy layer.
In many cases this is somewhat equivalent to the raw IFC data (but schema-agnostic in C++), but
in other cases such as IfcParameterizedProfileDef the returned item is the equivalent
of an explicit composite curve.
>>> point = ifc_file.by_type('IfcCartesianPoint')[0]
>>> ifcopenshell.geom.map_shape(ifcopenshell.geom.settings(), point).components
(0.0, 0.0, 0.0)
"""
return ifcopenshell_wrapper.map_shape(settings, inst.wrapped_data)
@overload
def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ...
@overload
def consume_iterator(
it: iterator, with_progress: Literal[True]
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def consume_iterator(
it: iterator, with_progress: bool
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def consume_iterator(
it: iterator, with_progress: bool = False
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
if it.initialize():
while True:
if with_progress:
yield it.progress(), it.get()
else:
yield it.get()
if not it.next():
break
# Overloads need to cover different return types
# based on `with_progress` argument.
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: Literal[False] = False,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[IteratorOutput, None, None]: ...
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: Literal[True] = True,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: bool = False,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: bool = False,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
"""Get a geometry iterator for the provided file.
:param cache: .h5 cache filepath (might not exist, will be created).
:param serializer_settings: Settings for cache serializer. Required if `cache` is provided.
"""
it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library)
if cache:
assert serializer_settings, "`serializer_settings` argument is not optional if `cache` is provided."
hdf5_cache = serializers.hdf5(cache, settings, serializer_settings)
it.set_cache(hdf5_cache)
yield from consume_iterator(it, with_progress=with_progress)
def make_shape_function(fn):
def entity_instance_or_none(e):
return None if e is None else entity_instance(e)
if has_occ:
def _(schema, string_or_shape, *args):
if isinstance(string_or_shape, TopoDS.TopoDS_Shape):
string_or_shape = utils.serialize_shape(string_or_shape)
return entity_instance_or_none(fn(schema, string_or_shape, *args))
else:
def _(schema, string, *args):
return entity_instance_or_none(fn(schema, string, *args))
return _
serialise = make_shape_function(ifcopenshell_wrapper.serialise)
tesselate = make_shape_function(ifcopenshell_wrapper.tesselate)
def transform_string(v: Union[str, serializers.buffer]) -> serializers.buffer:
if isinstance(v, str):
return ifcopenshell_wrapper.buffer(v)
return v
class serializers:
# Python does not have automatic casts. The C++ serializers accept a stream_or_filename
# which in C++ can be automatically constructed from a filename string. In Python we
# have to implement this cast/construction explicitly by transform_string.
@staticmethod
def obj(
out_filename: Union[str, serializers.buffer],
mtl_filename: Union[str, serializers.buffer],
geometry_settings: settings,
settings: serializer_settings,
) -> ifcopenshell_wrapper.WaveFrontOBJSerializer:
out_filename = transform_string(out_filename)
mtl_filename = transform_string(mtl_filename)
return ifcopenshell_wrapper.WaveFrontOBJSerializer(out_filename, mtl_filename, geometry_settings, settings)
@staticmethod
def svg(
out_filename: Union[str, serializers.buffer], geometry_settings: settings, settings: serializer_settings
) -> ifcopenshell_wrapper.SvgSerializer:
out_filename = transform_string(out_filename)
return ifcopenshell_wrapper.SvgSerializer(out_filename, geometry_settings, settings)
# Hdf- Xml- and glTF- serializers don't support writing to a buffer, only to filename
# so no wrap_buffer_creation() for these serializers
xml = ifcopenshell_wrapper.XmlSerializer
buffer = ifcopenshell_wrapper.buffer
# gltf, hdf5, collada and json availability depend on IfcOpenShell configuration settings
try:
gltf = ifcopenshell_wrapper.GltfSerializer
except:
pass
try:
hdf5 = ifcopenshell_wrapper.HdfSerializer
except:
pass
try:
collada = ifcopenshell_wrapper.ColladaSerializer
except:
pass
try:
json = ifcopenshell_wrapper.JsonSerializer
except:
pass
# ttl is always available since it doesn't depend on any C++ libraries,
# just people might be using an outdated binary
if hasattr(ifcopenshell_wrapper, "TtlWktSerializer"):
@staticmethod
def ttl(
out_filename: Union[str, serializers.buffer], geometry_settings: settings, settings: serializer_settings
) -> ifcopenshell_wrapper.SvgSerializer:
out_filename = transform_string(out_filename)
return ifcopenshell_wrapper.TtlWktSerializer(out_filename, geometry_settings, settings)
@classmethod
def guess_from_extension(cls, filepath: str):
ext = filepath.split(".")[-1]
mapping = {
"glb": "gltf",
"hdf": "hdf5",
"h5": "hdf5",
"hdf5": "hdf5",
"obj": "obj",
"svg": "svg",
"ttl": "ttl",
"xml": "xml",
"dae": "collada",
}
serializer_name = mapping.get(ext)
if not serializer_name:
raise ValueError(f"No serializer available for .{ext} file")
return getattr(cls, serializer_name)
@@ -0,0 +1,306 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 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
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import inspect
import operator
import random
import warnings
from collections.abc import Iterable
from typing import NamedTuple, Union
import OCC # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
from typing_extensions import assert_never
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
try:
from OCC.Core import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
AIS,
BRepTools,
Graphic3d,
Quantity,
TopoDS,
V3d,
gp,
)
USE_OCCT_HANDLE = False
except ImportError:
from OCC import ( # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
AIS,
BRepTools,
Graphic3d,
Quantity,
TopoDS,
V3d,
gp,
)
USE_OCCT_HANDLE = True
class shape_tuple(NamedTuple):
"""A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape."""
data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization]
geometry: TopoDS.TopoDS_Shape
styles: tuple[tuple[float, float, float, float], ...]
style_ids: tuple[int, ...]
handle, main_loop, add_menu, add_function_to_menu = None, None, None, None
DEFAULT_STYLES = {
"DEFAULT": (0.7, 0.7, 0.7),
"IfcSite": (0.75, 0.8, 0.65),
"IfcSlab": (0.4, 0.4, 0.4),
"IfcWallStandardCase": (0.9, 0.9, 0.9),
"IfcWall": (0.9, 0.9, 0.9),
"IfcWindow": (0.75, 0.8, 0.75, 0.3),
"IfcDoor": (0.55, 0.3, 0.15),
"IfcBeam": (0.75, 0.7, 0.7),
"IfcRailing": (0.65, 0.6, 0.6),
"IfcMember": (0.65, 0.6, 0.6),
"IfcPlate": (0.8, 0.8, 0.8),
}
def initialize_display():
import OCC.Display.SimpleGui # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
global handle, main_loop, add_menu, add_function_to_menu
handle, main_loop, add_menu, add_function_to_menu = OCC.Display.SimpleGui.init_display()
def setup():
viewer_handle = handle.GetViewer()
viewer = viewer_handle.GetObject() if hasattr(viewer_handle, "GetObject") else viewer_handle
def lights():
viewer.InitActiveLights()
for _ in range(2):
try:
active_light = viewer.ActiveLight()
except BaseException:
break
yield active_light
viewer.NextActiveLights()
lights = list(lights())
for l in lights:
viewer.DelLight(l)
if hasattr(V3d, "V3d_TypeOfOrientation_Yup_AxoRight"):
dirs = [[V3d.V3d_TypeOfOrientation_Yup_AxoRight], [V3d.V3d_TypeOfOrientation_Zup_AxoRight]]
else:
dirs = [(3, 2, 1), (-1, -2, -3)]
for dir in dirs:
if OCC.VERSION < "7.5":
light = V3d.V3d_DirectionalLight(viewer_handle)
light.SetDirection(*dir)
else:
light = V3d.V3d_DirectionalLight(*dir)
viewer.SetLightOn(light.GetHandle() if USE_OCCT_HANDLE else light)
setup()
return handle
def yield_subshapes(shape):
it = TopoDS.TopoDS_Iterator(shape)
while it.More():
yield it.Value()
it.Next()
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 = None
material = Graphic3d.Graphic3d_MaterialAspect(Graphic3d.Graphic3d_NOM_PLASTER)
if representation and not clr:
if len(set(representation.styles)) == 1:
clr = representation.styles[0]
if min(clr) < 0.0 or max(clr) > 1.0:
clr = DEFAULT_STYLES.get(representation.data.type, DEFAULT_STYLES["DEFAULT"])
if clr:
ais = AIS.AIS_Shape(shape)
ais.SetMaterial(material)
if isinstance(clr, str):
qclr = getattr(
Quantity, "Quantity_NOC_%s" % clr.upper(), getattr(Quantity, "Quantity_NOC_%s1" % clr.upper(), None)
)
if qclr is None:
raise Exception("No color named '%s'" % clr.upper())
elif isinstance(clr, Iterable):
clr = tuple(clr)
if len(clr) < 3 or len(clr) > 4:
raise Exception("Need 3 or 4 color components. Got '%r'." % len(clr))
qclr = Quantity.Quantity_Color(clr[0], clr[1], clr[2], Quantity.Quantity_TOC_RGB)
elif isinstance(clr, Quantity.Quantity_Color):
qclr = clr
else:
raise Exception("Object of type %r cannot be used as a color." % type(clr))
ais.SetColor(qclr)
if isinstance(clr, tuple) and len(clr) == 4 and clr[3] < 1.0:
ais.SetTransparency(1.0 - clr[3])
elif representation and hasattr(AIS, "AIS_MultipleConnectedShape"):
default_style_applied = None
ais = AIS.AIS_MultipleConnectedShape(shape)
subshapes = list(yield_subshapes(shape))
lens = len(representation.styles), len(subshapes)
if lens[0] != lens[1]:
warnings.warn("Unable to assign styles to subshapes. Encountered %d styles for %d shapes." % lens)
else:
for shp, stl in zip(subshapes, representation.styles):
subshape = AIS.AIS_Shape(shp)
if min(stl) < 0.0 or max(stl) > 1.0:
default_style_applied = stl = DEFAULT_STYLES.get(
representation.data.type, DEFAULT_STYLES["DEFAULT"]
)
subshape.SetColor(Quantity.Quantity_Color(stl[0], stl[1], stl[2], Quantity.Quantity_TOC_RGB))
subshape.SetMaterial(material)
if len(stl) == 4 and stl[3] < 1.0:
subshape.SetTransparency(1.0 - stl[3])
ais.Connect(subshape.GetHandle())
# For some reason it is necessary to set transparency here again
# in order for transparency to be rendered on the subshape.
applied_styles = representation.styles
if default_style_applied:
if len(default_style_applied) == 3:
default_style_applied += (1.0,)
applied_styles += (default_style_applied,)
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.0:
ais.SetTransparency(1.0)
else:
ais = AIS.AIS_Shape(shape)
ais.SetMaterial(material)
def r():
return random.random() * 0.3 + 0.7
clr = Quantity.Quantity_Color(r(), r(), r(), Quantity.Quantity_TOC_RGB)
ais.SetColor(clr)
ais_handle = ais.GetHandle() if USE_OCCT_HANDLE else ais
viewer_handle.Context.Display(ais_handle, False)
return ais_handle
def set_shape_transparency(ais, t, update_viewer=True):
handle.Context.SetTransparency(ais, t, update_viewer)
def get_bounding_box_center(bbox):
bbmin = [0.0] * 3
bbmax = [0.0] * 3
bbmin[0], bbmin[1], bbmin[2], bbmax[0], bbmax[1], bbmax[2] = bbox.Get()
return gp.gp_Pnt(*map(lambda xy: (xy[0] + xy[1]) / 2.0, zip(bbmin, bbmax)))
def serialize_shape(shape):
shapes = BRepTools.BRepTools_ShapeSet()
# @todo provide method to get ifcopenshell's built-in occt version to
# see whether this is necessary
shapes.SetFormatNb(2)
shapes.Add(shape)
# Check if WriteToString method exists and has the correct signature
# In PythonOCC >= 7.8.0, WriteToString signature changed and requires additional arguments
if hasattr(shapes, "WriteToString"):
try:
# Try to get the method signature
sig = inspect.signature(shapes.WriteToString)
# If WriteToString has no parameters (just self), use it
# This works for PythonOCC < 7.8.0
if len(sig.parameters) == 0:
return shapes.WriteToString()
except (ValueError, TypeError):
# If signature inspection fails, fall through to Write() method
pass
# Fall back to Write() method for newer PythonOCC versions (>= 7.8.0)
# or when WriteToString is not available/compatible
return shapes.Write()
def create_shape_from_serialization(
brep_object: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization],
) -> Union[shape_tuple, TopoDS.TopoDS_Shape]:
brep_data, occ_shape, styles, style_ids = None, None, (), ()
is_product_shape = True
if isinstance(brep_object, ifcopenshell_wrapper.SerializedElement):
brep_data = brep_object.geometry.brep_data
styles = brep_object.geometry.surface_styles
style_ids = brep_object.geometry.surface_style_ids
elif isinstance(brep_object, ifcopenshell_wrapper.Serialization):
try:
brep_data = brep_object.brep_data
styles = brep_object.surface_styles
style_ids = brep_object.surface_style_ids
is_product_shape = False
except BaseException as e:
print("Error occurred creating a shape:", e)
else:
assert_never(brep_object)
styles = tuple(styles[i : i + 4] for i in range(0, len(styles), 4))
if not brep_data:
return shape_tuple(brep_object, None, styles, style_ids)
try:
if OCC.VERSION < "7.8":
ss = BRepTools.BRepTools_ShapeSet()
ss.ReadFromString(brep_data)
occ_shape = ss.Shape(ss.NbShapes())
else:
ss = BRepTools.breptools()
occ_shape = ss.ReadFromString(brep_data)
except BaseException as e:
print("Error occurred parsing a shape from a string:", e)
if is_product_shape:
return shape_tuple(brep_object, occ_shape, styles, style_ids)
else:
return occ_shape
@@ -0,0 +1,246 @@
import sys
from itertools import accumulate
from typing import IO, Optional
from . import ifcopenshell_wrapper
class TransformDefaultDict:
def __init__(self, keyfunc=lambda x: x, default_factory=None):
"""
Similar to collections.defaultdict, but allows for storing
non-hashable keys by means of a transform function
"""
self.keyfunc = keyfunc
self.default_factory = default_factory
self._data = {}
self._original_keys = {}
def __setitem__(self, key, value):
k = self.keyfunc(key)
self._data[k] = value
self._original_keys[k] = key
def __getitem__(self, key):
k = self.keyfunc(key)
if k in self._data:
return self._data[k]
if self.default_factory is not None:
value = self.default_factory()
self._data[k] = value
self._original_keys[k] = key
return value
raise KeyError(key)
def __contains__(self, key):
return self.keyfunc(key) in self._data
def __delitem__(self, key):
k = self.keyfunc(key)
del self._data[k]
del self._original_keys[k]
def __iter__(self):
return iter(self._original_keys.values())
def __len__(self):
return len(self._data)
def items(self):
return ((self._original_keys[k], v) for k, v in self._data.items())
def keys(self):
return self._original_keys.values()
def values(self):
return self._data.values()
def get(self, key, default=None):
return self._data.get(self.keyfunc(key), default)
def setdefault(self, key, default=None):
k = self.keyfunc(key)
if k not in self._data:
self._data[k] = default
self._original_keys[k] = key
return self._data[k]
def update(self, other):
for k, v in other.items():
self[k] = v
def clear(self):
self._data.clear()
self._original_keys.clear()
def __repr__(self):
items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
return f"{self.__class__.__name__}({{{items}}})"
class ProductCounter:
def __init__(self, only_with_representation=True):
self.total = 0
transform_func = lambda decl: decl.name() if hasattr(decl, "name") else decl
self.counts = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
self.counts_excl = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
self.only_with_representation = only_with_representation
def count(self, decl, values):
if decl._is("IfcProduct"):
if self.only_with_representation and values.get("Representation") is None:
return
self.counts_excl[decl] += 1
while decl.name() != "IfcProduct":
self.counts[decl] += 1
decl = decl.supertype()
self.total += 1
def print(self, out=sys.stdout):
if self.counts:
results = []
def process(ent, level=-1):
if ent in self.counts:
results.append((level, ent.name(), self.counts[ent]))
for subtype in ent.subtypes():
process(subtype, level=level + 1)
prod = next(iter(self.counts.keys())).schema().declaration_by_name("ifcproduct")
process(prod)
max_width = max(level * 2 + len(name) for level, name, _ in results)
for level, name, count in results:
print(" " * (level * 2), f"{name:<{max_width - level * 2}}", ": ", count, file=out, sep="")
class BooleanResultCounter:
def __init__(self, exclude_union=True):
self.total = 0
self.exclude_union = exclude_union
def count(self, decl, values):
if decl._is("IfcBooleanResult"):
if self.exclude_union and values.get("Operator") == "UNION":
return
self.total += 1
def print(self, out=sys.stdout):
print("IfcBooleanResult:", self.total, file=out)
class StatsCollector:
streamer: ifcopenshell_wrapper.InstanceStreamer
page_size: Optional[int]
file_stream: Optional[IO[str]]
finalized: bool = False
needs_data: bool = False
counters: list
num_semis: int = 0
def __init__(self):
self.streamer = ifcopenshell_wrapper.InstanceStreamer()
self.needs_data = True
self.counters = [ProductCounter(), BooleanResultCounter()]
def feedFromFile(self, f: Optional[IO[str]] = None):
if f:
self.file_stream = f
self.feed(self.file_stream.read(self.page_size))
def feed(self, data: str):
self.streamer.pushPage(data)
self.needs_data = False
self.num_semis = self.streamer.semicolonCount()
@staticmethod
def fromFilePath(fn, page_size: int = 102400):
collector = StatsCollector()
collector.page_size = page_size
collector.feedFromFile(open(str(fn), encoding="ascii"))
return collector
def next(self):
if self.num_semis > 0:
if inst := self.streamer.readInstancePy(True):
self.num_semis -= 1
return inst["type"], dict(list(inst.items())[2:])
else:
self.finalized = True
return None
elif self.file_stream:
self.feedFromFile()
return self.next()
else:
self.needs_data = True
return None
def process(self):
if n := self.next():
decl, values = n
else:
return
if decl.schema().name() == "HEADER_SECTION_SCHEMA":
return
for cnt in self.counters:
cnt.count(decl, values)
def print(self, out=sys.stdout):
for cnt in self.counters:
print(type(cnt).__name__, file=out)
print("=" * len(type(cnt).__name__), file=out)
cnt.print(out)
def includeElementTypesBasedOnBudget(self, priorities: dict, budget):
def specificity(decl, target):
if decl._is(target):
i = 0
while decl.name() != target:
decl = decl.supertype()
i += 1
return i
return None
def calc_prio(ty):
# find most specific priority directive for the given type in ty
return min(((specificity(ty, k), v) for k, v in priorities.items() if ty._is(k)), default=(0, 0))[1]
sorted_types = sorted(self.counters[0].counts_excl.keys(), key=calc_prio, reverse=True)
sorted_types = [s for s in sorted_types if not s._is("IfcFeatureElement")]
counts = [self.counters[0].counts_excl[v] for v in sorted_types]
ccounts = list(accumulate(counts))
if ccounts[0] >= budget:
# At the very least include one type
return sorted_types[0:1]
for i, s in enumerate(ccounts):
if s >= budget:
over = s - budget
under = budget - ccounts[i - 1]
k = i if over <= under else i - 1
return sorted_types[0 : k + 1]
# We're lucky we can render everything in budget
return sorted_types[:]
if __name__ == "__main__":
collector = StatsCollector.fromFilePath(sys.argv[1])
while not collector.finalized:
collector.process()
collector.print()
print(
*collector.includeElementTypesBasedOnBudget(
{
"IfcWall": 3,
"IfcSlab": 3,
"IfcWindow": 2,
"IfcDoor": 2,
"IfcBuildingElement": 1,
}
)
)