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

429 lines
14 KiB
Python

# 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