116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
# Copyright (c) 2024, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import Sequence, TYPE_CHECKING, Iterable, Any
|
|
import math
|
|
from ezdxf.math import UVec, Vec2, has_clockwise_orientation
|
|
from ezdxf.entities import LWPolyline, DXFEntity
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.layouts import BaseLayout
|
|
|
|
|
|
REVCLOUD_PROPS = "RevcloudProps"
|
|
REQUIRED_BULGE = 0.520567050552
|
|
|
|
|
|
def points(
|
|
vertices: Iterable[UVec],
|
|
segment_length: float,
|
|
*,
|
|
bulge: float = REQUIRED_BULGE, # required to be recognized as REVCLOUD
|
|
start_width: float = 0.0,
|
|
end_width: float = 0.0,
|
|
) -> list[Sequence[float]]:
|
|
"""Returns the points for a :class:`~ezdxf.entities.LWPolyline` entity to render a
|
|
revision cloud, similar to the REVCLOUD command in CAD applications.
|
|
|
|
Args:
|
|
vertices: corner points of a polygon
|
|
segment_length: approximate segment length
|
|
bulge: LWPOLYLINE bulge value
|
|
start_width: start width of the segment arc
|
|
end_width: end width of the segment arc, CAD applications use 0.1 * segment_length
|
|
for a calligraphy effect
|
|
|
|
"""
|
|
if segment_length < 1e-6:
|
|
raise ValueError("segment length too small.")
|
|
_vertices = _to_vec2_list(vertices)
|
|
lw_points: list[Sequence[float]] = []
|
|
|
|
for s, e in zip(_vertices, _vertices[1:]):
|
|
lw_points.append((s.x, s.y, start_width, end_width, bulge))
|
|
diff = e - s
|
|
length = diff.magnitude
|
|
if length <= segment_length:
|
|
continue
|
|
|
|
count = math.ceil(length / segment_length)
|
|
_segment_length = length / count
|
|
offset = diff.normalize(_segment_length)
|
|
for _ in range(count - 1):
|
|
s += offset
|
|
lw_points.append((s.x, s.y, start_width, end_width, bulge))
|
|
return lw_points
|
|
|
|
|
|
def add_entity(
|
|
layout: BaseLayout,
|
|
vertices: Iterable[UVec],
|
|
segment_length: float,
|
|
*,
|
|
calligraphy=True,
|
|
dxfattribs: Any = None,
|
|
) -> LWPolyline:
|
|
"""Adds a revision cloud as :class:`~ezdxf.entities.LWPolyline` entity to `layout`,
|
|
similar to the REVCLOUD command in CAD applications.
|
|
|
|
Args:
|
|
layout: target layout
|
|
vertices: corner points of a polygon
|
|
segment_length: approximate segment length
|
|
calligraphy: ``True`` for a calligraphy effect
|
|
dxfattribs: additional DXF attributes
|
|
|
|
"""
|
|
_vertices = _to_vec2_list(vertices)
|
|
bulge = REQUIRED_BULGE
|
|
if has_clockwise_orientation(_vertices):
|
|
bulge = -bulge
|
|
|
|
end_width = segment_length * 0.1 if calligraphy else 0.0
|
|
lw_points = points(_vertices, segment_length, bulge=bulge, end_width=end_width)
|
|
lwp = layout.add_lwpolyline(lw_points, close=True, dxfattribs=dxfattribs)
|
|
lwp.set_xdata(REVCLOUD_PROPS, [(1070, 0), (1040, segment_length)])
|
|
doc = layout.doc
|
|
if doc is not None and not doc.appids.has_entry(REVCLOUD_PROPS):
|
|
doc.appids.add(REVCLOUD_PROPS)
|
|
return lwp
|
|
|
|
|
|
def is_revcloud(entity: DXFEntity) -> bool:
|
|
"""Returns ``True`` when the given entity represents a revision cloud."""
|
|
if not isinstance(entity, LWPolyline):
|
|
return False
|
|
lwpolyline: LWPolyline = entity
|
|
if not lwpolyline.is_closed:
|
|
return False
|
|
if not lwpolyline.has_xdata(REVCLOUD_PROPS):
|
|
return False
|
|
return all(
|
|
abs(REQUIRED_BULGE - abs(p[0])) < 0.02
|
|
for p in lwpolyline.get_points(format="b")
|
|
)
|
|
|
|
|
|
def _to_vec2_list(vertices: Iterable[UVec]) -> list[Vec2]:
|
|
_vertices: list[Vec2] = Vec2.list(vertices)
|
|
if len(_vertices) < 3:
|
|
raise ValueError("3 or more points required.")
|
|
if not _vertices[0].isclose(_vertices[-1]):
|
|
_vertices.append(_vertices[0])
|
|
if len(_vertices) < 4:
|
|
raise ValueError("3 or more points required.")
|
|
return _vertices
|