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

176 lines
5.0 KiB
Python

# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional
import ezdxf
from ezdxf import disassemble
from ezdxf.math import BoundingBox
if TYPE_CHECKING:
from ezdxf.entities import DXFEntity
MAX_FLATTENING_DISTANCE = disassemble.Primitive.max_flattening_distance
class Cache:
"""Caching object for :class:`ezdxf.math.BoundingBox` objects.
Args:
uuid: use UUIDs for virtual entities
"""
def __init__(self, uuid=False) -> None:
self._boxes: dict[str, BoundingBox] = dict()
self._use_uuid = bool(uuid)
self.hits: int = 0
self.misses: int = 0
@property
def has_data(self) -> bool:
return bool(self._boxes)
def __str__(self):
return (
f"Cache(n={len(self._boxes)}, "
f"hits={self.hits}, "
f"misses={self.misses})"
)
def get(self, entity: DXFEntity) -> Optional[BoundingBox]:
assert entity is not None
key = self._get_key(entity)
if key is None:
self.misses += 1
return None
box = self._boxes.get(key)
if box is None:
self.misses += 1
else:
self.hits += 1
return box
def store(self, entity: DXFEntity, box: BoundingBox) -> None:
assert entity is not None
key = self._get_key(entity)
if key is None:
return
self._boxes[key] = box
def invalidate(self, entities: Iterable[DXFEntity]) -> None:
"""Invalidate cache entries for the given DXF `entities`.
If entities are changed by the user, it is possible to invalidate
individual entities. Use with care - discarding the whole cache is
the safer workflow.
Ignores entities which are not stored in cache.
"""
for entity in entities:
try:
del self._boxes[self._get_key(entity)] # type: ignore
except KeyError:
pass
def _get_key(self, entity: DXFEntity) -> Optional[str]:
if entity.dxftype() == "HATCH":
# Special treatment for multiple primitives for the same
# HATCH entity - all have the same handle:
# Do not store boundary path they are not distinguishable,
# which boundary path should be returned for the handle?
return None
key = entity.dxf.handle
if key is None or key == "0":
return str(entity.uuid) if self._use_uuid else None
else:
return key
def multi_recursive(
entities: Iterable[DXFEntity],
*,
fast=False,
cache: Optional[Cache] = None,
) -> Iterable[BoundingBox]:
"""Yields all bounding boxes for the given `entities` **or** all bounding
boxes for their sub entities. If an entity (INSERT) has sub entities, only
the bounding boxes of these sub entities will be yielded, **not** the
bounding box of the entity (INSERT) itself.
If argument `fast` is ``True`` the calculation of Bézier curves is based on
their control points, this may return a slightly larger bounding box.
"""
flat_entities = disassemble.recursive_decompose(entities)
primitives = disassemble.to_primitives(flat_entities)
for primitive in primitives:
if primitive.is_empty:
continue
entity = primitive.entity
if cache is not None:
box = cache.get(entity)
if box is None:
box = primitive.bbox(fast=fast)
if box.has_data:
cache.store(entity, box)
else:
box = primitive.bbox(fast=fast)
if box.has_data:
yield box
def extents(
entities: Iterable[DXFEntity],
*,
fast=False,
cache: Optional[Cache] = None,
) -> BoundingBox:
"""Returns a single bounding box for all given `entities`.
If argument `fast` is ``True`` the calculation of Bézier curves is based on
their control points, this may return a slightly larger bounding box.
"""
_extends = BoundingBox()
for box in multi_flat(entities, fast=fast, cache=cache):
_extends.extend(box)
return _extends
def multi_flat(
entities: Iterable[DXFEntity],
*,
fast=False,
cache: Optional[Cache] = None,
) -> Iterable[BoundingBox]:
"""Yields a bounding box for each of the given `entities`.
If argument `fast` is ``True`` the calculation of Bézier curves is based on
their control points, this may return a slightly larger bounding box.
"""
def extends_(entities_: Iterable[DXFEntity]) -> BoundingBox:
_extends = BoundingBox()
for _box in multi_recursive(entities_, fast=fast, cache=cache):
_extends.extend(_box)
return _extends
for entity in entities:
box = None
if cache:
box = cache.get(entity)
if box is None:
box = extends_([entity])
if cache:
cache.store(entity, box)
if box.has_data:
yield box