Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/addons/acisbrowser/browser.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

298 lines
9.5 KiB
Python

# Copyright (c) 2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Iterator, Iterable, Optional
import ezdxf
from ezdxf.addons.xqt import (
QtWidgets,
QtGui,
QAction,
QMessageBox,
QFileDialog,
Qt,
QModelIndex,
)
from ezdxf.document import Drawing
from ezdxf.entities import Body
from ezdxf.lldxf.const import DXFStructureError
from .data import AcisData, BinaryAcisData, TextAcisData
APP_NAME = "ACIS Structure Browser"
BROWSER_WIDTH = 1024
BROWSER_HEIGHT = 768
SELECTOR_WIDTH_FACTOR = 0.20
FONT_FAMILY = "monospaced"
def make_font():
font = QtGui.QFont(FONT_FAMILY)
font.setStyleHint(QtGui.QFont.Monospace)
return font
class AcisStructureBrowser(QtWidgets.QMainWindow):
def __init__(
self,
filename: str = "",
handle: str = "",
):
super().__init__()
self.doc: Optional[Drawing] = None
self.acis_entities: list[AcisData] = []
self.current_acis_entity = AcisData()
self.entity_selector = self.make_entity_selector()
self.acis_content_viewer = self.make_content_viewer()
self.statusbar = QtWidgets.QStatusBar(self)
self.setup_actions()
self.setup_menu()
if filename:
self.load_dxf(filename)
else:
self.setWindowTitle(APP_NAME)
self.setStatusBar(self.statusbar)
self.setCentralWidget(self.make_central_widget())
self.resize(BROWSER_WIDTH, BROWSER_HEIGHT)
self.connect_slots()
if handle:
try:
int(handle, 16)
except ValueError:
msg = f"Given handle is not a hex value: '{handle}'"
self.statusbar.showMessage(msg)
print(msg)
else:
if not self.goto_handle(handle):
msg = f"Handle '{handle}' not found."
self.statusbar.showMessage(msg)
print(msg)
def make_entity_selector(self):
return QtWidgets.QListWidget(self)
def make_content_viewer(self):
viewer = QtWidgets.QPlainTextEdit(self)
viewer.setReadOnly(True)
viewer.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
return viewer
def make_central_widget(self):
container = QtWidgets.QSplitter(Qt.Horizontal)
container.addWidget(self.entity_selector)
container.addWidget(self.acis_content_viewer)
selector_width = int(BROWSER_WIDTH * SELECTOR_WIDTH_FACTOR)
entity_view_width = BROWSER_WIDTH - selector_width
container.setSizes([selector_width, entity_view_width])
container.setCollapsible(0, False)
container.setCollapsible(1, False)
return container
def connect_slots(self):
self.entity_selector.clicked.connect(self.acis_entity_activated)
self.entity_selector.activated.connect(self.acis_entity_activated)
# noinspection PyAttributeOutsideInit
def setup_actions(self):
self._open_action = self.make_action(
"&Open DXF File...", self.open_dxf, shortcut="Ctrl+O"
)
self._reload_action = self.make_action(
"Reload DXF File",
self.reload_dxf,
shortcut="Ctrl+R",
)
self._export_entity_action = self.make_action(
"&Export Current Entity View...",
self.export_entity,
shortcut="Ctrl+E",
)
self._export_raw_data_action = self.make_action(
"&Export Raw SAT/SAB Data...",
self.export_raw_entity,
shortcut="Ctrl+W",
)
self._quit_action = self.make_action(
"&Quit", self.close, shortcut="Ctrl+Q"
)
def make_action(
self,
name,
slot,
*,
shortcut: str = "",
tip: str = "",
) -> QAction:
action = QAction(name, self)
if shortcut:
action.setShortcut(shortcut)
if tip:
action.setToolTip(tip)
action.triggered.connect(slot)
return action
def setup_menu(self):
menu = self.menuBar()
file_menu = menu.addMenu("&File")
file_menu.addAction(self._open_action)
file_menu.addAction(self._reload_action)
file_menu.addSeparator()
file_menu.addAction(self._export_entity_action)
file_menu.addAction(self._export_raw_data_action)
file_menu.addSeparator()
file_menu.addAction(self._quit_action)
def open_dxf(self):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
caption="Select DXF file",
filter="DXF Documents (*.dxf *.DXF)",
)
if path:
self.load_dxf(path)
def load_dxf(self, path: str):
try:
doc = ezdxf.readfile(path)
except IOError as e:
QMessageBox.critical(self, "Loading Error", str(e))
return
except DXFStructureError as e:
QMessageBox.critical(
self,
"DXF Structure Error",
f'Invalid DXF file "{path}": {str(e)}',
)
return
entities = list(get_acis_entities(doc))
if len(entities):
self.doc = doc
self.set_acis_entities(entities)
self.update_title(path)
self.statusbar.showMessage(self.make_loading_message())
else:
msg = f"DXF file '{path}' contains no ACIS data"
QMessageBox.information(self, "Loading Error", msg)
def make_loading_message(self) -> str:
assert self.doc is not None
dxfversion = self.doc.dxfversion
acis_type = "SAB" if dxfversion >= "AC1027" else "SAT"
return f"Loaded DXF file has version {self.doc.acad_release}/{dxfversion}" \
f" and contains {acis_type} data"
def set_acis_entities(self, entities: list[AcisData]):
self.acis_entities = entities
self.update_entity_selector(entities)
self.set_current_acis_entity(entities[0])
def reload_dxf(self):
try:
index = self.acis_entities.index(self.current_acis_entity)
except IndexError:
index = -1
self.load_dxf(self.doc.filename)
if index > 0:
self.set_current_acis_entity(self.acis_entities[index])
def export_entity(self):
dxf_entity = self.get_current_dxf_entity()
if dxf_entity is None:
return
path, _ = QFileDialog.getSaveFileName(
self,
caption="Export Current Entity View",
dir=f"{dxf_entity.dxftype()}-{dxf_entity.dxf.handle}.txt",
filter="Text Files (*.txt *.TXT)",
)
if path:
write_data(self.current_acis_entity, path)
def export_raw_entity(self):
dxf_entity = self.get_current_dxf_entity()
if dxf_entity is None:
return
filename = f"{dxf_entity.dxftype()}-{dxf_entity.dxf.handle}"
sab = dxf_entity.has_binary_data
if sab:
filter_ = "Standard ACIS Binary Files (*.sab *.SAB)"
filename += ".sab"
else:
filter_ = "Standard ACIS Text Files (*.sat *.SAT)"
filename += ".sat"
path, _ = QFileDialog.getSaveFileName(
self,
caption="Export ACIS Raw Data",
dir=filename,
filter=filter_,
)
if path:
if sab:
with open(path, "wb") as fp:
fp.write(dxf_entity.sab)
else:
with open(path, "wt") as fp:
fp.write("\n".join(dxf_entity.sat))
def get_current_dxf_entity(self) -> Optional[Body]:
current = self.current_acis_entity
if not current.handle or self.doc is None:
return None
return self.doc.entitydb.get(current.handle) # type: ignore
def update_title(self, path: str):
self.setWindowTitle(f"{APP_NAME} - {path}")
def acis_entity_activated(self, index: QModelIndex):
if len(self.acis_entities) == 0:
return
try:
self.set_current_acis_entity(self.acis_entities[index.row()])
except IndexError:
self.set_current_acis_entity(self.acis_entities[0])
def set_current_acis_entity(self, entity: AcisData):
if entity:
self.current_acis_entity = entity
self.update_acis_content_viewer(entity)
def update_acis_content_viewer(self, entity: AcisData):
viewer = self.acis_content_viewer
viewer.clear()
viewer.setPlainText("\n".join(entity.lines))
def update_entity_selector(self, entities: Iterable[AcisData]):
viewer = self.entity_selector
viewer.clear()
viewer.addItems([e.name for e in entities])
def goto_handle(self, handle: str) -> bool:
for entity in self.acis_entities:
if entity.handle == handle:
self.set_current_acis_entity(entity)
return True
return False
def get_acis_entities(doc: Drawing) -> Iterator[AcisData]:
for e in doc.entitydb.values():
if isinstance(e, Body):
handle = e.dxf.handle
name = f"<{handle}> {e.dxftype()}"
if e.has_binary_data:
yield BinaryAcisData(e.sab, name, handle)
else:
yield TextAcisData(e.sat, name, handle)
def write_data(entity: AcisData, path: str):
try:
with open(path, "wt") as fp:
fp.write("\n".join(entity.lines))
except IOError:
pass