123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- # Created: 25.03.2011
- # Copyright (c) 2011-2018, Manfred Moitzi
- # License: MIT License
- from contextlib import contextmanager
- import array
- from ezdxf.lldxf.types import DXFTag, DXFVertex
- from ezdxf.lldxf.packedtags import VertexArray, replace_tags
- from ezdxf.lldxf import loader
- from .graphics import ExtendedTags, DXFAttr, DefSubclass, DXFAttributes, XType
- from .graphics import none_subclass, entity_subclass, ModernGraphicEntity
- from typing import TYPE_CHECKING, Tuple, Iterable, cast, Sequence, List
- if TYPE_CHECKING:
- from ezdxf.eztypes import Tags, Vertex
- LWPointType = Tuple[float, float, float, float, float]
- FORMAT_CODES = frozenset('xysebv')
- DEFAULT_FORMAT = 'xyseb'
- _LWPOLYLINE_TPL = """0
- LWPOLYLINE
- 5
- 0
- 330
- 0
- 100
- AcDbEntity
- 8
- 0
- 100
- AcDbPolyline
- 90
- 0
- 70
- 0
- 43
- 0.0
- """
- # Order doesn't matter, not valid for AutoCAD:
- # If tag 90 is not the first TAG, AutoCAD does not close the polyline when the `close` flag is set.
- lwpolyline_subclass = DefSubclass('AcDbPolyline', {
- 'count': DXFAttr(90, xtype=XType.callback, getter='__len__', setter='_set_count'),
- # always return actual length and set tag 90
- '_count': DXFAttr(90), # special attribute to update length
- 'elevation': DXFAttr(38, default=0.0),
- 'thickness': DXFAttr(39, default=0.0),
- 'flags': DXFAttr(70, default=0),
- 'const_width': DXFAttr(43, default=0.0),
- 'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
- })
- LWPOINTCODES = (10, 20, 40, 41, 42)
- class LWPolylinePoints(VertexArray):
- code = -10 # compatible with DXFTag.code
- VERTEX_CODE = 10
- START_WIDTH_CODE = 40
- END_WIDTH_CODE = 41
- BULGE_CODE = 42
- VERTEX_SIZE = 5
- __slots__ = ('value',)
- @classmethod
- def from_tags(cls, tags: ExtendedTags) -> 'LWPolylinePoints':
- """
- Setup point array from extended tags.
- Args:
- tags: ExtendedTags() object
- """
- subclass = tags.get_subclass('AcDbPolyline')
- def get_vertex() -> LWPointType:
- point.append(attribs.get(cls.START_WIDTH_CODE, 0))
- point.append(attribs.get(cls.END_WIDTH_CODE, 0))
- point.append(attribs.get(cls.BULGE_CODE, 0))
- return tuple(point)
- data = []
- point = None
- attribs = {}
- for tag in subclass:
- if tag.code in LWPOINTCODES:
- if tag.code == 10:
- if point is not None:
- data.extend(get_vertex())
- point = list(tag.value[0:2]) # just use x, y coordinates, z is invalid but you never know!
- attribs = {}
- else:
- attribs[tag.code] = tag.value
- if point is not None:
- data.extend(get_vertex())
- return cls(data=data)
- def append(self, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
- super(LWPolylinePoints, self).append(compile_array(point, format=format))
- def dxftags(self) -> Iterable[DXFTag]:
- for point in self:
- x, y, start_width, end_width, bulge = point
- yield DXFVertex(self.VERTEX_CODE, (x, y))
- if start_width:
- yield DXFTag(self.START_WIDTH_CODE, start_width)
- if end_width:
- yield DXFTag(self.END_WIDTH_CODE, end_width)
- if bulge:
- yield DXFTag(self.BULGE_CODE, bulge)
- @loader.register('LWPOLYLINE', legacy=False)
- def tag_processor(tags: ExtendedTags) -> ExtendedTags:
- points = LWPolylinePoints.from_tags(tags)
- subclass = tags.get_subclass('AcDbPolyline')
- replace_tags(subclass, codes=LWPOINTCODES, packed_data=points)
- return tags
- class LWPolyline(ModernGraphicEntity):
- __slots__ = ()
- TEMPLATE = tag_processor(ExtendedTags.from_text(_LWPOLYLINE_TPL))
- DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, lwpolyline_subclass)
- CLOSED = 1
- PLINEGEN = 128
- @property
- def AcDbPolyline(self) -> 'Tags':
- return self.tags.subclasses[2]
- @property
- def lwpoints(self) -> LWPolylinePoints:
- return cast(LWPolylinePoints, self.AcDbPolyline.get_first_tag(LWPolylinePoints.code))
- @property
- def closed(self) -> bool:
- return self.get_flag_state(self.CLOSED, name='flags')
- @closed.setter
- def closed(self, status: bool) -> None:
- self.set_flag_state(self.CLOSED, status, name='flags')
- # same as POLYLINE
- def close(self, state=True) -> None:
- self.closed = state
- def __len__(self) -> int:
- return len(self.lwpoints)
- def __iter__(self) -> Iterable[LWPointType]:
- """
- Yielding tuples of (x, y, start_width, end_width, bulge).
- """
- return iter(self.lwpoints)
- def __getitem__(self, index: int) -> LWPointType:
- """
- Returns polyline point at position index as (x, y, start_width, end_width, bulge) tuple.
- """
- return self.lwpoints[index]
- def __setitem__(self, index: int, value: Sequence[float]) -> None:
- """
- Set polyline point at position index. Point format is fixed as 'xyseb'.
- Args:
- index: point index
- value: point value as (x, y, [start_width, [end_width, [bulge]]]) tuple
- """
- self.lwpoints[index] = compile_array(value)
- def __delitem__(self, index: int) -> None:
- del self.lwpoints[index]
- self.update_count()
- def vertices(self) -> Iterable[Tuple[float, float]]:
- """
- Yields all points as (x, y) tuples.
- """
- for point in self:
- yield point[0], point[1]
- def vertices_in_wcs(self) -> Iterable['Vertex']:
- """
- Yields all points as (x, y, z) tuples in WCS.
- """
- ocs = self.ocs()
- elevation = self.get_dxf_attrib('elevation', default=0.)
- for x, y in self.vertices():
- yield ocs.to_wcs((x, y, elevation))
- def append(self, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
- """
- Append point to polyline, format specifies a user defined point format.
- Args:
- point: (x, y, [start_width, [end_width, [bulge]]]) tuple
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- """
- self.lwpoints.append(point, format=format)
- self.update_count()
- def insert(self, pos: int, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
- """
- Insert new point in front of positions pos, format specifies a user defined point format.
- Args:
- pos: insert position
- point: point data
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- """
- data = compile_array(point, format=format)
- self.lwpoints.insert(pos, data)
- self.update_count()
- def append_points(self, points: Iterable[Sequence[float]], format: str = DEFAULT_FORMAT) -> None:
- """
- Append new points to polyline, format specifies a user defined point format.
- Args:
- points: iterable of point, point is (x, y, [start_width, [end_width, [bulge]]]) tuple
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- """
- for point in points:
- self.lwpoints.append(point, format=format)
- self.update_count()
- @contextmanager
- def points(self, format: str = DEFAULT_FORMAT) -> List[Sequence[float]]:
- points = self.get_points(format=format)
- yield points
- self.set_points(points, format=format)
- def get_points(self, format: str = DEFAULT_FORMAT) -> List[Sequence[float]]:
- """
- Returns all points as list of tuples, format specifies a user defined point format.
- Args:
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- """
- return [format_point(p, format=format) for p in self.lwpoints]
- def set_points(self, points: List[Sequence[float]], format: str = DEFAULT_FORMAT) -> None:
- """
- Remove all points and append new points.
- Args:
- points: iterable of point, point is (x, y, [start_width, [end_width, [bulge]]]) tuple
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- """
- self.lwpoints.clear()
- self.append_points(points, format=format)
- def clear(self) -> None:
- self.lwpoints.clear()
- self.update_count()
- def _set_count(self, value: int) -> None:
- # use special DXF attribute, because 'count' is a callback attribute
- self.set_dxf_attrib('_count', value)
- def update_count(self):
- self._set_count(len(self.lwpoints))
- def format_point(point: Sequence[float], format: str = 'xyseb') -> Sequence[float]:
- """
- Reformat point components.
- Args:
- point: list or tuple of (x, y, start_width, end_width, bulge)
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- Returns: tuple of selected components
- """
- x, y, s, e, b = point
- v = (x, y)
- vars = locals()
- return tuple(vars[code] for code in format.lower() if code in FORMAT_CODES)
- def compile_array(data: Sequence[float], format='xyseb'):
- """
- Gather point components from input data.
- Args:
- data: list or tuple of point components
- format: format string, default is 'xyseb'
- x = x coordinate
- y = y coordinate
- s = start width
- e = end width
- b = bulge value
- v = (x, y) as tuple
- Returns: array.array('d', (x, y, start_width, end_width, bulge))
- """
- a = array.array('d', (0., 0., 0., 0., 0.))
- format = [code for code in format.lower() if code in FORMAT_CODES]
- for code, value in zip(format, data):
- if code not in FORMAT_CODES:
- continue
- if code == 'v':
- value = cast('Vertex', value)
- a[0] = value[0]
- a[1] = value[1]
- else:
- a['xyseb'.index(code)] = value
- return a
|