First Commit
This commit is contained in:
@@ -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 *
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user