refactor: excel parse
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user