refactor: excel parse

This commit is contained in:
Blizzard
2026-04-16 10:01:11 +08:00
parent 680ecc320f
commit f62f95ec02
7941 changed files with 2899112 additions and 0 deletions
@@ -0,0 +1,6 @@
# Copyright (c) 2021, Manfred Moitzi
# License: MIT License
from .data import *
from .model import *
from .browser import *
@@ -0,0 +1,33 @@
# Copyright (c) 2021, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import NamedTuple, Optional
class Bookmark(NamedTuple):
name: str
handle: str
offset: int
class Bookmarks:
def __init__(self) -> None:
self.bookmarks: dict[str, Bookmark] = dict()
def add(self, name: str, handle: str, offset: int):
self.bookmarks[name] = Bookmark(name, handle, offset)
def get(self, name: str) -> Optional[Bookmark]:
return self.bookmarks.get(name)
def names(self) -> list[str]:
return list(self.bookmarks.keys())
def discard(self, name: str):
try:
del self.bookmarks[name]
except KeyError:
pass
def clear(self):
self.bookmarks.clear()
@@ -0,0 +1,796 @@
# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Optional, Set
from functools import partial
from pathlib import Path
import subprocess
import shlex
from ezdxf.addons.xqt import (
QtWidgets,
QtGui,
QAction,
QMessageBox,
QFileDialog,
QInputDialog,
Qt,
QModelIndex,
QSettings,
QFileSystemWatcher,
QSize,
)
import ezdxf
from ezdxf.lldxf.const import DXFStructureError, DXFValueError
from ezdxf.lldxf.types import DXFTag, is_pointer_code
from ezdxf.lldxf.tags import Tags
from ezdxf.addons.browser.reflinks import get_reference_link
from .model import (
DXFStructureModel,
DXFTagsModel,
DXFTagsRole,
)
from .data import (
DXFDocument,
get_row_from_line_number,
dxfstr,
EntityHistory,
SearchIndex,
)
from .views import StructureTree, DXFTagsTable
from .find_dialog import Ui_FindDialog
from .bookmarks import Bookmarks
__all__ = ["DXFStructureBrowser"]
APP_NAME = "DXF Structure Browser"
BROWSE_COMMAND = ezdxf.options.BROWSE_COMMAND
TEXT_EDITOR = ezdxf.options.get(BROWSE_COMMAND, "TEXT_EDITOR")
ICON_SIZE = max(16, ezdxf.options.get_int(BROWSE_COMMAND, "ICON_SIZE"))
SearchSections = Set[str]
def searchable_entities(
doc: DXFDocument, search_sections: SearchSections
) -> list[Tags]:
entities: list[Tags] = []
for name, section_entities in doc.sections.items():
if name in search_sections:
entities.extend(section_entities) # type: ignore
return entities
BROWSER_WIDTH = 1024
BROWSER_HEIGHT = 768
TREE_WIDTH_FACTOR = 0.33
class DXFStructureBrowser(QtWidgets.QMainWindow):
def __init__(
self,
filename: str = "",
line: Optional[int] = None,
handle: Optional[str] = None,
resource_path: Path = Path("."),
):
super().__init__()
self.doc = DXFDocument()
self.resource_path = resource_path
self._structure_tree = StructureTree()
self._dxf_tags_table = DXFTagsTable()
self._current_entity: Optional[Tags] = None
self._active_search: Optional[SearchIndex] = None
self._search_sections: set[str] = set()
self._find_dialog: FindDialog = self.create_find_dialog()
self._file_watcher = QFileSystemWatcher()
self._exclusive_reload_dialog = True # see ask_for_reloading() method
self.history = EntityHistory()
self.bookmarks = Bookmarks()
self.setup_actions()
self.setup_menu()
self.setup_toolbar()
if filename:
self.load_dxf(filename)
else:
self.setWindowTitle(APP_NAME)
self.setCentralWidget(self.build_central_widget())
self.resize(BROWSER_WIDTH, BROWSER_HEIGHT)
self.connect_slots()
if line is not None:
try:
line = int(line)
except ValueError:
print(f"Invalid line number: {line}")
else:
self.goto_line(line)
if handle is not None:
try:
int(handle, 16)
except ValueError:
print(f"Given handle is not a hex value: {handle}")
else:
if not self.goto_handle(handle):
print(f"Handle {handle} not found.")
def build_central_widget(self):
container = QtWidgets.QSplitter(Qt.Horizontal)
container.addWidget(self._structure_tree)
container.addWidget(self._dxf_tags_table)
tree_width = int(BROWSER_WIDTH * TREE_WIDTH_FACTOR)
table_width = BROWSER_WIDTH - tree_width
container.setSizes([tree_width, table_width])
container.setCollapsible(0, False)
container.setCollapsible(1, False)
return container
def connect_slots(self):
self._structure_tree.activated.connect(self.entity_activated)
self._dxf_tags_table.activated.connect(self.tag_activated)
# noinspection PyUnresolvedReferences
self._file_watcher.fileChanged.connect(self.ask_for_reloading)
# noinspection PyAttributeOutsideInit
def setup_actions(self):
self._open_action = self.make_action(
"&Open DXF File...", self.open_dxf, shortcut="Ctrl+O"
)
self._export_entity_action = self.make_action(
"&Export DXF Entity...", self.export_entity, shortcut="Ctrl+E"
)
self._copy_entity_action = self.make_action(
"&Copy DXF Entity to Clipboard",
self.copy_entity,
shortcut="Shift+Ctrl+C",
icon_name="icon-copy-64px.png",
)
self._copy_selected_tags_action = self.make_action(
"&Copy selected DXF Tags to Clipboard",
self.copy_selected_tags,
shortcut="Ctrl+C",
icon_name="icon-copy-64px.png",
)
self._quit_action = self.make_action(
"&Quit", self.close, shortcut="Ctrl+Q"
)
self._goto_handle_action = self.make_action(
"&Go to Handle...",
self.ask_for_handle,
shortcut="Ctrl+G",
icon_name="icon-goto-handle-64px.png",
tip="Go to Entity Handle",
)
self._goto_line_action = self.make_action(
"Go to &Line...",
self.ask_for_line_number,
shortcut="Ctrl+L",
icon_name="icon-goto-line-64px.png",
tip="Go to Line Number",
)
self._find_text_action = self.make_action(
"Find &Text...",
self.find_text,
shortcut="Ctrl+F",
icon_name="icon-find-64px.png",
tip="Find Text in Entities",
)
self._goto_predecessor_entity_action = self.make_action(
"&Previous Entity",
self.goto_previous_entity,
shortcut="Ctrl+Left",
icon_name="icon-prev-entity-64px.png",
tip="Go to Previous Entity in File Order",
)
self._goto_next_entity_action = self.make_action(
"&Next Entity",
self.goto_next_entity,
shortcut="Ctrl+Right",
icon_name="icon-next-entity-64px.png",
tip="Go to Next Entity in File Order",
)
self._entity_history_back_action = self.make_action(
"Entity History &Back",
self.go_back_entity_history,
shortcut="Alt+Left",
icon_name="icon-left-arrow-64px.png",
tip="Go to Previous Entity in Browser History",
)
self._entity_history_forward_action = self.make_action(
"Entity History &Forward",
self.go_forward_entity_history,
shortcut="Alt+Right",
icon_name="icon-right-arrow-64px.png",
tip="Go to Next Entity in Browser History",
)
self._open_entity_in_text_editor_action = self.make_action(
"&Open in Text Editor",
self.open_entity_in_text_editor,
shortcut="Ctrl+T",
)
self._show_entity_in_tree_view_action = self.make_action(
"Show Entity in Structure &Tree",
self.show_current_entity_in_tree_view,
shortcut="Ctrl+Down",
icon_name="icon-show-in-tree-64px.png",
tip="Show Current Entity in Structure Tree",
)
self._goto_header_action = self.make_action(
"Go to HEADER Section",
partial(self.go_to_section, name="HEADER"),
shortcut="Shift+H",
)
self._goto_blocks_action = self.make_action(
"Go to BLOCKS Section",
partial(self.go_to_section, name="BLOCKS"),
shortcut="Shift+B",
)
self._goto_entities_action = self.make_action(
"Go to ENTITIES Section",
partial(self.go_to_section, name="ENTITIES"),
shortcut="Shift+E",
)
self._goto_objects_action = self.make_action(
"Go to OBJECTS Section",
partial(self.go_to_section, name="OBJECTS"),
shortcut="Shift+O",
)
self._store_bookmark = self.make_action(
"Store Bookmark...",
self.store_bookmark,
shortcut="Shift+Ctrl+B",
icon_name="icon-store-bookmark-64px.png",
)
self._go_to_bookmark = self.make_action(
"Go to Bookmark...",
self.go_to_bookmark,
shortcut="Ctrl+B",
icon_name="icon-goto-bookmark-64px.png",
)
self._reload_action = self.make_action(
"Reload DXF File",
self.reload_dxf,
shortcut="Ctrl+R",
)
def make_action(
self,
name,
slot,
*,
shortcut: str = "",
icon_name: str = "",
tip: str = "",
) -> QAction:
action = QAction(name, self)
if shortcut:
action.setShortcut(shortcut)
if icon_name:
icon = QtGui.QIcon(str(self.resource_path / icon_name))
action.setIcon(icon)
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.addAction(self._open_entity_in_text_editor_action)
file_menu.addSeparator()
file_menu.addAction(self._copy_selected_tags_action)
file_menu.addAction(self._copy_entity_action)
file_menu.addAction(self._export_entity_action)
file_menu.addSeparator()
file_menu.addAction(self._quit_action)
navigate_menu = menu.addMenu("&Navigate")
navigate_menu.addAction(self._goto_handle_action)
navigate_menu.addAction(self._goto_line_action)
navigate_menu.addAction(self._find_text_action)
navigate_menu.addSeparator()
navigate_menu.addAction(self._goto_next_entity_action)
navigate_menu.addAction(self._goto_predecessor_entity_action)
navigate_menu.addAction(self._show_entity_in_tree_view_action)
navigate_menu.addSeparator()
navigate_menu.addAction(self._entity_history_back_action)
navigate_menu.addAction(self._entity_history_forward_action)
navigate_menu.addSeparator()
navigate_menu.addAction(self._goto_header_action)
navigate_menu.addAction(self._goto_blocks_action)
navigate_menu.addAction(self._goto_entities_action)
navigate_menu.addAction(self._goto_objects_action)
bookmarks_menu = menu.addMenu("&Bookmarks")
bookmarks_menu.addAction(self._store_bookmark)
bookmarks_menu.addAction(self._go_to_bookmark)
def setup_toolbar(self) -> None:
toolbar = QtWidgets.QToolBar("MainToolbar")
toolbar.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
toolbar.addAction(self._entity_history_back_action)
toolbar.addAction(self._entity_history_forward_action)
toolbar.addAction(self._goto_predecessor_entity_action)
toolbar.addAction(self._goto_next_entity_action)
toolbar.addAction(self._show_entity_in_tree_view_action)
toolbar.addAction(self._find_text_action)
toolbar.addAction(self._goto_line_action)
toolbar.addAction(self._goto_handle_action)
toolbar.addAction(self._store_bookmark)
toolbar.addAction(self._go_to_bookmark)
toolbar.addAction(self._copy_selected_tags_action)
self.addToolBar(toolbar)
def create_find_dialog(self) -> "FindDialog":
dialog = FindDialog()
dialog.setModal(True)
dialog.find_forward_button.clicked.connect(self.find_forward)
dialog.find_backwards_button.clicked.connect(self.find_backwards)
dialog.find_forward_button.setShortcut("F3")
dialog.find_backwards_button.setShortcut("F4")
return dialog
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:
self._load(path)
except IOError as e:
QMessageBox.critical(self, "Loading Error", str(e))
except DXFStructureError as e:
QMessageBox.critical(
self,
"DXF Structure Error",
f'Invalid DXF file "{path}": {str(e)}',
)
else:
self.history.clear()
self.view_header_section()
self.update_title()
def reload_dxf(self):
if self._current_entity is not None:
entity = self.get_current_entity()
handle = self.get_current_entity_handle()
first_row = self._dxf_tags_table.first_selected_row()
line_number = self.doc.get_line_number(entity, first_row)
self._load(self.doc.filename)
if handle is not None:
entity = self.doc.get_entity(handle)
if entity is not None: # select entity with same handle
self.set_current_entity_and_row_index(entity, first_row)
self._structure_tree.expand_to_entity(entity)
return
# select entity at the same line number
entity = self.doc.get_entity_at_line(line_number)
self.set_current_entity_and_row_index(entity, first_row)
self._structure_tree.expand_to_entity(entity)
def ask_for_reloading(self):
if self.doc.filename and self._exclusive_reload_dialog:
# Ignore further reload signals until first signal is processed.
# Saving files by ezdxf triggers two "fileChanged" signals!?
self._exclusive_reload_dialog = False
ok = QMessageBox.question(
self,
"Reload",
f'"{self.doc.absolute_filepath()}"\n\nThis file has been '
f"modified by another program, reload file?",
buttons=QMessageBox.Yes | QMessageBox.No,
defaultButton=QMessageBox.Yes,
)
if ok == QMessageBox.Yes:
self.reload_dxf()
self._exclusive_reload_dialog = True
def _load(self, filename: str):
if self.doc.filename:
self._file_watcher.removePath(self.doc.filename)
self.doc.load(filename)
model = DXFStructureModel(self.doc.filepath.name, self.doc)
self._structure_tree.set_structure(model)
self.history.clear()
self._file_watcher.addPath(self.doc.filename)
def export_entity(self):
if self._dxf_tags_table is None:
return
path, _ = QFileDialog.getSaveFileName(
self,
caption="Export DXF Entity",
filter="Text Files (*.txt *.TXT)",
)
if path:
model = self._dxf_tags_table.model()
tags = model.compiled_tags()
self.export_tags(path, tags)
def copy_entity(self):
if self._dxf_tags_table is None:
return
model = self._dxf_tags_table.model()
tags = model.compiled_tags()
copy_dxf_to_clipboard(tags)
def copy_selected_tags(self):
if self._current_entity is None:
return
rows = self._dxf_tags_table.selected_rows()
model = self._dxf_tags_table.model()
tags = model.compiled_tags()
try:
export_tags = Tags(tags[row] for row in rows)
except IndexError:
return
copy_dxf_to_clipboard(export_tags)
def view_header_section(self):
header = self.doc.get_section("HEADER")
if header:
self.set_current_entity_with_history(header[0])
else: # DXF R12 with only a ENTITIES section
entities = self.doc.get_section("ENTITIES")
if entities:
self.set_current_entity_with_history(entities[1])
def update_title(self):
self.setWindowTitle(f"{APP_NAME} - {self.doc.absolute_filepath()}")
def get_current_entity_handle(self) -> Optional[str]:
active_entity = self.get_current_entity()
if active_entity:
try:
return active_entity.get_handle()
except DXFValueError:
pass
return None
def get_current_entity(self) -> Optional[Tags]:
return self._current_entity
def set_current_entity_by_handle(self, handle: str):
entity = self.doc.get_entity(handle)
if entity:
self.set_current_entity(entity)
def set_current_entity(
self, entity: Tags, select_line_number: Optional[int] = None
):
if entity:
self._current_entity = entity
start_line_number = self.doc.get_line_number(entity)
model = DXFTagsModel(
entity, start_line_number, self.doc.entity_index
)
self._dxf_tags_table.setModel(model)
if select_line_number is not None:
row = get_row_from_line_number(
model.compiled_tags(), start_line_number, select_line_number
)
self._dxf_tags_table.selectRow(row)
index = self._dxf_tags_table.model().index(row, 0)
self._dxf_tags_table.scrollTo(index)
def set_current_entity_with_history(self, entity: Tags):
self.set_current_entity(entity)
self.history.append(entity)
def set_current_entity_and_row_index(self, entity: Tags, index: int):
line = self.doc.get_line_number(entity, index)
self.set_current_entity(entity, select_line_number=line)
self.history.append(entity)
def entity_activated(self, index: QModelIndex):
tags = index.data(role=DXFTagsRole)
# PySide6: Tags() are converted to type list by PySide6?
# print(type(tags))
if isinstance(tags, (Tags, list)):
self.set_current_entity_with_history(Tags(tags))
def tag_activated(self, index: QModelIndex):
tag = index.data(role=DXFTagsRole)
if isinstance(tag, DXFTag):
code, value = tag
if is_pointer_code(code):
if not self.goto_handle(value):
self.show_error_handle_not_found(value)
elif code == 0:
self.open_web_browser(get_reference_link(value))
def ask_for_handle(self):
handle, ok = QInputDialog.getText(
self,
"Go to",
"Go to entity handle:",
)
if ok:
if not self.goto_handle(handle):
self.show_error_handle_not_found(handle)
def goto_handle(self, handle: str) -> bool:
entity = self.doc.get_entity(handle)
if entity:
self.set_current_entity_with_history(entity)
return True
return False
def show_error_handle_not_found(self, handle: str):
QMessageBox.critical(self, "Error", f"Handle {handle} not found!")
def ask_for_line_number(self):
max_line_number = self.doc.max_line_number
number, ok = QInputDialog.getInt(
self,
"Go to",
f"Go to line number: (max. {max_line_number})",
1, # value
1, # PyQt5: min, PySide6: minValue
max_line_number, # PyQt5: max, PySide6: maxValue
)
if ok:
self.goto_line(number)
def goto_line(self, number: int) -> bool:
entity = self.doc.get_entity_at_line(int(number))
if entity:
self.set_current_entity(entity, number)
return True
return False
def find_text(self):
self._active_search = None
dialog = self._find_dialog
dialog.restore_geometry()
dialog.show_message("F3 searches forward, F4 searches backwards")
dialog.find_text_edit.setFocus()
dialog.show()
def update_search(self):
def setup_search():
self._search_sections = dialog.search_sections()
entities = searchable_entities(self.doc, self._search_sections)
self._active_search = SearchIndex(entities)
dialog = self._find_dialog
if self._active_search is None:
setup_search()
# noinspection PyUnresolvedReferences
self._active_search.set_current_entity(self._current_entity)
else:
search_sections = dialog.search_sections()
if search_sections != self._search_sections:
setup_search()
dialog.update_options(self._active_search)
def find_forward(self):
self._find(backward=False)
def find_backwards(self):
self._find(backward=True)
def _find(self, backward=False):
if self._find_dialog.isVisible():
self.update_search()
search = self._active_search
if search.is_end_of_index:
search.reset_cursor(backward=backward)
entity, index = (
search.find_backwards() if backward else search.find_forward()
)
if entity:
self.set_current_entity_and_row_index(entity, index)
self.show_entity_found_message(entity, index)
else:
if search.is_end_of_index:
self.show_message("Not found and end of file!")
else:
self.show_message("Not found!")
def show_message(self, msg: str):
self._find_dialog.show_message(msg)
def show_entity_found_message(self, entity: Tags, index: int):
dxftype = entity.dxftype()
if dxftype == "SECTION":
tail = " @ {0} Section".format(entity.get_first_value(2))
else:
try:
handle = entity.get_handle()
tail = f" @ {dxftype}(#{handle})"
except ValueError:
tail = ""
line = self.doc.get_line_number(entity, index)
self.show_message(f"Found in Line: {line}{tail}")
def export_tags(self, filename: str, tags: Tags):
try:
with open(filename, "wt", encoding="utf8") as fp:
fp.write(dxfstr(tags))
except IOError as e:
QMessageBox.critical(self, "IOError", str(e))
def goto_next_entity(self):
if self._dxf_tags_table:
current_entity = self.get_current_entity()
if current_entity is not None:
next_entity = self.doc.next_entity(current_entity)
if next_entity is not None:
self.set_current_entity_with_history(next_entity)
def goto_previous_entity(self):
if self._dxf_tags_table:
current_entity = self.get_current_entity()
if current_entity is not None:
prev_entity = self.doc.previous_entity(current_entity)
if prev_entity is not None:
self.set_current_entity_with_history(prev_entity)
def go_back_entity_history(self):
entity = self.history.back()
if entity is not None:
self.set_current_entity(entity) # do not change history
def go_forward_entity_history(self):
entity = self.history.forward()
if entity is not None:
self.set_current_entity(entity) # do not change history
def go_to_section(self, name: str):
section = self.doc.get_section(name)
if section:
index = 0 if name == "HEADER" else 1
self.set_current_entity_with_history(section[index])
def open_entity_in_text_editor(self):
current_entity = self.get_current_entity()
line_number = self.doc.get_line_number(current_entity)
if self._dxf_tags_table:
indices = self._dxf_tags_table.selectedIndexes()
if indices:
model = self._dxf_tags_table.model()
row = indices[0].row()
line_number = model.line_number(row)
self._open_text_editor(
str(self.doc.absolute_filepath()), line_number
)
def _open_text_editor(self, filename: str, line_number: int) -> None:
cmd = TEXT_EDITOR.format(
filename=filename,
num=line_number,
)
args = shlex.split(cmd)
try:
subprocess.Popen(args)
except FileNotFoundError:
QMessageBox.critical(
self, "Text Editor", "Error calling text editor:\n" + cmd
)
def open_web_browser(self, url: str):
import webbrowser
webbrowser.open(url)
def show_current_entity_in_tree_view(self):
entity = self.get_current_entity()
if entity:
self._structure_tree.expand_to_entity(entity)
def store_bookmark(self):
if self._current_entity is not None:
bookmarks = self.bookmarks.names()
if len(bookmarks) == 0:
bookmarks = ["0"]
name, ok = QInputDialog.getItem(
self,
"Store Bookmark",
"Bookmark:",
bookmarks,
editable=True,
)
if ok:
entity = self._current_entity
rows = self._dxf_tags_table.selectedIndexes()
if rows:
offset = rows[0].row()
else:
offset = 0
handle = self.doc.get_handle(entity)
self.bookmarks.add(name, handle, offset)
def go_to_bookmark(self):
bookmarks = self.bookmarks.names()
if len(bookmarks) == 0:
QMessageBox.information(self, "Info", "No Bookmarks defined!")
return
name, ok = QInputDialog.getItem(
self,
"Go to Bookmark",
"Bookmark:",
self.bookmarks.names(),
editable=False,
)
if ok:
bookmark = self.bookmarks.get(name)
if bookmark is not None:
self.set_current_entity_by_handle(bookmark.handle)
self._dxf_tags_table.selectRow(bookmark.offset)
model = self._dxf_tags_table.model()
index = QModelIndex(model.index(bookmark.offset, 0))
self._dxf_tags_table.scrollTo(index)
else:
QtWidgets.QMessageBox.critical(
self, "Bookmark not found!", str(name)
)
def copy_dxf_to_clipboard(tags: Tags):
clipboard = QtWidgets.QApplication.clipboard()
try:
mode = clipboard.Mode.Clipboard
except AttributeError:
mode = clipboard.Clipboard # type: ignore # legacy location
clipboard.setText(dxfstr(tags), mode=mode)
class FindDialog(QtWidgets.QDialog, Ui_FindDialog):
def __init__(self):
super().__init__()
self.setupUi(self)
self.close_button.clicked.connect(lambda: self.close())
self.settings = QSettings("ezdxf", "DXFBrowser")
def restore_geometry(self):
geometry = self.settings.value("find.dialog.geometry")
if geometry is not None:
self.restoreGeometry(geometry)
def search_sections(self) -> SearchSections:
sections = set()
if self.header_check_box.isChecked():
sections.add("HEADER")
if self.classes_check_box.isChecked():
sections.add("CLASSES")
if self.tables_check_box.isChecked():
sections.add("TABLES")
if self.blocks_check_box.isChecked():
sections.add("BLOCKS")
if self.entities_check_box.isChecked():
sections.add("ENTITIES")
if self.objects_check_box.isChecked():
sections.add("OBJECTS")
return sections
def update_options(self, search: SearchIndex) -> None:
search.reset_search_term(self.find_text_edit.text())
search.case_insensitive = not self.match_case_check_box.isChecked()
search.whole_words = self.whole_words_check_box.isChecked()
search.numbers = self.number_tags_check_box.isChecked()
def closeEvent(self, event):
self.settings.setValue("find.dialog.geometry", self.saveGeometry())
super().closeEvent(event)
def show_message(self, msg: str):
self.message.setText(msg)
@@ -0,0 +1,428 @@
# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Optional, Iterable, Any, TYPE_CHECKING
from pathlib import Path
from ezdxf.addons.browser.loader import load_section_dict
from ezdxf.lldxf.types import DXFVertex, tag_type
from ezdxf.lldxf.tags import Tags
if TYPE_CHECKING:
from ezdxf.eztypes import SectionDict
__all__ = [
"DXFDocument",
"IndexEntry",
"get_row_from_line_number",
"dxfstr",
"EntityHistory",
"SearchIndex",
]
class DXFDocument:
def __init__(self, sections: Optional[SectionDict] = None):
# Important: the section dict has to store the raw string tags
# else an association of line numbers to entities is not possible.
# Comment tags (999) are ignored, because the load_section_dict()
# function can not handle and store comments.
# Therefore comments causes incorrect results for the line number
# associations and should be stripped off before processing for precise
# debugging of DXF files (-b for backup):
# ezdxf strip -b <your.dxf>
self.sections: SectionDict = dict()
self.entity_index: Optional[EntityIndex] = None
self.valid_handles = None
self.filename = ""
if sections:
self.update(sections)
@property
def filepath(self):
return Path(self.filename)
@property
def max_line_number(self) -> int:
if self.entity_index:
return self.entity_index.max_line_number
else:
return 1
def load(self, filename: str):
self.filename = filename
self.update(load_section_dict(filename))
def update(self, sections: SectionDict):
self.sections = sections
self.entity_index = EntityIndex(self.sections)
def absolute_filepath(self):
return self.filepath.absolute()
def get_section(self, name: str) -> list[Tags]:
return self.sections.get(name) # type: ignore
def get_entity(self, handle: str) -> Optional[Tags]:
if self.entity_index:
return self.entity_index.get(handle)
return None
def get_line_number(self, entity: Tags, offset: int = 0) -> int:
if self.entity_index:
return (
self.entity_index.get_start_line_for_entity(entity) + offset * 2
)
return 0
def get_entity_at_line(self, number: int) -> Optional[Tags]:
if self.entity_index:
return self.entity_index.get_entity_at_line(number)
return None
def next_entity(self, entity: Tags) -> Optional[Tags]:
return self.entity_index.next_entity(entity) # type: ignore
def previous_entity(self, entity: Tags) -> Optional[Tags]:
return self.entity_index.previous_entity(entity) # type: ignore
def get_handle(self, entity) -> Optional[str]:
return self.entity_index.get_handle(entity) # type: ignore
class IndexEntry:
def __init__(self, tags: Tags, line: int = 0):
self.tags: Tags = tags
self.start_line_number: int = line
self.prev: Optional["IndexEntry"] = None
self.next: Optional["IndexEntry"] = None
class EntityIndex:
def __init__(self, sections: SectionDict):
# dict() entries have to be ordered since Python 3.6!
# Therefore _index.values() returns the DXF entities in file order!
self._index: dict[str, IndexEntry] = dict()
# Index dummy handle of entities without handles by the id of the
# first tag for faster retrieval of the dummy handle from tags:
# dict items: (id, handle)
self._dummy_handle_index: dict[int, str] = dict()
self._max_line_number: int = 0
self._build(sections)
def _build(self, sections: SectionDict) -> None:
start_line_number = 1
dummy_handle = 1
entity_index: dict[str, IndexEntry] = dict()
dummy_handle_index: dict[int, str] = dict()
prev_entry: Optional[IndexEntry] = None
for section in sections.values():
for tags in section:
assert isinstance(tags, Tags), "expected class Tags"
assert len(tags) > 0, "empty tags should not be possible"
try:
handle = tags.get_handle().upper()
except ValueError:
handle = f"*{dummy_handle:X}"
# index dummy handle by id of the first tag:
dummy_handle_index[id(tags[0])] = handle
dummy_handle += 1
next_entry = IndexEntry(tags, start_line_number)
if prev_entry is not None:
next_entry.prev = prev_entry
prev_entry.next = next_entry
entity_index[handle] = next_entry
prev_entry = next_entry
# calculate next start line number:
# add 2 lines for each tag: group code, value
start_line_number += len(tags) * 2
start_line_number += 2 # for removed ENDSEC tag
# subtract 1 and 2 for the last ENDSEC tag!
self._max_line_number = start_line_number - 3
self._index = entity_index
self._dummy_handle_index = dummy_handle_index
def __contains__(self, handle: str) -> bool:
return handle.upper() in self._index
@property
def max_line_number(self) -> int:
return self._max_line_number
def get(self, handle: str) -> Optional[Tags]:
index_entry = self._index.get(handle.upper())
if index_entry is not None:
return index_entry.tags
else:
return None
def get_handle(self, entity: Tags) -> Optional[str]:
if not len(entity):
return None
try:
return entity.get_handle()
except ValueError:
# fast retrieval of dummy handle which isn't stored in tags:
return self._dummy_handle_index.get(id(entity[0]))
def next_entity(self, entity: Tags) -> Tags:
handle = self.get_handle(entity)
if handle:
index_entry = self._index.get(handle)
next_entry = index_entry.next # type: ignore
# next of last entity is None!
if next_entry:
return next_entry.tags
return entity
def previous_entity(self, entity: Tags) -> Tags:
handle = self.get_handle(entity)
if handle:
index_entry = self._index.get(handle)
prev_entry = index_entry.prev # type: ignore
# prev of first entity is None!
if prev_entry:
return prev_entry.tags
return entity
def get_start_line_for_entity(self, entity: Tags) -> int:
handle = self.get_handle(entity)
if handle:
index_entry = self._index.get(handle)
if index_entry:
return index_entry.start_line_number
return 0
def get_entity_at_line(self, number: int) -> Optional[Tags]:
tags = None
for index_entry in self._index.values():
if index_entry.start_line_number > number:
return tags # tags of previous entry!
tags = index_entry.tags
return tags
def get_row_from_line_number(
entity: Tags, start_line_number: int, select_line_number: int
) -> int:
count = select_line_number - start_line_number
lines = 0
row = 0
for tag in entity:
if lines >= count:
return row
if isinstance(tag, DXFVertex):
lines += len(tag.value) * 2
else:
lines += 2
row += 1
return row
def dxfstr(tags: Tags) -> str:
return "".join(tag.dxfstr() for tag in tags)
class EntityHistory:
def __init__(self) -> None:
self._history: list[Tags] = list()
self._index: int = 0
self._time_travel: list[Tags] = list()
def __len__(self):
return len(self._history)
@property
def index(self):
return self._index
def clear(self):
self._history.clear()
self._time_travel.clear()
self._index = 0
def append(self, entity: Tags):
if self._time_travel:
self._history.extend(self._time_travel)
self._time_travel.clear()
count = len(self._history)
if count:
# only append if different to last entity
if self._history[-1] is entity:
return
self._index = count
self._history.append(entity)
def back(self) -> Optional[Tags]:
entity = None
if self._history:
index = self._index - 1
if index >= 0:
entity = self._time_wrap(index)
else:
entity = self._history[0]
return entity
def forward(self) -> Tags:
entity = None
history = self._history
if history:
index = self._index + 1
if index < len(history):
entity = self._time_wrap(index)
else:
entity = history[-1]
return entity # type: ignore
def _time_wrap(self, index) -> Tags:
self._index = index
entity = self._history[index]
self._time_travel.append(entity)
return entity
def content(self) -> list[Tags]:
return list(self._history)
class SearchIndex:
NOT_FOUND = None, -1
def __init__(self, entities: Iterable[Tags]):
self.entities: list[Tags] = list(entities)
self._current_entity_index: int = 0
self._current_tag_index: int = 0
self._search_term: Optional[str] = None
self._search_term_lower: Optional[str] = None
self._backward = False
self._end_of_index = not bool(self.entities)
self.case_insensitive = True
self.whole_words = False
self.numbers = False
self.regex = False # False = normal mode
@property
def is_end_of_index(self) -> bool:
return self._end_of_index
@property
def search_term(self) -> Optional[str]:
return self._search_term
def set_current_entity(self, entity: Tags, tag_index: int = 0):
self._current_tag_index = tag_index
try:
self._current_entity_index = self.entities.index(entity)
except ValueError:
self.reset_cursor()
def update_entities(self, entities: list[Tags]):
current_entity, index = self.current_entity()
self.entities = entities
if current_entity:
self.set_current_entity(current_entity, index)
def current_entity(self) -> tuple[Optional[Tags], int]:
if self.entities and not self._end_of_index:
return (
self.entities[self._current_entity_index],
self._current_tag_index,
)
return self.NOT_FOUND
def reset_cursor(self, backward: bool = False):
self._current_entity_index = 0
self._current_tag_index = 0
count = len(self.entities)
if count:
self._end_of_index = False
if backward:
self._current_entity_index = count - 1
entity = self.entities[-1]
self._current_tag_index = len(entity) - 1
else:
self._end_of_index = True
def cursor(self) -> tuple[int, int]:
return self._current_entity_index, self._current_tag_index
def move_cursor_forward(self) -> None:
if self.entities:
entity: Tags = self.entities[self._current_entity_index]
tag_index = self._current_tag_index + 1
if tag_index >= len(entity):
entity_index = self._current_entity_index + 1
if entity_index < len(self.entities):
self._current_entity_index = entity_index
self._current_tag_index = 0
else:
self._end_of_index = True
else:
self._current_tag_index = tag_index
def move_cursor_backward(self) -> None:
if self.entities:
tag_index = self._current_tag_index - 1
if tag_index < 0:
entity_index = self._current_entity_index - 1
if entity_index >= 0:
self._current_entity_index = entity_index
self._current_tag_index = (
len(self.entities[entity_index]) - 1
)
else:
self._end_of_index = True
else:
self._current_tag_index = tag_index
def reset_search_term(self, term: str) -> None:
self._search_term = str(term)
self._search_term_lower = self._search_term.lower()
def find(
self, term: str, backward: bool = False, reset_index: bool = True
) -> tuple[Optional[Tags], int]:
self.reset_search_term(term)
if reset_index:
self.reset_cursor(backward)
if len(self.entities) and not self._end_of_index:
if backward:
return self.find_backwards()
else:
return self.find_forward()
else:
return self.NOT_FOUND
def find_forward(self) -> tuple[Optional[Tags], int]:
return self._find(self.move_cursor_forward)
def find_backwards(self) -> tuple[Optional[Tags], int]:
return self._find(self.move_cursor_backward)
def _find(self, move_cursor) -> tuple[Optional[Tags], int]:
if self.entities and self._search_term and not self._end_of_index:
while not self._end_of_index:
entity, tag_index = self.current_entity()
move_cursor()
if self._match(*entity[tag_index]): # type: ignore
return entity, tag_index
return self.NOT_FOUND
def _match(self, code: int, value: Any) -> bool:
if tag_type(code) is not str:
if not self.numbers:
return False
value = str(value)
if self.case_insensitive:
search_term = self._search_term_lower
value = value.lower()
else:
search_term = self._search_term
if self.whole_words:
return any(search_term == word for word in value.split())
else:
return search_term in value
@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'find_dialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from ezdxf.addons.xqt import QtCore, QtGui, QtWidgets
class Ui_FindDialog(object):
def setupUi(self, FindDialog):
FindDialog.setObjectName("FindDialog")
FindDialog.resize(320, 376)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(FindDialog.sizePolicy().hasHeightForWidth())
FindDialog.setSizePolicy(sizePolicy)
FindDialog.setMinimumSize(QtCore.QSize(320, 376))
FindDialog.setMaximumSize(QtCore.QSize(320, 376))
FindDialog.setBaseSize(QtCore.QSize(320, 376))
self.verticalLayout_5 = QtWidgets.QVBoxLayout(FindDialog)
self.verticalLayout_5.setObjectName("verticalLayout_5")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.horizontalLayout.setObjectName("horizontalLayout")
self.label = QtWidgets.QLabel(FindDialog)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.find_text_edit = QtWidgets.QLineEdit(FindDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.find_text_edit.sizePolicy().hasHeightForWidth())
self.find_text_edit.setSizePolicy(sizePolicy)
self.find_text_edit.setMinimumSize(QtCore.QSize(0, 24))
self.find_text_edit.setMaximumSize(QtCore.QSize(16777215, 24))
self.find_text_edit.setObjectName("find_text_edit")
self.horizontalLayout.addWidget(self.find_text_edit)
self.verticalLayout.addLayout(self.horizontalLayout)
self.groupBox = QtWidgets.QGroupBox(FindDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth())
self.groupBox.setSizePolicy(sizePolicy)
self.groupBox.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.groupBox.setFlat(False)
self.groupBox.setObjectName("groupBox")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.whole_words_check_box = QtWidgets.QCheckBox(self.groupBox)
self.whole_words_check_box.setObjectName("whole_words_check_box")
self.verticalLayout_3.addWidget(self.whole_words_check_box)
self.match_case_check_box = QtWidgets.QCheckBox(self.groupBox)
self.match_case_check_box.setObjectName("match_case_check_box")
self.verticalLayout_3.addWidget(self.match_case_check_box)
self.number_tags_check_box = QtWidgets.QCheckBox(self.groupBox)
self.number_tags_check_box.setObjectName("number_tags_check_box")
self.verticalLayout_3.addWidget(self.number_tags_check_box)
self.verticalLayout.addWidget(self.groupBox, 0, QtCore.Qt.AlignTop)
self.groupBox_2 = QtWidgets.QGroupBox(FindDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.groupBox_2.sizePolicy().hasHeightForWidth())
self.groupBox_2.setSizePolicy(sizePolicy)
self.groupBox_2.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.groupBox_2.setObjectName("groupBox_2")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.groupBox_2)
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.header_check_box = QtWidgets.QCheckBox(self.groupBox_2)
self.header_check_box.setChecked(True)
self.header_check_box.setObjectName("header_check_box")
self.verticalLayout_4.addWidget(self.header_check_box)
self.classes_check_box = QtWidgets.QCheckBox(self.groupBox_2)
self.classes_check_box.setObjectName("classes_check_box")
self.verticalLayout_4.addWidget(self.classes_check_box)
self.tables_check_box = QtWidgets.QCheckBox(self.groupBox_2)
self.tables_check_box.setChecked(True)
self.tables_check_box.setObjectName("tables_check_box")
self.verticalLayout_4.addWidget(self.tables_check_box)
self.blocks_check_box = QtWidgets.QCheckBox(self.groupBox_2)
self.blocks_check_box.setChecked(True)
self.blocks_check_box.setObjectName("blocks_check_box")
self.verticalLayout_4.addWidget(self.blocks_check_box)
self.entities_check_box = QtWidgets.QCheckBox(self.groupBox_2)
self.entities_check_box.setChecked(True)
self.entities_check_box.setObjectName("entities_check_box")
self.verticalLayout_4.addWidget(self.entities_check_box)
self.objects_check_box = QtWidgets.QCheckBox(self.groupBox_2)
self.objects_check_box.setChecked(False)
self.objects_check_box.setObjectName("objects_check_box")
self.verticalLayout_4.addWidget(self.objects_check_box)
self.verticalLayout.addWidget(self.groupBox_2, 0, QtCore.Qt.AlignTop)
self.verticalLayout_5.addLayout(self.verticalLayout)
self.message = QtWidgets.QLabel(FindDialog)
self.message.setObjectName("message")
self.verticalLayout_5.addWidget(self.message)
self.buttons_layout = QtWidgets.QHBoxLayout()
self.buttons_layout.setSizeConstraint(QtWidgets.QLayout.SetNoConstraint)
self.buttons_layout.setObjectName("buttons_layout")
self.find_forward_button = QtWidgets.QPushButton(FindDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.find_forward_button.sizePolicy().hasHeightForWidth())
self.find_forward_button.setSizePolicy(sizePolicy)
self.find_forward_button.setMinimumSize(QtCore.QSize(0, 0))
self.find_forward_button.setMaximumSize(QtCore.QSize(200, 100))
self.find_forward_button.setObjectName("find_forward_button")
self.buttons_layout.addWidget(self.find_forward_button, 0, QtCore.Qt.AlignBottom)
self.find_backwards_button = QtWidgets.QPushButton(FindDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.find_backwards_button.sizePolicy().hasHeightForWidth())
self.find_backwards_button.setSizePolicy(sizePolicy)
self.find_backwards_button.setMinimumSize(QtCore.QSize(0, 0))
self.find_backwards_button.setMaximumSize(QtCore.QSize(200, 100))
self.find_backwards_button.setObjectName("find_backwards_button")
self.buttons_layout.addWidget(self.find_backwards_button, 0, QtCore.Qt.AlignBottom)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.buttons_layout.addItem(spacerItem)
self.close_button = QtWidgets.QPushButton(FindDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.close_button.sizePolicy().hasHeightForWidth())
self.close_button.setSizePolicy(sizePolicy)
self.close_button.setMinimumSize(QtCore.QSize(0, 0))
self.close_button.setMaximumSize(QtCore.QSize(200, 100))
self.close_button.setToolTip("")
self.close_button.setObjectName("close_button")
self.buttons_layout.addWidget(self.close_button, 0, QtCore.Qt.AlignRight|QtCore.Qt.AlignBottom)
self.verticalLayout_5.addLayout(self.buttons_layout)
self.retranslateUi(FindDialog)
QtCore.QMetaObject.connectSlotsByName(FindDialog)
def retranslateUi(self, FindDialog):
_translate = QtCore.QCoreApplication.translate
FindDialog.setWindowTitle(_translate("FindDialog", "Find"))
self.label.setText(_translate("FindDialog", "Find Text:"))
self.groupBox.setTitle(_translate("FindDialog", "Options"))
self.whole_words_check_box.setToolTip(_translate("FindDialog", "Search only whole words in normal mode if checked."))
self.whole_words_check_box.setText(_translate("FindDialog", "Whole Words"))
self.match_case_check_box.setToolTip(_translate("FindDialog", "Case sensitive search in normal mode if checked."))
self.match_case_check_box.setText(_translate("FindDialog", "Match Case"))
self.number_tags_check_box.setToolTip(_translate("FindDialog", "Ignore numeric DXF tags if checked."))
self.number_tags_check_box.setText(_translate("FindDialog", "Search in Numeric Tags"))
self.groupBox_2.setToolTip(_translate("FindDialog", "Select sections to search in."))
self.groupBox_2.setTitle(_translate("FindDialog", "Search in Sections"))
self.header_check_box.setText(_translate("FindDialog", "HEADER"))
self.classes_check_box.setText(_translate("FindDialog", "CLASSES"))
self.tables_check_box.setText(_translate("FindDialog", "TABLES"))
self.blocks_check_box.setText(_translate("FindDialog", "BLOCKS"))
self.entities_check_box.setText(_translate("FindDialog", "ENTITIES"))
self.objects_check_box.setText(_translate("FindDialog", "OBJECTS"))
self.message.setText(_translate("FindDialog", "TextLabel"))
self.find_forward_button.setToolTip(_translate("FindDialog", "or press F3"))
self.find_forward_button.setText(_translate("FindDialog", "Find &Forward"))
self.find_backwards_button.setToolTip(_translate("FindDialog", "or press F4"))
self.find_backwards_button.setText(_translate("FindDialog", "Find &Backwards"))
self.close_button.setText(_translate("FindDialog", "Close"))
@@ -0,0 +1,36 @@
# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Union, Iterable, TYPE_CHECKING
from pathlib import Path
from ezdxf.lldxf import loader
from ezdxf.lldxf.types import DXFTag
from ezdxf.lldxf.tagger import ascii_tags_loader, binary_tags_loader
from ezdxf.lldxf.validator import is_dxf_file, is_binary_dxf_file
from ezdxf.filemanagement import dxf_file_info
if TYPE_CHECKING:
from ezdxf.eztypes import SectionDict
def load_section_dict(filename: Union[str, Path]) -> SectionDict:
tagger = get_tag_loader(filename)
return loader.load_dxf_structure(tagger)
def get_tag_loader(
filename: Union[str, Path], errors: str = "ignore"
) -> Iterable[DXFTag]:
filename = str(filename)
if is_binary_dxf_file(filename):
with open(filename, "rb") as fp:
data = fp.read()
return binary_tags_loader(data, errors=errors)
if not is_dxf_file(filename):
raise IOError(f"File '{filename}' is not a DXF file.")
info = dxf_file_info(filename)
with open(filename, mode="rt", encoding=info.encoding, errors=errors) as fp:
return list(ascii_tags_loader(fp, skip_comments=True))
@@ -0,0 +1,574 @@
# Copyright (c) 2021, Manfred Moitzi
# License: MIT License
# mypy: ignore_errors=True
from __future__ import annotations
from typing import Any, Optional
import textwrap
from ezdxf.lldxf.types import (
render_tag,
DXFVertex,
GROUP_MARKERS,
POINTER_CODES,
)
from ezdxf.addons.xqt import QModelIndex, QAbstractTableModel, Qt, QtWidgets
from ezdxf.addons.xqt import QStandardItemModel, QStandardItem, QColor
from .tags import compile_tags, Tags
__all__ = [
"DXFTagsModel",
"DXFStructureModel",
"EntityContainer",
"Entity",
"DXFTagsRole",
]
DXFTagsRole = Qt.UserRole + 1 # type: ignore
def name_fmt(handle, name: str) -> str:
if handle is None:
return name
else:
return f"<{handle}> {name}"
HEADER_LABELS = ["Group Code", "Data Type", "Content", "4", "5"]
def calc_line_numbers(start: int, tags: Tags) -> list[int]:
numbers = [start]
index = start
for tag in tags:
if isinstance(tag, DXFVertex):
index += len(tag.value) * 2
else:
index += 2
numbers.append(index)
return numbers
class DXFTagsModel(QAbstractTableModel):
def __init__(
self, tags: Tags, start_line_number: int = 1, valid_handles=None
):
super().__init__()
self._tags = compile_tags(tags)
self._line_numbers = calc_line_numbers(start_line_number, self._tags)
self._valid_handles = valid_handles or set()
palette = QtWidgets.QApplication.palette()
self._group_marker_color = palette.highlight().color()
def data(self, index: QModelIndex, role: int = ...) -> Any: # type: ignore
def is_invalid_handle(tag):
if (
tag.code in POINTER_CODES
and not tag.value.upper() in self._valid_handles
):
return True
return False
if role == Qt.DisplayRole:
tag = self._tags[index.row()]
return render_tag(tag, index.column())
elif role == Qt.ForegroundRole:
tag = self._tags[index.row()]
if tag.code in GROUP_MARKERS:
return self._group_marker_color
elif is_invalid_handle(tag):
return QColor("red")
elif role == DXFTagsRole:
return self._tags[index.row()]
elif role == Qt.ToolTipRole:
code, value = self._tags[index.row()]
if index.column() == 0: # group code column
return GROUP_CODE_TOOLTIPS_DICT.get(code)
code, value = self._tags[index.row()]
if code in POINTER_CODES:
if value.upper() in self._valid_handles:
return f"Double click to go to the referenced entity"
else:
return f"Handle does not exist"
elif code == 0:
return f"Double click to go to the DXF reference provided by Autodesk"
def headerData(
self, section: int, orientation: Qt.Orientation, role: int = ... # type: ignore
) -> Any:
if orientation == Qt.Horizontal:
if role == Qt.DisplayRole:
return HEADER_LABELS[section]
elif role == Qt.TextAlignmentRole:
return Qt.AlignLeft
elif orientation == Qt.Vertical:
if role == Qt.DisplayRole:
return self._line_numbers[section]
elif role == Qt.ToolTipRole:
return "Line number in DXF file"
def rowCount(self, parent: QModelIndex = ...) -> int: # type: ignore
return len(self._tags)
def columnCount(self, parent: QModelIndex = ...) -> int: # type: ignore
return 3
def compiled_tags(self) -> Tags:
"""Returns the compiled tags. Only points codes are compiled, group
code 10, ...
"""
return self._tags
def line_number(self, row: int) -> int:
"""Return the DXF file line number of the widget-row."""
try:
return self._line_numbers[row]
except IndexError:
return 0
class EntityContainer(QStandardItem):
def __init__(self, name: str, entities: list[Tags]):
super().__init__()
self.setEditable(False)
self.setText(name + f" ({len(entities)})")
self.setup_content(entities)
def setup_content(self, entities):
self.appendRows([Entity(e) for e in entities])
class Classes(EntityContainer):
def setup_content(self, entities):
self.appendRows([Class(e) for e in entities])
class AcDsData(EntityContainer):
def setup_content(self, entities):
self.appendRows([AcDsEntry(e) for e in entities])
class NamedEntityContainer(EntityContainer):
def setup_content(self, entities):
self.appendRows([NamedEntity(e) for e in entities])
class Tables(EntityContainer):
def setup_content(self, entities):
container = []
name = ""
for e in entities:
container.append(e)
dxftype = e.dxftype()
if dxftype == "TABLE":
try:
handle = e.get_handle()
except ValueError:
handle = None
name = e.get_first_value(2, default="UNDEFINED")
name = name_fmt(handle, name)
elif dxftype == "ENDTAB":
if container:
container.pop() # remove ENDTAB
self.appendRow(NamedEntityContainer(name, container))
container.clear()
class Blocks(EntityContainer):
def setup_content(self, entities):
container = []
name = "UNDEFINED"
for e in entities:
container.append(e)
dxftype = e.dxftype()
if dxftype == "BLOCK":
try:
handle = e.get_handle()
except ValueError:
handle = None
name = e.get_first_value(2, default="UNDEFINED")
name = name_fmt(handle, name)
elif dxftype == "ENDBLK":
if container:
self.appendRow(EntityContainer(name, container))
container.clear()
def get_section_name(section: list[Tags]) -> str:
if len(section) > 0:
header = section[0]
if len(header) > 1 and header[0].code == 0 and header[1].code == 2:
return header[1].value
return "INVALID SECTION HEADER!"
class Entity(QStandardItem):
def __init__(self, tags: Tags):
super().__init__()
self.setEditable(False)
self._tags = tags
self._handle: Optional[str]
try:
self._handle = tags.get_handle()
except ValueError:
self._handle = None
self.setText(self.entity_name())
def entity_name(self):
name = "INVALID ENTITY!"
tags = self._tags
if tags and tags[0].code == 0:
name = name_fmt(self._handle, tags[0].value)
return name
def data(self, role: int = ...) -> Any: # type: ignore
if role == DXFTagsRole:
return self._tags
else:
return super().data(role)
class Header(Entity):
def entity_name(self):
return "HEADER"
class ThumbnailImage(Entity):
def entity_name(self):
return "THUMBNAILIMAGE"
class NamedEntity(Entity):
def entity_name(self):
name = self._tags.get_first_value(2, "<noname>")
return name_fmt(str(self._handle), name)
class Class(Entity):
def entity_name(self):
tags = self._tags
name = "INVALID CLASS!"
if len(tags) > 1 and tags[0].code == 0 and tags[1].code == 1:
name = tags[1].value
return name
class AcDsEntry(Entity):
def entity_name(self):
return self._tags[0].value
class DXFStructureModel(QStandardItemModel):
def __init__(self, filename: str, doc):
super().__init__()
root = QStandardItem(filename)
root.setEditable(False)
self.appendRow(root)
row: Any
for section in doc.sections.values():
name = get_section_name(section)
if name == "HEADER":
row = Header(section[0])
elif name == "THUMBNAILIMAGE":
row = ThumbnailImage(section[0])
elif name == "CLASSES":
row = Classes(name, section[1:])
elif name == "TABLES":
row = Tables(name, section[1:])
elif name == "BLOCKS":
row = Blocks(name, section[1:])
elif name == "ACDSDATA":
row = AcDsData(name, section[1:])
else:
row = EntityContainer(name, section[1:])
root.appendRow(row)
def index_of_entity(self, entity: Tags) -> QModelIndex:
root = self.item(0, 0)
index = find_index(root, entity)
if index is None:
return root.index()
else:
return index
def find_index(item: QStandardItem, entity: Tags) -> Optional[QModelIndex]:
def _find(sub_item: QStandardItem):
for index in range(sub_item.rowCount()):
child = sub_item.child(index, 0)
tags = child.data(DXFTagsRole)
if tags and tags is entity:
return child.index()
if child.rowCount() > 0:
index2 = _find(child)
if index2 is not None:
return index2
return None
return _find(item)
GROUP_CODE_TOOLTIPS = [
(0, "Text string indicating the entity type (fixed)"),
(1, "Primary text value for an entity"),
(2, "Name (attribute tag, block name, and so on)"),
((3, 4), "Other text or name values"),
(5, "Entity handle; text string of up to 16 hexadecimal digits (fixed)"),
(6, "Linetype name (fixed)"),
(7, "Text style name (fixed)"),
(8, "Layer name (fixed)"),
(
9,
"DXF: variable name identifier (used only in HEADER section of the DXF file)",
),
(
10,
"Primary point; this is the start point of a line or text entity, center "
"of a circle, and so on DXF: X value of the primary point (followed by Y "
"and Z value codes 20 and 30) APP: 3D point (list of three reals)",
),
(
(11, 18),
"Other points DXF: X value of other points (followed by Y value codes "
"21-28 and Z value codes 31-38) APP: 3D point (list of three reals)",
),
(20, "DXF: Y value of the primary point"),
(30, "DXF: Z value of the primary point"),
((21, 28), "DXF: Y values of other points"),
((31, 37), "DXF: Z values of other points"),
(38, "DXF: entity's elevation if nonzero"),
(39, "Entity's thickness if nonzero (fixed)"),
(
(40, 47),
"Double-precision floating-point values (text height, scale factors, and so on)",
),
(48, "Linetype scale; default value is defined for all entity types"),
(
49,
"Multiple 49 groups may appear in one entity for variable-length tables "
"(such as the dash lengths in the LTYPE table). A 7x group always appears "
"before the first 49 group to specify the table length",
),
(
(50, 58),
"Angles (output in degrees to DXF files and radians through AutoLISP and ObjectARX applications)",
),
(
60,
"Entity visibility; absence or 0 indicates visibility; 1 indicates invisibility",
),
(62, "Color number (fixed)"),
(66, "Entities follow flag (fixed)"),
(67, "0 for model space or 1 for paper space (fixed)"),
(
68,
"APP: identifies whether viewport is on but fully off screen; is not active or is off",
),
(69, "APP: viewport identification number"),
((70, 79), "Integer values, such as repeat counts, flag bits, or modes"),
((90, 99), "32-bit integer values"),
(
100,
"Subclass data marker (with derived class name as a string). "
"Required for all objects and entity classes that are derived from "
"another concrete class. The subclass data marker segregates data defined by different "
"classes in the inheritance chain for the same object. This is in addition "
"to the requirement for DXF names for each distinct concrete class derived "
"from ObjectARX (see Subclass Markers)",
),
(101, "Embedded object marker"),
(
102,
"Control string, followed by '{arbitrary name' or '}'. Similar to the "
"xdata 1002 group code, except that when the string begins with '{', it "
"can be followed by an arbitrary string whose interpretation is up to the "
"application. The only other control string allowed is '}' as a group "
"terminator. AutoCAD does not interpret these strings except during d"
"rawing audit operations. They are for application use.",
),
(105, "Object handle for DIMVAR symbol table entry"),
(
110,
"UCS origin (appears only if code 72 is set to 1); DXF: X value; APP: 3D point",
),
(
111,
"UCS Y-axis (appears only if code 72 is set to 1); DXF: Y value; APP: 3D vector",
),
(
112,
"UCS Z-axis (appears only if code 72 is set to 1); DXF: Z value; APP: 3D vector",
),
((120, 122), "DXF: Y value of UCS origin, UCS X-axis, and UCS Y-axis"),
((130, 132), "DXF: Z value of UCS origin, UCS X-axis, and UCS Y-axis"),
(
(140, 149),
"Double-precision floating-point values (points, elevation, and DIMSTYLE settings, for example)",
),
(
(170, 179),
"16-bit integer values, such as flag bits representing DIMSTYLE settings",
),
(
210,
"Extrusion direction (fixed) "
+ "DXF: X value of extrusion direction "
+ "APP: 3D extrusion direction vector",
),
(220, "DXF: Y value of the extrusion direction"),
(230, "DXF: Z value of the extrusion direction"),
((270, 279), "16-bit integer values"),
((280, 289), "16-bit integer value"),
((290, 299), "Boolean flag value; 0 = False; 1 = True"),
((300, 309), "Arbitrary text strings"),
(
(310, 319),
"Arbitrary binary chunks with same representation and limits as 1004 "
"group codes: hexadecimal strings of up to 254 characters represent data "
"chunks of up to 127 bytes",
),
(
(320, 329),
"Arbitrary object handles; handle values that are taken 'as is'. They "
"are not translated during INSERT and XREF operations",
),
(
(330, 339),
"Soft-pointer handle; arbitrary soft pointers to other objects within "
"same DXF file or drawing. Translated during INSERT and XREF operations",
),
(
(340, 349),
"Hard-pointer handle; arbitrary hard pointers to other objects within "
"same DXF file or drawing. Translated during INSERT and XREF operations",
),
(
(350, 359),
"Soft-owner handle; arbitrary soft ownership links to other objects "
"within same DXF file or drawing. Translated during INSERT and XREF "
"operations",
),
(
(360, 369),
"Hard-owner handle; arbitrary hard ownership links to other objects within "
"same DXF file or drawing. Translated during INSERT and XREF operations",
),
(
(370, 379),
"Lineweight enum value (AcDb::LineWeight). Stored and moved around as a 16-bit integer. "
"Custom non-entity objects may use the full range, but entity classes only use 371-379 DXF "
"group codes in their representation, because AutoCAD and AutoLISP both always assume a 370 "
"group code is the entity's lineweight. This allows 370 to behave like other 'common' entity fields",
),
(
(380, 389),
"PlotStyleName type enum (AcDb::PlotStyleNameType). Stored and moved around as a 16-bit integer. "
"Custom non-entity objects may use the full range, but entity classes only use 381-389 "
"DXF group codes in their representation, for the same reason as the lineweight range",
),
(
(390, 399),
"String representing handle value of the PlotStyleName object, basically a hard pointer, but has "
"a different range to make backward compatibility easier to deal with. Stored and moved around "
"as an object ID (a handle in DXF files) and a special type in AutoLISP. Custom non-entity objects "
"may use the full range, but entity classes only use 391-399 DXF group codes in their representation, "
"for the same reason as the lineweight range",
),
((400, 409), "16-bit integers"),
((410, 419), "String"),
(
(420, 427),
"32-bit integer value. When used with True Color; a 32-bit integer representing a 24-bit color value. "
"The high-order byte (8 bits) is 0, the low-order byte an unsigned char holding the Blue value (0-255), "
"then the Green value, and the next-to-high order byte is the Red Value. Converting this integer value to "
"hexadecimal yields the following bit mask: 0x00RRGGBB. "
"For example, a true color with Red==200, Green==100 and Blue==50 is 0x00C86432, and in DXF, in decimal, 13132850",
),
(
(430, 437),
"String; when used for True Color, a string representing the name of the color",
),
(
(440, 447),
"32-bit integer value. When used for True Color, the transparency value",
),
((450, 459), "Long"),
((460, 469), "Double-precision floating-point value"),
((470, 479), "String"),
(
(480, 481),
"Hard-pointer handle; arbitrary hard pointers to other objects within same DXF file or drawing. "
"Translated during INSERT and XREF operations",
),
(
999,
"DXF: The 999 group code indicates that the line following it is a comment string. SAVEAS does "
"not include such groups in a DXF output file, but OPEN honors them and ignores the comments. "
"You can use the 999 group to include comments in a DXF file that you have edited",
),
(1000, "ASCII string (up to 255 bytes long) in extended data"),
(
1001,
"Registered application name (ASCII string up to 31 bytes long) for extended data",
),
(1002, "Extended data control string ('{' or '}')"),
(1003, "Extended data layer name"),
(1004, "Chunk of bytes (up to 127 bytes long) in extended data"),
(
1005,
"Entity handle in extended data; text string of up to 16 hexadecimal digits",
),
(
1010,
"A point in extended data; DXF: X value (followed by 1020 and 1030 groups); APP: 3D point",
),
(1020, "DXF: Y values of a point"),
(1030, "DXF: Z values of a point"),
(
1011,
"A 3D world space position in extended data "
"DXF: X value (followed by 1021 and 1031 groups) "
"APP: 3D point",
),
(1021, "DXF: Y value of a world space position"),
(1031, "DXF: Z value of a world space position"),
(
1012,
"A 3D world space displacement in extended data "
"DXF: X value (followed by 1022 and 1032 groups) "
"APP: 3D vector",
),
(1022, "DXF: Y value of a world space displacement"),
(1032, "DXF: Z value of a world space displacement"),
(
1013,
"A 3D world space direction in extended data "
"DXF: X value (followed by 1022 and 1032 groups) "
"APP: 3D vector",
),
(1023, "DXF: Y value of a world space direction"),
(1033, "DXF: Z value of a world space direction"),
(1040, "Extended data double-precision floating-point value"),
(1041, "Extended data distance value"),
(1042, "Extended data scale factor"),
(1070, "Extended data 16-bit signed integer"),
(1071, "Extended data 32-bit signed long"),
]
def build_group_code_tooltip_dict() -> dict[int, str]:
tooltips = dict()
for code, tooltip in GROUP_CODE_TOOLTIPS:
tooltip = "\n".join(textwrap.wrap(tooltip, width=80))
if isinstance(code, int):
tooltips[code] = tooltip
elif isinstance(code, tuple):
s, e = code
for group_code in range(s, e + 1):
tooltips[group_code] = tooltip
else:
raise ValueError(type(code))
return tooltips
GROUP_CODE_TOOLTIPS_DICT = build_group_code_tooltip_dict()
@@ -0,0 +1,117 @@
# Autodesk DXF 2018 Reference
link_tpl = "https://help.autodesk.com/view/OARX/2018/ENU/?guid={guid}"
# Autodesk DXF 2014 Reference
# link_tpl = 'https://docs.autodesk.com/ACD/2014/ENU/files/{guid}.htm'
main_index_guid = "GUID-235B22E0-A567-4CF6-92D3-38A2306D73F3" # main index
reference_guids = {
"HEADER": "GUID-EA9CDD11-19D1-4EBC-9F56-979ACF679E3C",
"CLASSES": "GUID-6160F1F1-2805-4C69-8077-CA1AEB6B1005",
"TABLES": "GUID-A9FD9590-C97B-4E41-9F26-BD82C34A4F9F",
"BLOCKS": "GUID-1D14A213-5E4D-4EA6-A6B5-8709EB925D01",
"ENTITIES": "GUID-7D07C886-FD1D-4A0C-A7AB-B4D21F18E484",
"OBJECTS": "GUID-2D71EE99-A6BE-4060-9B43-808CF1E201C6",
"THUMBNAILIMAGE": "GUID-792F79DC-0D5D-43B5-AB0E-212E0EDF6BAE",
"APPIDS": "GUID-6E3140E9-E560-4C77-904E-480382F0553E",
"APPID": "GUID-6E3140E9-E560-4C77-904E-480382F0553E",
"BLOCK_RECORDS": "GUID-A1FD1934-7EF5-4D35-A4B0-F8AE54A9A20A",
"BLOCK_RECORD": "GUID-A1FD1934-7EF5-4D35-A4B0-F8AE54A9A20A",
"DIMSTYLES": "GUID-F2FAD36F-0CE3-4943-9DAD-A9BCD2AE81DA",
"DIMSTYLE": "GUID-F2FAD36F-0CE3-4943-9DAD-A9BCD2AE81DA",
"LAYERS": "GUID-D94802B0-8BE8-4AC9-8054-17197688AFDB",
"LAYER": "GUID-D94802B0-8BE8-4AC9-8054-17197688AFDB",
"LINETYPES": "GUID-F57A316C-94A2-416C-8280-191E34B182AC",
"LTYPE": "GUID-F57A316C-94A2-416C-8280-191E34B182AC",
"STYLES": "GUID-EF68AF7C-13EF-45A1-8175-ED6CE66C8FC9",
"STYLE": "GUID-EF68AF7C-13EF-45A1-8175-ED6CE66C8FC9",
"UCS": "GUID-1906E8A7-3393-4BF9-BD27-F9AE4352FB8B",
"VIEWS": "GUID-CF3094AB-ECA9-43C1-8075-7791AC84F97C",
"VIEW": "GUID-CF3094AB-ECA9-43C1-8075-7791AC84F97C",
"VIEWPORTS": "GUID-8CE7CC87-27BD-4490-89DA-C21F516415A9",
"VPORT": "GUID-8CE7CC87-27BD-4490-89DA-C21F516415A9",
"BLOCK": "GUID-66D32572-005A-4E23-8B8B-8726E8C14302",
"ENDBLK": "GUID-27F7CC8A-E340-4C7F-A77F-5AF139AD502D",
"3DFACE": "GUID-747865D5-51F0-45F2-BEFE-9572DBC5B151",
"3DSOLID": "GUID-19AB1C40-0BE0-4F32-BCAB-04B37044A0D3",
"ACAD_PROXY_ENTITY": "GUID-89A690F9-E859-4D57-89EA-750F3FB76C6B",
"ARC": "GUID-0B14D8F1-0EBA-44BF-9108-57D8CE614BC8",
"ATTDEF": "GUID-F0EA099B-6F88-4BCC-BEC7-247BA64838A4",
"ATTRIB": "GUID-7DD8B495-C3F8-48CD-A766-14F9D7D0DD9B",
"BODY": "GUID-7FB91514-56FF-4487-850E-CF1047999E77",
"CIRCLE": "GUID-8663262B-222C-414D-B133-4A8506A27C18",
"DIMENSION": "GUID-239A1BDD-7459-4BB9-8DD7-08EC79BF1EB0",
"ELLIPSE": "GUID-107CB04F-AD4D-4D2F-8EC9-AC90888063AB",
"HATCH": "GUID-C6C71CED-CE0F-4184-82A5-07AD6241F15B",
"HELIX": "GUID-76DB3ABF-3C8C-47D1-8AFB-72942D9AE1FF",
"IMAGE": "GUID-3A2FF847-BE14-4AC5-9BD4-BD3DCAEF2281",
"INSERT": "GUID-28FA4CFB-9D5E-4880-9F11-36C97578252F",
"LEADER": "GUID-396B2369-F89F-47D7-8223-8B7FB794F9F3",
"LIGHT": "GUID-1A23DB42-6A92-48E9-9EB2-A7856A479930",
"LINE": "GUID-FCEF5726-53AE-4C43-B4EA-C84EB8686A66",
"LWPOLYLINE": "GUID-748FC305-F3F2-4F74-825A-61F04D757A50",
"MESH": "GUID-4B9ADA67-87C8-4673-A579-6E4C76FF7025",
"MLINE": "GUID-590E8AE3-C6D9-4641-8485-D7B3693E432C",
"MLEADERSTYLE": "GUID-0E489B69-17A4-4439-8505-9DCE032100B4",
"MLEADER": "GUID-72D20B8C-0F5E-4993-BEB7-0FCF94F32BE0",
"MULTILEADER": "GUID-72D20B8C-0F5E-4993-BEB7-0FCF94F32BE0",
"MTEXT": "GUID-5E5DB93B-F8D3-4433-ADF7-E92E250D2BAB",
"OLEFRAME": "GUID-4A10EF68-35A3-4961-8B15-1222ECE5E8C6",
"OLE2FRAME": "GUID-77747CE6-82C6-4452-97ED-4CEEB38BE960",
"POINT": "GUID-9C6AD32D-769D-4213-85A4-CA9CCB5C5317",
"POLYLINE": "GUID-ABF6B778-BE20-4B49-9B58-A94E64CEFFF3",
"RAY": "GUID-638B9F01-5D86-408E-A2DE-FA5D6ADBD415",
"REGION": "GUID-644BF0F0-FD79-4C5E-AD5A-0053FCC5A5A4",
"SECTION": "GUID-8B60CBAB-B226-4A5F-ABB1-46FD8AABB928",
"SEQEND": "GUID-FD4FAA74-1F6D-45F6-B132-BF0C4BE6CC3B",
"SHAPE": "GUID-0988D755-9AAB-4D6C-8E26-EC636F507F2C",
"SOLID": "GUID-E0C5F04E-D0C5-48F5-AC09-32733E8848F2",
"SPLINE": "GUID-E1F884F8-AA90-4864-A215-3182D47A9C74",
"SUN": "GUID-BB191D89-9302-45E4-9904-108AB418FAE1",
"SURFACE": "GUID-BB62483A-89C3-47C4-80E5-EA3F08979863",
"TABLE": "GUID-D8CCD2F0-18A3-42BB-A64D-539114A07DA0",
"TEXT": "GUID-62E5383D-8A14-47B4-BFC4-35824CAE8363",
"TOLERANCE": "GUID-ADFCED35-B312-4996-B4C1-61C53757B3FD",
"TRACE": "GUID-EA6FBCA8-1AD6-4FB2-B149-770313E93511",
"UNDERLAY": "GUID-3EC8FBCC-A85A-4B0B-93CD-C6C785959077",
"VERTEX": "GUID-0741E831-599E-4CBF-91E1-8ADBCFD6556D",
"VIEWPORT": "GUID-2602B0FB-02E4-4B9A-B03C-B1D904753D34",
"WIPEOUT": "GUID-2229F9C4-3C80-4C67-9EDA-45ED684808DC",
"XLINE": "GUID-55080553-34B6-40AA-9EE2-3F3A3A2A5C0A",
"ACAD_PROXY_OBJECT": "GUID-F59F0EC3-D34D-4C1A-91AC-7FDA569EF016",
"ACDBDICTIONARYWDFLT": "GUID-A6605C05-1CF4-42A4-95EC-42190B2424EE",
"ACDBPLACEHOLDER": "GUID-3BC75FF1-6139-49F4-AEBB-AE2AB4F437E4",
"DATATABLE": "GUID-D09D0650-B926-40DD-A2F2-4FD5BDDFC330",
"DICTIONARY": "GUID-40B92C63-26F0-485B-A9C2-B349099B26D0",
"DICTIONARYVAR": "GUID-D305303C-F9CE-4714-9C92-607BFDA891B4",
"DIMASSOC": "GUID-C0B96256-A911-4B4D-85E6-EB4AF2C91E27",
"FIELD": "GUID-51B921F2-16CA-4948-AC75-196198DD1796",
"GEODATA": "GUID-104FE0E2-4801-4AC8-B92C-1DDF5AC7AB64",
"GROUP": "GUID-5F1372C4-37C8-4056-9303-EE1715F58E67",
"IDBUFFER": "GUID-7A243F2B-72D8-4C48-A29A-3F251B86D03F",
"IMAGEDEF": "GUID-EFE5319F-A71A-4612-9431-42B6C7C3941F",
"IMAGEDEF_REACTOR": "GUID-46C12333-1EDA-4619-B2C9-D7D2607110C8",
"LAYER_INDEX": "GUID-17560B05-31B9-44A5-BA92-E92C799398C0",
"LAYER_FILTER": "GUID-3B44DCFD-FA96-482B-8468-37B3C5B5F289",
"LAYOUT": "GUID-433D25BF-655D-4697-834E-C666EDFD956D",
"LIGHTLIST": "GUID-C4E7FFF8-C3ED-43DD-854D-304F87FFCF06",
"MATERIAL": "GUID-E540C5BB-E166-44FA-B36C-5C739878B272",
"MLINESTYLE": "GUID-3EC12E5B-F5F6-484D-880F-D69EBE186D79",
"OBJECT_PTR": "GUID-6D6885E2-281C-410A-92FB-8F6A7F54C9DF",
"PLOTSETTINGS": "GUID-1113675E-AB07-4567-801A-310CDE0D56E9",
"RASTERVARIABLES": "GUID-DDCC21A4-822A-469B-9954-1E1EC4F6DF82",
"SPATIAL_INDEX": "GUID-CD1E44DA-CDBA-4AA7-B08E-C53F71648984",
"SPATIAL_FILTER": "GUID-34F179D8-2030-47E4-8D49-F87B6538A05A",
"SORTENTSTABLE": "GUID-462F4378-F850-4E89-90F2-3C1880F55779",
"SUNSTUDY": "GUID-1C7C073F-4CFD-4939-97D9-7AB0C1E163A3",
"TABLESTYLE": "GUID-0DBCA057-9F6C-4DEB-A66F-8A9B3C62FB1A",
"UNDERLAYDEFINITION": "GUID-A4FF15D3-F745-4E1F-94D4-1DC3DF297B0F",
"VISUALSTYLE": "GUID-8A8BF2C4-FC56-44EC-A8C4-A60CE33A530C",
"VBA_PROJECT": "GUID-F247DB75-5C4D-4944-8C20-1567480221F4",
"WIPEOUTVARIABLES": "GUID-CD28B95F-483C-4080-82A6-420606F88356",
"XRECORD": "GUID-24668FAF-AE03-41AE-AFA4-276C3692827F",
}
def get_reference_link(name: str) -> str:
guid = reference_guids.get(name, main_index_guid)
return link_tpl.format(guid=guid)
@@ -0,0 +1,71 @@
# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from typing import Iterable, Sequence
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.types import (
DXFTag,
DXFVertex,
POINT_CODES,
TYPE_TABLE,
)
def tag_compiler(tags: Tags) -> Iterable[DXFTag]:
"""Special tag compiler for the DXF browser.
This compiler should never fail and always return printable tags:
- invalid point coordinates are returned as float("nan")
- invalid ints are returned as string
- invalid floats are returned as string
"""
def to_float(v: str) -> float:
try:
return float(v)
except ValueError:
return float("NaN")
count = len(tags)
index = 0
while index < count:
code, value = tags[index]
if code in POINT_CODES:
try:
y_code, y_value = tags[index + 1]
except IndexError: # x-coord as last tag
yield DXFTag(code, to_float(value))
return
if y_code != code + 10: # not an y-coord?
yield DXFTag(code, to_float(value)) # x-coord as single tag
index += 1
continue
try:
z_code, z_value = tags[index + 2]
except IndexError: # no z-coord exist
z_code = 0
z_value = 0
point: Sequence[float]
if z_code == code + 20: # is a z-coord?
point = (to_float(value), to_float(y_value), to_float(z_value))
index += 3
else: # a valid 2d point(x, y)
point = (to_float(value), to_float(y_value))
index += 2
yield DXFVertex(code, point)
else: # a single tag
try:
if code == 0:
value = value.strip()
yield DXFTag(code, TYPE_TABLE.get(code, str)(value))
except ValueError:
yield DXFTag(code, str(value)) # just as string
index += 1
def compile_tags(tags: Tags) -> Tags:
return Tags(tag_compiler(tags))
@@ -0,0 +1,41 @@
# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from ezdxf.addons.xqt import QTableView, QTreeView, QModelIndex
from ezdxf.lldxf.tags import Tags
class StructureTree(QTreeView):
def set_structure(self, model):
self.setModel(model)
self.expand(model.index(0, 0, QModelIndex()))
self.setHeaderHidden(True)
def expand_to_entity(self, entity: Tags):
model = self.model()
index = model.index_of_entity(entity) # type: ignore
self.setCurrentIndex(index)
class DXFTagsTable(QTableView):
def __init__(self):
super().__init__()
col_header = self.horizontalHeader()
col_header.setStretchLastSection(True)
row_header = self.verticalHeader()
row_header.setDefaultSectionSize(24) # default row height in pixels
self.setSelectionBehavior(QTableView.SelectRows)
def first_selected_row(self) -> int:
first_row: int = 0
selection = self.selectedIndexes()
if selection:
first_row = selection[0].row()
return first_row
def selected_rows(self) -> list[int]:
rows: set[int] = set()
selection = self.selectedIndexes()
for item in selection:
rows.add(item.row())
return sorted(rows)