initial
This commit is contained in:
30
.venv/lib/python3.12/site-packages/ezdxf/render/__init__.py
Normal file
30
.venv/lib/python3.12/site-packages/ezdxf/render/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2018-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from .arrows import ARROWS
|
||||
from .r12spline import R12Spline
|
||||
from .curves import Bezier, EulerSpiral, Spline, random_2d_path, random_3d_path
|
||||
from .mesh import (
|
||||
MeshBuilder,
|
||||
MeshVertexMerger,
|
||||
MeshTransformer,
|
||||
MeshAverageVertexMerger,
|
||||
MeshDiagnose,
|
||||
FaceOrientationDetector,
|
||||
MeshBuilderError,
|
||||
NonManifoldMeshError,
|
||||
MultipleMeshesError,
|
||||
NodeMergingError,
|
||||
DegeneratedPathError,
|
||||
)
|
||||
from .trace import TraceBuilder
|
||||
from .mleader import (
|
||||
MultiLeaderBuilder,
|
||||
MultiLeaderMTextBuilder,
|
||||
MultiLeaderBlockBuilder,
|
||||
ConnectionSide,
|
||||
HorizontalConnection,
|
||||
VerticalConnection,
|
||||
LeaderType,
|
||||
TextAlignment,
|
||||
BlockAlignment,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Tuple, Iterable, Sequence
|
||||
from typing_extensions import TypeAlias
|
||||
import math
|
||||
from ezdxf.math import Vec3, UVec
|
||||
|
||||
LineSegment: TypeAlias = Tuple[Vec3, Vec3]
|
||||
|
||||
|
||||
class _LineTypeRenderer:
|
||||
"""Renders a line segment as multiple short segments according to the given
|
||||
line pattern.
|
||||
|
||||
In contrast to the DXF line pattern is this pattern simplified and follow
|
||||
the rule line-gap-line-gap-... a line of length 0 is a point.
|
||||
The pattern should end with a gap (even count) and the dash length is in
|
||||
drawing units.
|
||||
|
||||
Args:
|
||||
dashes: sequence of floats, line-gap-line-gap-...
|
||||
|
||||
"""
|
||||
# Get the simplified line pattern by LineType.simplified_line_pattern()
|
||||
def __init__(self, dashes: Sequence[float]):
|
||||
self._dashes = dashes
|
||||
self._dash_count: int = len(dashes)
|
||||
self.is_solid: bool = True
|
||||
self._current_dash: int = 0
|
||||
self._current_dash_length: float = 0.0
|
||||
if self._dash_count > 1:
|
||||
self.is_solid = False
|
||||
self._current_dash_length = self._dashes[0]
|
||||
self._is_dash = True
|
||||
|
||||
def line_segment(self, start: UVec, end: UVec) -> Iterable[LineSegment]:
|
||||
"""Yields the line from `start` to `end` according to stored line
|
||||
pattern as short segments. Yields only the lines and points not the
|
||||
gaps.
|
||||
|
||||
"""
|
||||
_start = Vec3(start)
|
||||
_end = Vec3(end)
|
||||
if self.is_solid or _start.isclose(_end):
|
||||
yield _start, _end
|
||||
return
|
||||
|
||||
segment_vec = _end - _start
|
||||
segment_length = segment_vec.magnitude
|
||||
segment_dir = segment_vec / segment_length # normalize
|
||||
|
||||
for is_dash, dash_length in self._render_dashes(segment_length):
|
||||
_end = _start + segment_dir * dash_length
|
||||
if is_dash:
|
||||
yield _start, _end
|
||||
_start = _end
|
||||
|
||||
def _render_dashes(self, length: float) -> Iterable[tuple[bool, float]]:
|
||||
if length <= self._current_dash_length:
|
||||
self._current_dash_length -= length
|
||||
yield self._is_dash, length
|
||||
if math.isclose(self._current_dash_length, 0.0):
|
||||
self._cycle_dashes()
|
||||
else:
|
||||
# Avoid deep recursions!
|
||||
while length > self._current_dash_length:
|
||||
length -= self._current_dash_length
|
||||
yield from self._render_dashes(self._current_dash_length)
|
||||
if length > 0.0:
|
||||
yield from self._render_dashes(length)
|
||||
|
||||
def _cycle_dashes(self):
|
||||
self._current_dash = (self._current_dash + 1) % self._dash_count
|
||||
self._current_dash_length = self._dashes[self._current_dash]
|
||||
self._is_dash = not self._is_dash
|
||||
@@ -0,0 +1,296 @@
|
||||
# Copyright (c) 2021-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
|
||||
# This is the abstract link between the text layout engine implemented in
|
||||
# ezdxf.tools.text_layout and a concrete MTEXT renderer implementation like
|
||||
# MTextExplode or ComplexMTextRenderer.
|
||||
from __future__ import annotations
|
||||
from typing import Sequence, Optional
|
||||
import abc
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf import colors
|
||||
from ezdxf.entities.mtext import MText, MTextColumns
|
||||
from ezdxf.enums import (
|
||||
MTextParagraphAlignment,
|
||||
)
|
||||
from ezdxf.fonts import fonts
|
||||
from ezdxf.tools import text_layout as tl
|
||||
from ezdxf.tools.text import (
|
||||
MTextParser,
|
||||
MTextContext,
|
||||
TokenType,
|
||||
ParagraphProperties,
|
||||
estimate_mtext_extents,
|
||||
)
|
||||
|
||||
__all__ = ["AbstractMTextRenderer"]
|
||||
|
||||
ALIGN = {
|
||||
MTextParagraphAlignment.LEFT: tl.ParagraphAlignment.LEFT,
|
||||
MTextParagraphAlignment.RIGHT: tl.ParagraphAlignment.RIGHT,
|
||||
MTextParagraphAlignment.CENTER: tl.ParagraphAlignment.CENTER,
|
||||
MTextParagraphAlignment.JUSTIFIED: tl.ParagraphAlignment.JUSTIFIED,
|
||||
MTextParagraphAlignment.DISTRIBUTED: tl.ParagraphAlignment.JUSTIFIED,
|
||||
MTextParagraphAlignment.DEFAULT: tl.ParagraphAlignment.LEFT,
|
||||
}
|
||||
|
||||
ATTACHMENT_POINT_TO_ALIGN = {
|
||||
const.MTEXT_TOP_LEFT: tl.ParagraphAlignment.LEFT,
|
||||
const.MTEXT_MIDDLE_LEFT: tl.ParagraphAlignment.LEFT,
|
||||
const.MTEXT_BOTTOM_LEFT: tl.ParagraphAlignment.LEFT,
|
||||
const.MTEXT_TOP_CENTER: tl.ParagraphAlignment.CENTER,
|
||||
const.MTEXT_MIDDLE_CENTER: tl.ParagraphAlignment.CENTER,
|
||||
const.MTEXT_BOTTOM_CENTER: tl.ParagraphAlignment.CENTER,
|
||||
const.MTEXT_TOP_RIGHT: tl.ParagraphAlignment.RIGHT,
|
||||
const.MTEXT_MIDDLE_RIGHT: tl.ParagraphAlignment.RIGHT,
|
||||
const.MTEXT_BOTTOM_RIGHT: tl.ParagraphAlignment.RIGHT,
|
||||
}
|
||||
|
||||
STACKING = {
|
||||
"^": tl.Stacking.OVER,
|
||||
"/": tl.Stacking.LINE,
|
||||
"#": tl.Stacking.SLANTED,
|
||||
}
|
||||
|
||||
|
||||
def make_default_tab_stops(cap_height: float, width: float) -> list[tl.TabStop]:
|
||||
tab_stops = []
|
||||
step = 4.0 * cap_height
|
||||
pos = step
|
||||
while pos < width:
|
||||
tab_stops.append(tl.TabStop(pos, tl.TabStopType.LEFT))
|
||||
pos += step
|
||||
return tab_stops
|
||||
|
||||
|
||||
def append_default_tab_stops(
|
||||
tab_stops: list[tl.TabStop], default_stops: Sequence[tl.TabStop]
|
||||
) -> None:
|
||||
last_pos = 0.0
|
||||
if tab_stops:
|
||||
last_pos = tab_stops[-1].pos
|
||||
tab_stops.extend(stop for stop in default_stops if stop.pos > last_pos)
|
||||
|
||||
|
||||
def make_tab_stops(
|
||||
cap_height: float,
|
||||
width: float,
|
||||
tab_stops: Sequence,
|
||||
default_stops: Sequence[tl.TabStop],
|
||||
) -> list[tl.TabStop]:
|
||||
_tab_stops = []
|
||||
for stop in tab_stops:
|
||||
if isinstance(stop, str):
|
||||
value = float(stop[1:])
|
||||
if stop[0] == "c":
|
||||
kind = tl.TabStopType.CENTER
|
||||
else:
|
||||
kind = tl.TabStopType.RIGHT
|
||||
else:
|
||||
kind = tl.TabStopType.LEFT
|
||||
value = float(stop)
|
||||
pos = value * cap_height
|
||||
if pos < width:
|
||||
_tab_stops.append(tl.TabStop(pos, kind))
|
||||
|
||||
append_default_tab_stops(_tab_stops, default_stops)
|
||||
return _tab_stops
|
||||
|
||||
|
||||
def get_stroke(ctx: MTextContext) -> int:
|
||||
stroke = 0
|
||||
if ctx.underline:
|
||||
stroke += tl.Stroke.UNDERLINE
|
||||
if ctx.strike_through:
|
||||
stroke += tl.Stroke.STRIKE_THROUGH
|
||||
if ctx.overline:
|
||||
stroke += tl.Stroke.OVERLINE
|
||||
if ctx.continue_stroke:
|
||||
stroke += tl.Stroke.CONTINUE
|
||||
return stroke
|
||||
|
||||
|
||||
def new_paragraph(
|
||||
cells: list,
|
||||
ctx: MTextContext,
|
||||
cap_height: float,
|
||||
line_spacing: float = 1,
|
||||
width: float = 0,
|
||||
default_stops: Optional[Sequence[tl.TabStop]] = None,
|
||||
):
|
||||
if cells:
|
||||
p = ctx.paragraph
|
||||
align = ALIGN.get(p.align, tl.ParagraphAlignment.LEFT)
|
||||
left = p.left * cap_height
|
||||
right = p.right * cap_height
|
||||
first = left + p.indent * cap_height # relative to left
|
||||
_default_stops: Sequence[tl.TabStop] = default_stops or []
|
||||
tab_stops = _default_stops
|
||||
if p.tab_stops:
|
||||
tab_stops = make_tab_stops(cap_height, width, p.tab_stops, _default_stops)
|
||||
paragraph = tl.Paragraph(
|
||||
align=align,
|
||||
indent=(first, left, right),
|
||||
line_spacing=line_spacing,
|
||||
tab_stops=tab_stops,
|
||||
)
|
||||
paragraph.append_content(cells)
|
||||
else:
|
||||
paragraph = tl.EmptyParagraph( # type: ignore
|
||||
cap_height=ctx.cap_height, line_spacing=line_spacing
|
||||
)
|
||||
return paragraph
|
||||
|
||||
|
||||
def super_glue():
|
||||
return tl.NonBreakingSpace(width=0, min_width=0, max_width=0)
|
||||
|
||||
|
||||
def defined_width(mtext: MText) -> float:
|
||||
width = mtext.dxf.get("width", 0.0)
|
||||
if width < 1e-6:
|
||||
width, height = estimate_mtext_extents(mtext)
|
||||
return width
|
||||
|
||||
|
||||
def column_heights(columns: MTextColumns) -> list[Optional[float]]:
|
||||
heights: list[Optional[float]]
|
||||
if columns.heights: # dynamic manual
|
||||
heights = list(columns.heights)
|
||||
# last height has to be auto height = None
|
||||
heights[-1] = None
|
||||
return heights
|
||||
# static, dynamic auto
|
||||
defined_height = abs(columns.defined_height)
|
||||
if defined_height < 1e-6:
|
||||
return [None]
|
||||
return [defined_height] * columns.count
|
||||
|
||||
|
||||
class AbstractMTextRenderer(abc.ABC):
|
||||
def __init__(self) -> None:
|
||||
self._font_cache: dict[tuple[str, float, float], fonts.AbstractFont] = {}
|
||||
|
||||
@abc.abstractmethod
|
||||
def word(self, test: str, ctx: MTextContext) -> tl.ContentCell:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def fraction(self, data: tuple[str, str, str], ctx: MTextContext) -> tl.ContentCell:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_font_face(self, mtext: MText) -> fonts.FontFace:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def make_bg_renderer(self, mtext: MText) -> tl.ContentRenderer:
|
||||
...
|
||||
|
||||
def make_mtext_context(self, mtext: MText) -> MTextContext:
|
||||
ctx = MTextContext()
|
||||
ctx.paragraph = ParagraphProperties(
|
||||
align=ATTACHMENT_POINT_TO_ALIGN.get( # type: ignore
|
||||
mtext.dxf.attachment_point, tl.ParagraphAlignment.LEFT
|
||||
)
|
||||
)
|
||||
ctx.font_face = self.get_font_face(mtext)
|
||||
ctx.cap_height = mtext.dxf.char_height
|
||||
ctx.aci = mtext.dxf.color
|
||||
rgb = mtext.rgb
|
||||
if rgb is not None:
|
||||
ctx.rgb = colors.RGB(*rgb)
|
||||
return ctx
|
||||
|
||||
def get_font(self, ctx: MTextContext) -> fonts.AbstractFont:
|
||||
ttf = fonts.find_font_file_name(ctx.font_face) # 1st call is very slow
|
||||
key = (ttf, ctx.cap_height, ctx.width_factor)
|
||||
font = self._font_cache.get(key)
|
||||
if font is None:
|
||||
font = fonts.make_font(ttf, ctx.cap_height, ctx.width_factor)
|
||||
self._font_cache[key] = font
|
||||
return font
|
||||
|
||||
def get_stroke(self, ctx: MTextContext) -> int:
|
||||
return get_stroke(ctx)
|
||||
|
||||
def get_stacking(self, type_: str) -> tl.Stacking:
|
||||
return STACKING.get(type_, tl.Stacking.LINE)
|
||||
|
||||
def space_width(self, ctx: MTextContext) -> float:
|
||||
return self.get_font(ctx).space_width()
|
||||
|
||||
def space(self, ctx: MTextContext):
|
||||
return tl.Space(width=self.space_width(ctx))
|
||||
|
||||
def tabulator(self, ctx: MTextContext):
|
||||
return tl.Tabulator(width=self.space_width(ctx))
|
||||
|
||||
def non_breaking_space(self, ctx: MTextContext):
|
||||
return tl.NonBreakingSpace(width=self.space_width(ctx))
|
||||
|
||||
def layout_engine(self, mtext: MText) -> tl.Layout:
|
||||
initial_cap_height = mtext.dxf.char_height
|
||||
line_spacing = mtext.dxf.line_spacing_factor
|
||||
|
||||
def append_paragraph():
|
||||
paragraph = new_paragraph(
|
||||
cells,
|
||||
ctx,
|
||||
initial_cap_height,
|
||||
line_spacing,
|
||||
width,
|
||||
default_stops,
|
||||
)
|
||||
layout.append_paragraphs([paragraph])
|
||||
cells.clear()
|
||||
|
||||
bg_renderer = self.make_bg_renderer(mtext)
|
||||
width = defined_width(mtext)
|
||||
default_stops = make_default_tab_stops(initial_cap_height, width)
|
||||
layout = tl.Layout(width=width)
|
||||
if mtext.has_columns:
|
||||
columns = mtext.columns
|
||||
assert columns is not None
|
||||
for height in column_heights(columns):
|
||||
layout.append_column(
|
||||
width=columns.width,
|
||||
height=height,
|
||||
gutter=columns.gutter_width,
|
||||
renderer=bg_renderer,
|
||||
)
|
||||
else:
|
||||
# column with auto height and default width
|
||||
layout.append_column(renderer=bg_renderer)
|
||||
|
||||
content = mtext.all_columns_raw_content()
|
||||
ctx = self.make_mtext_context(mtext)
|
||||
cells: list[tl.Cell] = []
|
||||
for token in MTextParser(content, ctx):
|
||||
ctx = token.ctx
|
||||
if token.type == TokenType.NEW_PARAGRAPH:
|
||||
append_paragraph()
|
||||
elif token.type == TokenType.NEW_COLUMN:
|
||||
append_paragraph()
|
||||
layout.next_column()
|
||||
elif token.type == TokenType.SPACE:
|
||||
cells.append(self.space(ctx))
|
||||
elif token.type == TokenType.NBSP:
|
||||
cells.append(self.non_breaking_space(ctx))
|
||||
elif token.type == TokenType.TABULATOR:
|
||||
cells.append(self.tabulator(ctx))
|
||||
elif token.type == TokenType.WORD:
|
||||
if cells and isinstance(cells[-1], (tl.Text, tl.Fraction)):
|
||||
# Create an unbreakable connection between those two parts.
|
||||
cells.append(super_glue())
|
||||
cells.append(self.word(token.data, ctx))
|
||||
elif token.type == TokenType.STACK:
|
||||
if cells and isinstance(cells[-1], (tl.Text, tl.Fraction)):
|
||||
# Create an unbreakable connection between those two parts.
|
||||
cells.append(super_glue())
|
||||
cells.append(self.fraction(token.data, ctx))
|
||||
|
||||
if cells:
|
||||
append_paragraph()
|
||||
|
||||
return layout
|
||||
626
.venv/lib/python3.12/site-packages/ezdxf/render/arrows.py
Normal file
626
.venv/lib/python3.12/site-packages/ezdxf/render/arrows.py
Normal file
@@ -0,0 +1,626 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Iterator
|
||||
from ezdxf.math import Vec2, Shape2d, NULLVEC, UVec
|
||||
from .forms import open_arrow, arrow2
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFGraphic
|
||||
from ezdxf.sections.blocks import BlocksSection
|
||||
from ezdxf.eztypes import GenericLayoutType
|
||||
|
||||
DEFAULT_ARROW_ANGLE = 18.924644
|
||||
DEFAULT_BETA = 45.0
|
||||
|
||||
|
||||
# The base arrow is oriented for the right hand side ->| of the dimension line,
|
||||
# reverse is the left hand side |<-.
|
||||
class BaseArrow:
|
||||
def __init__(self, vertices: Iterable[UVec]):
|
||||
self.shape = Shape2d(vertices)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
pass
|
||||
|
||||
def place(self, insert: UVec, angle: float):
|
||||
self.shape.rotate(angle)
|
||||
self.shape.translate(insert)
|
||||
|
||||
|
||||
class NoneStroke(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
super().__init__([Vec2(insert)])
|
||||
|
||||
|
||||
class ObliqueStroke(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
self.size = size
|
||||
s2 = size / 2
|
||||
# shape = [center, lower left, upper right]
|
||||
super().__init__([Vec2((-s2, -s2)), Vec2((s2, s2))])
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
layout.add_line(
|
||||
start=self.shape[0], end=self.shape[1], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class ArchTick(ObliqueStroke):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
width = self.size * 0.15
|
||||
dxfattribs = dxfattribs or {}
|
||||
if layout.dxfversion > "AC1009":
|
||||
dxfattribs["const_width"] = width
|
||||
layout.add_lwpolyline(
|
||||
self.shape, format="xy", dxfattribs=dxfattribs # type: ignore
|
||||
)
|
||||
else:
|
||||
dxfattribs["default_start_width"] = width
|
||||
dxfattribs["default_end_width"] = width
|
||||
layout.add_polyline2d(self.shape, dxfattribs=dxfattribs) # type: ignore
|
||||
|
||||
|
||||
class ClosedArrowBlank(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
super().__init__(open_arrow(size, angle=DEFAULT_ARROW_ANGLE))
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
if layout.dxfversion > "AC1009":
|
||||
polyline = layout.add_lwpolyline(
|
||||
points=self.shape, dxfattribs=dxfattribs # type: ignore
|
||||
)
|
||||
else:
|
||||
polyline = layout.add_polyline2d( # type: ignore
|
||||
points=self.shape, dxfattribs=dxfattribs # type: ignore
|
||||
)
|
||||
polyline.close(True)
|
||||
|
||||
|
||||
class ClosedArrow(ClosedArrowBlank):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
super().render(layout, dxfattribs)
|
||||
end_point = self.shape[0].lerp(self.shape[2])
|
||||
|
||||
layout.add_line(
|
||||
start=self.shape[1], end=end_point, dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class ClosedArrowFilled(ClosedArrow):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
layout.add_solid(
|
||||
points=self.shape, # type: ignore
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
|
||||
|
||||
class _OpenArrow(BaseArrow):
|
||||
def __init__(
|
||||
self,
|
||||
arrow_angle: float,
|
||||
insert: UVec,
|
||||
size: float = 1.0,
|
||||
angle: float = 0,
|
||||
):
|
||||
points = list(open_arrow(size, angle=arrow_angle))
|
||||
points.append((-1, 0))
|
||||
super().__init__(points)
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
if layout.dxfversion > "AC1009":
|
||||
layout.add_lwpolyline(points=self.shape[:-1], dxfattribs=dxfattribs)
|
||||
else:
|
||||
layout.add_polyline2d(points=self.shape[:-1], dxfattribs=dxfattribs)
|
||||
layout.add_line(
|
||||
start=self.shape[1], end=self.shape[-1], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class OpenArrow(_OpenArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
super().__init__(DEFAULT_ARROW_ANGLE, insert, size, angle)
|
||||
|
||||
|
||||
class OpenArrow30(_OpenArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
super().__init__(30, insert, size, angle)
|
||||
|
||||
|
||||
class OpenArrow90(_OpenArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
super().__init__(90, insert, size, angle)
|
||||
|
||||
|
||||
class Circle(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
self.radius = size / 2
|
||||
# shape = [center point, connection point]
|
||||
super().__init__(
|
||||
[
|
||||
Vec2((0, 0)),
|
||||
Vec2((-self.radius, 0)),
|
||||
Vec2((-size, 0)),
|
||||
]
|
||||
)
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
layout.add_circle(
|
||||
center=self.shape[0], radius=self.radius, dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class Origin(Circle):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
super().render(layout, dxfattribs)
|
||||
layout.add_line(
|
||||
start=self.shape[0], end=self.shape[2], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class CircleBlank(Circle):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
super().render(layout, dxfattribs)
|
||||
layout.add_line(
|
||||
start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class Origin2(Circle):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
layout.add_circle(
|
||||
center=self.shape[0], radius=self.radius, dxfattribs=dxfattribs
|
||||
)
|
||||
layout.add_circle(
|
||||
center=self.shape[0], radius=self.radius / 2, dxfattribs=dxfattribs
|
||||
)
|
||||
layout.add_line(
|
||||
start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class DotSmall(Circle):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
center = self.shape[0]
|
||||
d = Vec2((self.radius / 2, 0))
|
||||
p1 = center - d
|
||||
p2 = center + d
|
||||
dxfattribs = dxfattribs or {}
|
||||
if layout.dxfversion > "AC1009":
|
||||
dxfattribs["const_width"] = self.radius
|
||||
layout.add_lwpolyline(
|
||||
[(p1, 1), (p2, 1)],
|
||||
format="vb",
|
||||
close=True,
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
else:
|
||||
dxfattribs["default_start_width"] = self.radius
|
||||
dxfattribs["default_end_width"] = self.radius
|
||||
polyline = layout.add_polyline2d(
|
||||
points=[p1, p2], close=True, dxfattribs=dxfattribs
|
||||
)
|
||||
polyline[0].dxf.bulge = 1
|
||||
polyline[1].dxf.bulge = 1
|
||||
|
||||
|
||||
class Dot(DotSmall):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
layout.add_line(
|
||||
start=self.shape[1], end=self.shape[2], dxfattribs=dxfattribs
|
||||
)
|
||||
super().render(layout, dxfattribs)
|
||||
|
||||
|
||||
class Box(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
# shape = [lower_left, lower_right, upper_right, upper_left, connection point]
|
||||
s2 = size / 2
|
||||
super().__init__(
|
||||
[
|
||||
Vec2((-s2, -s2)),
|
||||
Vec2((+s2, -s2)),
|
||||
Vec2((+s2, +s2)),
|
||||
Vec2((-s2, +s2)),
|
||||
Vec2((-s2, 0)),
|
||||
Vec2((-size, 0)),
|
||||
]
|
||||
)
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
if layout.dxfversion > "AC1009":
|
||||
polyline = layout.add_lwpolyline(
|
||||
points=self.shape[0:4], dxfattribs=dxfattribs
|
||||
)
|
||||
else:
|
||||
polyline = layout.add_polyline2d( # type: ignore
|
||||
points=self.shape[0:4], dxfattribs=dxfattribs
|
||||
)
|
||||
polyline.close(True)
|
||||
layout.add_line(
|
||||
start=self.shape[4], end=self.shape[5], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class BoxFilled(Box):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
def solid_order():
|
||||
v = self.shape.vertices
|
||||
return [v[0], v[1], v[3], v[2]]
|
||||
|
||||
layout.add_solid(points=solid_order(), dxfattribs=dxfattribs)
|
||||
layout.add_line(
|
||||
start=self.shape[4], end=self.shape[5], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class Integral(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
self.radius = size * 0.3535534
|
||||
self.angle = angle
|
||||
# shape = [center, left_center, right_center]
|
||||
super().__init__(
|
||||
[
|
||||
Vec2((0, 0)),
|
||||
Vec2((-self.radius, 0)),
|
||||
Vec2((self.radius, 0)),
|
||||
]
|
||||
)
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
angle = self.angle
|
||||
layout.add_arc(
|
||||
center=self.shape[1],
|
||||
radius=self.radius,
|
||||
start_angle=-90 + angle,
|
||||
end_angle=angle,
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
layout.add_arc(
|
||||
center=self.shape[2],
|
||||
radius=self.radius,
|
||||
start_angle=90 + angle,
|
||||
end_angle=180 + angle,
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
|
||||
|
||||
class DatumTriangle(BaseArrow):
|
||||
REVERSE_ANGLE = 180
|
||||
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
d = 0.577350269 * size # tan(30)
|
||||
# shape = [upper_corner, lower_corner, connection_point]
|
||||
super().__init__(
|
||||
[
|
||||
Vec2((0, d)),
|
||||
Vec2((0, -d)),
|
||||
Vec2((-size, 0)),
|
||||
]
|
||||
)
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
if layout.dxfversion > "AC1009":
|
||||
polyline = layout.add_lwpolyline(
|
||||
points=self.shape, dxfattribs=dxfattribs # type: ignore
|
||||
)
|
||||
else:
|
||||
polyline = layout.add_polyline2d( # type: ignore
|
||||
points=self.shape, dxfattribs=dxfattribs # type: ignore
|
||||
)
|
||||
polyline.close(True)
|
||||
|
||||
|
||||
class DatumTriangleFilled(DatumTriangle):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
layout.add_solid(points=self.shape, dxfattribs=dxfattribs) # type: ignore
|
||||
|
||||
|
||||
class _EzArrow(BaseArrow):
|
||||
def __init__(self, insert: UVec, size: float = 1.0, angle: float = 0):
|
||||
points = list(arrow2(size, angle=DEFAULT_ARROW_ANGLE))
|
||||
points.append((-1, 0))
|
||||
super().__init__(points)
|
||||
self.place(insert, angle)
|
||||
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
if layout.dxfversion > "AC1009":
|
||||
polyline = layout.add_lwpolyline(
|
||||
self.shape[:-1], dxfattribs=dxfattribs
|
||||
)
|
||||
else:
|
||||
polyline = layout.add_polyline2d( # type: ignore
|
||||
self.shape[:-1], dxfattribs=dxfattribs
|
||||
)
|
||||
polyline.close(True)
|
||||
|
||||
|
||||
class EzArrowBlank(_EzArrow):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
super().render(layout, dxfattribs)
|
||||
layout.add_line(
|
||||
start=self.shape[-2], end=self.shape[-1], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class EzArrow(_EzArrow):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
super().render(layout, dxfattribs)
|
||||
layout.add_line(
|
||||
start=self.shape[1], end=self.shape[-1], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class EzArrowFilled(_EzArrow):
|
||||
def render(self, layout: GenericLayoutType, dxfattribs=None):
|
||||
points = self.shape.vertices
|
||||
layout.add_solid(
|
||||
[points[0], points[1], points[3], points[2]], dxfattribs=dxfattribs
|
||||
)
|
||||
layout.add_line(
|
||||
start=self.shape[-2], end=self.shape[-1], dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class _Arrows:
|
||||
closed_filled = ""
|
||||
dot = "DOT"
|
||||
dot_small = "DOTSMALL"
|
||||
dot_blank = "DOTBLANK"
|
||||
origin_indicator = "ORIGIN"
|
||||
origin_indicator_2 = "ORIGIN2"
|
||||
open = "OPEN"
|
||||
right_angle = "OPEN90"
|
||||
open_30 = "OPEN30"
|
||||
closed = "CLOSED"
|
||||
dot_smallblank = "SMALL"
|
||||
none = "NONE"
|
||||
oblique = "OBLIQUE"
|
||||
box_filled = "BOXFILLED"
|
||||
box = "BOXBLANK"
|
||||
closed_blank = "CLOSEDBLANK"
|
||||
datum_triangle_filled = "DATUMFILLED"
|
||||
datum_triangle = "DATUMBLANK"
|
||||
integral = "INTEGRAL"
|
||||
architectural_tick = "ARCHTICK"
|
||||
# ezdxf special arrows
|
||||
ez_arrow = "EZ_ARROW"
|
||||
ez_arrow_blank = "EZ_ARROW_BLANK"
|
||||
ez_arrow_filled = "EZ_ARROW_FILLED"
|
||||
|
||||
CLASSES = {
|
||||
closed_filled: ClosedArrowFilled,
|
||||
dot: Dot,
|
||||
dot_small: DotSmall,
|
||||
dot_blank: CircleBlank,
|
||||
origin_indicator: Origin,
|
||||
origin_indicator_2: Origin2,
|
||||
open: OpenArrow,
|
||||
right_angle: OpenArrow90,
|
||||
open_30: OpenArrow30,
|
||||
closed: ClosedArrow,
|
||||
dot_smallblank: Circle,
|
||||
none: NoneStroke,
|
||||
oblique: ObliqueStroke,
|
||||
box_filled: BoxFilled,
|
||||
box: Box,
|
||||
closed_blank: ClosedArrowBlank,
|
||||
datum_triangle: DatumTriangle,
|
||||
datum_triangle_filled: DatumTriangleFilled,
|
||||
integral: Integral,
|
||||
architectural_tick: ArchTick,
|
||||
ez_arrow: EzArrow,
|
||||
ez_arrow_blank: EzArrowBlank,
|
||||
ez_arrow_filled: EzArrowFilled,
|
||||
}
|
||||
# arrows with origin at dimension line start/end
|
||||
ORIGIN_ZERO = {
|
||||
architectural_tick,
|
||||
oblique,
|
||||
dot_small,
|
||||
dot_smallblank,
|
||||
integral,
|
||||
none,
|
||||
}
|
||||
|
||||
__acad__ = {
|
||||
closed_filled,
|
||||
dot,
|
||||
dot_small,
|
||||
dot_blank,
|
||||
origin_indicator,
|
||||
origin_indicator_2,
|
||||
open,
|
||||
right_angle,
|
||||
open_30,
|
||||
closed,
|
||||
dot_smallblank,
|
||||
none,
|
||||
oblique,
|
||||
box_filled,
|
||||
box,
|
||||
closed_blank,
|
||||
datum_triangle,
|
||||
datum_triangle_filled,
|
||||
integral,
|
||||
architectural_tick,
|
||||
}
|
||||
__ezdxf__ = {
|
||||
ez_arrow,
|
||||
ez_arrow_blank,
|
||||
ez_arrow_filled,
|
||||
}
|
||||
__all_arrows__ = __acad__ | __ezdxf__
|
||||
|
||||
EXTENSIONS_ALLOWED = {
|
||||
architectural_tick,
|
||||
oblique,
|
||||
none,
|
||||
dot_smallblank,
|
||||
integral,
|
||||
dot_small,
|
||||
}
|
||||
|
||||
def is_acad_arrow(self, item: str) -> bool:
|
||||
"""Returns ``True`` if `item` is a standard AutoCAD arrow."""
|
||||
return item.upper() in self.__acad__
|
||||
|
||||
def is_ezdxf_arrow(self, item: str) -> bool:
|
||||
"""Returns ``True`` if `item` is a special `ezdxf` arrow."""
|
||||
return item.upper() in self.__ezdxf__
|
||||
|
||||
def has_extension_line(self, name):
|
||||
"""Returns ``True`` if the arrow `name` supports extension lines."""
|
||||
return name in self.EXTENSIONS_ALLOWED
|
||||
|
||||
def __contains__(self, item: str) -> bool:
|
||||
"""Returns `True` if `item` is an arrow managed by this class."""
|
||||
if item is None:
|
||||
return False
|
||||
return item.upper() in self.__all_arrows__
|
||||
|
||||
def create_block(self, blocks: BlocksSection, name: str) -> str:
|
||||
"""Creates the BLOCK definition for arrow `name`."""
|
||||
block_name = self.block_name(name)
|
||||
if block_name not in blocks:
|
||||
block = blocks.new(block_name)
|
||||
arrow = self.arrow_shape(name, insert=(0, 0), size=1, rotation=0)
|
||||
arrow.render(block, dxfattribs={"color": 0, "linetype": "BYBLOCK"})
|
||||
return block_name
|
||||
|
||||
def arrow_handle(self, blocks: BlocksSection, name: str) -> str:
|
||||
"""Returns the BLOCK_RECORD handle for arrow `name`."""
|
||||
arrow_name = self.arrow_name(name)
|
||||
block_name = self.create_block(blocks, arrow_name)
|
||||
block = blocks.get(block_name)
|
||||
return block.block_record_handle
|
||||
|
||||
def block_name(self, name: str) -> str:
|
||||
"""Returns the block name."""
|
||||
if not self.is_acad_arrow(name): # common BLOCK definition
|
||||
# e.g. Dimension.dxf.bkl = 'EZ_ARROW' == Insert.dxf.name
|
||||
return name.upper()
|
||||
elif name == "":
|
||||
# special AutoCAD arrow symbol 'CLOSED_FILLED' has no name
|
||||
# ezdxf uses blocks for ALL arrows, but '_' (closed filled) as block name?
|
||||
return "_CLOSEDFILLED" # Dimension.dxf.bkl = '' != Insert.dxf.name = '_CLOSED_FILLED'
|
||||
else:
|
||||
# add preceding '_' to AutoCAD arrow symbol names
|
||||
# Dimension.dxf.bkl = 'DOT' != Insert.dxf.name = '_DOT'
|
||||
return "_" + name.upper()
|
||||
|
||||
def arrow_name(self, block_name: str) -> str:
|
||||
"""Returns the arrow name."""
|
||||
if block_name.startswith("_"):
|
||||
name = block_name[1:].upper()
|
||||
if name == "CLOSEDFILLED":
|
||||
return ""
|
||||
elif self.is_acad_arrow(name):
|
||||
return name
|
||||
return block_name
|
||||
|
||||
def insert_arrow(
|
||||
self,
|
||||
layout: GenericLayoutType,
|
||||
name: str,
|
||||
insert: UVec = NULLVEC,
|
||||
size: float = 1.0,
|
||||
rotation: float = 0,
|
||||
*,
|
||||
dxfattribs=None,
|
||||
) -> Vec2:
|
||||
"""Insert arrow as block reference into `layout`."""
|
||||
block_name = self.create_block(layout.doc.blocks, name)
|
||||
|
||||
dxfattribs = dict(dxfattribs or {})
|
||||
dxfattribs["rotation"] = rotation
|
||||
dxfattribs["xscale"] = size
|
||||
dxfattribs["yscale"] = size
|
||||
layout.add_blockref(block_name, insert=insert, dxfattribs=dxfattribs)
|
||||
return connection_point(
|
||||
name, insert=insert, scale=size, rotation=rotation
|
||||
)
|
||||
|
||||
def render_arrow(
|
||||
self,
|
||||
layout: GenericLayoutType,
|
||||
name: str,
|
||||
insert: UVec = NULLVEC,
|
||||
size: float = 1.0,
|
||||
rotation: float = 0,
|
||||
*,
|
||||
dxfattribs=None,
|
||||
) -> Vec2:
|
||||
"""Render arrow as basic DXF entities into `layout`."""
|
||||
dxfattribs = dict(dxfattribs or {})
|
||||
arrow = self.arrow_shape(name, insert, size, rotation)
|
||||
arrow.render(layout, dxfattribs)
|
||||
return connection_point(
|
||||
name, insert=insert, scale=size, rotation=rotation
|
||||
)
|
||||
|
||||
def virtual_entities(
|
||||
self,
|
||||
name: str,
|
||||
insert: UVec = NULLVEC,
|
||||
size: float = 0.625,
|
||||
rotation: float = 0,
|
||||
*,
|
||||
dxfattribs=None,
|
||||
) -> Iterator[DXFGraphic]:
|
||||
"""Returns all arrow components as virtual DXF entities."""
|
||||
from ezdxf.layouts import VirtualLayout
|
||||
|
||||
if name in self:
|
||||
layout = VirtualLayout()
|
||||
ARROWS.render_arrow(
|
||||
layout,
|
||||
name,
|
||||
insert=insert,
|
||||
size=size,
|
||||
rotation=rotation,
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
yield from iter(layout)
|
||||
|
||||
def arrow_shape(
|
||||
self, name: str, insert: UVec, size: float, rotation: float
|
||||
) -> BaseArrow:
|
||||
"""Returns an instance of the shape management class for arrow `name`."""
|
||||
# size depending shapes
|
||||
name = name.upper()
|
||||
if name == self.dot_small:
|
||||
size *= 0.25
|
||||
elif name == self.dot_smallblank:
|
||||
size *= 0.5
|
||||
cls = self.CLASSES[name]
|
||||
return cls(insert, size, rotation)
|
||||
|
||||
|
||||
def connection_point(
|
||||
arrow_name: str, insert: UVec, scale: float = 1.0, rotation: float = 0.0
|
||||
) -> Vec2:
|
||||
"""Returns the connection point for `arrow_name`."""
|
||||
insert = Vec2(insert)
|
||||
if ARROWS.arrow_name(arrow_name) in _Arrows.ORIGIN_ZERO:
|
||||
return insert
|
||||
else:
|
||||
return insert - Vec2.from_deg_angle(rotation, scale)
|
||||
|
||||
|
||||
def arrow_length(arrow_name: str, scale: float = 1.0) -> float:
|
||||
"""Returns the scaled arrow length of `arrow_name`."""
|
||||
if ARROWS.arrow_name(arrow_name) in _Arrows.ORIGIN_ZERO:
|
||||
return 0.0
|
||||
else:
|
||||
return scale
|
||||
|
||||
|
||||
ARROWS: _Arrows = _Arrows()
|
||||
504
.venv/lib/python3.12/site-packages/ezdxf/render/curves.py
Normal file
504
.venv/lib/python3.12/site-packages/ezdxf/render/curves.py
Normal file
@@ -0,0 +1,504 @@
|
||||
# Copyright (c) 2010-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
import random
|
||||
import math
|
||||
from ezdxf.math import (
|
||||
Vec3,
|
||||
Vec2,
|
||||
UVec,
|
||||
Matrix44,
|
||||
perlin,
|
||||
Bezier4P,
|
||||
global_bspline_interpolation,
|
||||
BSpline,
|
||||
open_uniform_bspline,
|
||||
closed_uniform_bspline,
|
||||
EulerSpiral as _EulerSpiral,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.layouts import BaseLayout
|
||||
|
||||
|
||||
def rnd(max_value):
|
||||
return max_value / 2.0 - random.random() * max_value
|
||||
|
||||
|
||||
def rnd_perlin(max_value, walker):
|
||||
r = perlin.snoise2(walker.x, walker.y)
|
||||
return max_value / 2.0 - r * max_value
|
||||
|
||||
|
||||
def random_2d_path(
|
||||
steps: int = 100,
|
||||
max_step_size: float = 1.0,
|
||||
max_heading: float = math.pi / 2,
|
||||
retarget: int = 20,
|
||||
) -> Iterable[Vec2]:
|
||||
"""Returns a random 2D path as iterable of :class:`~ezdxf.math.Vec2`
|
||||
objects.
|
||||
|
||||
Args:
|
||||
steps: count of vertices to generate
|
||||
max_step_size: max step size
|
||||
max_heading: limit heading angle change per step to ± max_heading/2 in
|
||||
radians
|
||||
retarget: specifies steps before changing global walking target
|
||||
|
||||
"""
|
||||
max_ = max_step_size * steps
|
||||
|
||||
def next_global_target():
|
||||
return Vec2((rnd(max_), rnd(max_)))
|
||||
|
||||
walker = Vec2(0, 0)
|
||||
target = next_global_target()
|
||||
for i in range(steps):
|
||||
if i % retarget == 0:
|
||||
target = target + next_global_target()
|
||||
angle = (target - walker).angle
|
||||
heading = angle + rnd_perlin(max_heading, walker)
|
||||
length = max_step_size * random.random()
|
||||
walker = walker + Vec2.from_angle(heading, length)
|
||||
yield walker
|
||||
|
||||
|
||||
def random_3d_path(
|
||||
steps: int = 100,
|
||||
max_step_size: float = 1.0,
|
||||
max_heading: float = math.pi / 2.0,
|
||||
max_pitch: float = math.pi / 8.0,
|
||||
retarget: int = 20,
|
||||
) -> Iterable[Vec3]:
|
||||
"""Returns a random 3D path as iterable of :class:`~ezdxf.math.Vec3`
|
||||
objects.
|
||||
|
||||
Args:
|
||||
steps: count of vertices to generate
|
||||
max_step_size: max step size
|
||||
max_heading: limit heading angle change per step to ± max_heading/2,
|
||||
rotation about the z-axis in radians
|
||||
max_pitch: limit pitch angle change per step to ± max_pitch/2, rotation
|
||||
about the x-axis in radians
|
||||
retarget: specifies steps before changing global walking target
|
||||
|
||||
"""
|
||||
max_ = max_step_size * steps
|
||||
|
||||
def next_global_target():
|
||||
return Vec3((rnd(max_), rnd(max_), rnd(max_)))
|
||||
|
||||
walker = Vec3()
|
||||
target = next_global_target()
|
||||
for i in range(steps):
|
||||
if i % retarget == 0:
|
||||
target = target + next_global_target()
|
||||
angle = (target - walker).angle
|
||||
length = max_step_size * random.random()
|
||||
heading_angle = angle + rnd_perlin(max_heading, walker)
|
||||
next_step = Vec3.from_angle(heading_angle, length)
|
||||
pitch_angle = rnd_perlin(max_pitch, walker)
|
||||
walker += Matrix44.x_rotate(pitch_angle).transform(next_step)
|
||||
yield walker
|
||||
|
||||
|
||||
class Bezier:
|
||||
"""Render a bezier curve as 2D/3D :class:`~ezdxf.entities.Polyline`.
|
||||
|
||||
The :class:`Bezier` class is implemented with multiple segments, each
|
||||
segment is an optimized 4 point bezier curve, the 4 control points of the
|
||||
curve are: the start point (1) and the end point (4), point (2) is start
|
||||
point + start vector and point (3) is end point + end vector. Each segment
|
||||
has its own approximation count.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The new :mod:`ezdxf.path` package provides many advanced construction tools
|
||||
based on the :class:`~ezdxf.path.Path` class.
|
||||
|
||||
"""
|
||||
|
||||
class Segment:
|
||||
def __init__(
|
||||
self,
|
||||
start: UVec,
|
||||
end: UVec,
|
||||
start_tangent: UVec,
|
||||
end_tangent: UVec,
|
||||
segments: int,
|
||||
):
|
||||
self.start = Vec3(start)
|
||||
self.end = Vec3(end)
|
||||
self.start_tangent = Vec3(
|
||||
start_tangent
|
||||
) # as vector, from start point
|
||||
self.end_tangent = Vec3(end_tangent) # as vector, from end point
|
||||
self.segments = segments
|
||||
|
||||
def approximate(self) -> Iterable[Vec3]:
|
||||
control_points = [
|
||||
self.start,
|
||||
self.start + self.start_tangent,
|
||||
self.end + self.end_tangent,
|
||||
self.end,
|
||||
]
|
||||
bezier = Bezier4P(control_points)
|
||||
return bezier.approximate(self.segments)
|
||||
|
||||
def __init__(self) -> None:
|
||||
# fit point, first control vector, second control vector, segment count
|
||||
self.points: list[
|
||||
tuple[Vec3, Optional[Vec3], Optional[Vec3], Optional[int]]
|
||||
] = []
|
||||
|
||||
def start(self, point: UVec, tangent: UVec) -> None:
|
||||
"""Set start point and start tangent.
|
||||
|
||||
Args:
|
||||
point: start point
|
||||
tangent: start tangent as vector, example: (5, 0, 0) means a
|
||||
horizontal tangent with a length of 5 drawing units
|
||||
"""
|
||||
self.points.append((Vec3(point), None, tangent, None))
|
||||
|
||||
def append(
|
||||
self,
|
||||
point: UVec,
|
||||
tangent1: UVec,
|
||||
tangent2: Optional[UVec] = None,
|
||||
segments: int = 20,
|
||||
):
|
||||
"""Append a control point with two control tangents.
|
||||
|
||||
Args:
|
||||
point: control point
|
||||
tangent1: first tangent as vector "left" of the control point
|
||||
tangent2: second tangent as vector "right" of the control point,
|
||||
if omitted `tangent2` = `-tangent1`
|
||||
segments: count of line segments for the polyline approximation,
|
||||
count of line segments from the previous control point to the
|
||||
appended control point.
|
||||
|
||||
"""
|
||||
tangent1 = Vec3(tangent1)
|
||||
if tangent2 is None:
|
||||
tangent2 = -tangent1
|
||||
else:
|
||||
tangent2 = Vec3(tangent2)
|
||||
self.points.append((Vec3(point), tangent1, tangent2, int(segments)))
|
||||
|
||||
def _build_bezier_segments(self) -> Iterable[Segment]:
|
||||
if len(self.points) > 1:
|
||||
for from_point, to_point in zip(self.points[:-1], self.points[1:]):
|
||||
start_point = from_point[0]
|
||||
start_tangent = from_point[2] # tangent2
|
||||
end_point = to_point[0]
|
||||
end_tangent = to_point[1] # tangent1
|
||||
count = to_point[3]
|
||||
yield Bezier.Segment(
|
||||
start_point, end_point, start_tangent, end_tangent, count # type: ignore
|
||||
)
|
||||
else:
|
||||
raise ValueError("Two or more points needed!")
|
||||
|
||||
def render(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
force3d: bool = False,
|
||||
dxfattribs=None,
|
||||
) -> None:
|
||||
"""Render Bezier curve as 2D/3D :class:`~ezdxf.entities.Polyline`.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
force3d: force 3D polyline rendering
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
points: list[Vec3] = []
|
||||
for segment in self._build_bezier_segments():
|
||||
points.extend(segment.approximate())
|
||||
if force3d or any(p[2] for p in points):
|
||||
layout.add_polyline3d(points, dxfattribs=dxfattribs)
|
||||
else:
|
||||
layout.add_polyline2d(points, dxfattribs=dxfattribs)
|
||||
|
||||
|
||||
class Spline:
|
||||
"""This class can be used to render B-splines into DXF R12 files as
|
||||
approximated :class:`~ezdxf.entities.Polyline` entities.
|
||||
The advantage of this class over the :class:`R12Spline` class is,
|
||||
that this is a real 3D curve, which means that the B-spline vertices do
|
||||
have to be located in a flat plane, and no :ref:`UCS` class is needed to
|
||||
place the curve in 3D space.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The newer :class:`~ezdxf.math.BSpline` class provides the
|
||||
advanced vertex interpolation method :meth:`~ezdxf.math.BSpline.flattening`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, points: Optional[Iterable[UVec]] = None, segments: int = 100
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
points: spline definition points
|
||||
segments: count of line segments for approximation, vertex count is
|
||||
`segments` + 1
|
||||
|
||||
"""
|
||||
if points is None:
|
||||
points = []
|
||||
self.points: list[Vec3] = Vec3.list(points)
|
||||
self.segments = int(segments)
|
||||
|
||||
def subdivide(self, segments: int = 4) -> None:
|
||||
"""Calculate overall segment count, where segments is the sub-segment
|
||||
count, `segments` = 4, means 4 line segments between two definition
|
||||
points e.g. 4 definition points and 4 segments = 12 overall segments,
|
||||
useful for fit point rendering.
|
||||
|
||||
Args:
|
||||
segments: sub-segments count between two definition points
|
||||
|
||||
"""
|
||||
self.segments = (len(self.points) - 1) * segments
|
||||
|
||||
def render_as_fit_points(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
degree: int = 3,
|
||||
method: str = "chord",
|
||||
dxfattribs: Optional[dict] = None,
|
||||
) -> None:
|
||||
"""Render a B-spline as 2D/3D :class:`~ezdxf.entities.Polyline`, where
|
||||
the definition points are fit points.
|
||||
|
||||
- 2D spline vertices uses: :meth:`~ezdxf.layouts.BaseLayout.add_polyline2d`
|
||||
- 3D spline vertices uses: :meth:`~ezdxf.layouts.BaseLayout.add_polyline3d`
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
method: "uniform", "distance"/"chord", "centripetal"/"sqrt_chord" or
|
||||
"arc" calculation method for parameter t
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = global_bspline_interpolation(
|
||||
self.points, degree=degree, method=method
|
||||
)
|
||||
vertices = list(spline.approximate(self.segments))
|
||||
if any(vertex.z != 0.0 for vertex in vertices):
|
||||
layout.add_polyline3d(vertices, dxfattribs=dxfattribs)
|
||||
else:
|
||||
layout.add_polyline2d(vertices, dxfattribs=dxfattribs)
|
||||
|
||||
render = render_as_fit_points
|
||||
|
||||
def render_open_bspline(
|
||||
self, layout: BaseLayout, degree: int = 3, dxfattribs=None
|
||||
) -> None:
|
||||
"""Render an open uniform B-spline as 3D :class:`~ezdxf.entities.Polyline`.
|
||||
Definition points are control points.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = BSpline(self.points, order=degree + 1)
|
||||
layout.add_polyline3d(
|
||||
list(spline.approximate(self.segments)), dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
def render_uniform_bspline(
|
||||
self, layout: BaseLayout, degree: int = 3, dxfattribs=None
|
||||
) -> None:
|
||||
"""Render a uniform B-spline as 3D :class:`~ezdxf.entities.Polyline`.
|
||||
Definition points are control points.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = open_uniform_bspline(self.points, order=degree + 1)
|
||||
layout.add_polyline3d(
|
||||
list(spline.approximate(self.segments)), dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
def render_closed_bspline(
|
||||
self, layout: BaseLayout, degree: int = 3, dxfattribs=None
|
||||
) -> None:
|
||||
"""Render a closed uniform B-spline as 3D :class:`~ezdxf.entities.Polyline`.
|
||||
Definition points are control points.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = closed_uniform_bspline(self.points, order=degree + 1)
|
||||
layout.add_polyline3d(
|
||||
list(spline.approximate(self.segments)), dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
def render_open_rbspline(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
weights: Iterable[float],
|
||||
degree: int = 3,
|
||||
dxfattribs=None,
|
||||
) -> None:
|
||||
"""Render a rational open uniform BSpline as 3D :class:`~ezdxf.entities.Polyline`.
|
||||
Definition points are control points.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
weights: list of weights, requires a weight value (float) for each
|
||||
definition point.
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = BSpline(self.points, order=degree + 1, weights=weights)
|
||||
layout.add_polyline3d(
|
||||
list(spline.approximate(self.segments)), dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
def render_uniform_rbspline(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
weights: Iterable[float],
|
||||
degree: int = 3,
|
||||
dxfattribs=None,
|
||||
) -> None:
|
||||
"""Render a rational uniform B-spline as 3D :class:`~ezdxf.entities.Polyline`.
|
||||
Definition points are control points.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
weights: list of weights, requires a weight value (float) for each
|
||||
definition point.
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = closed_uniform_bspline(
|
||||
self.points, order=degree + 1, weights=weights
|
||||
)
|
||||
layout.add_polyline3d(
|
||||
list(spline.approximate(self.segments)), dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
def render_closed_rbspline(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
weights: Iterable[float],
|
||||
degree: int = 3,
|
||||
dxfattribs=None,
|
||||
) -> None:
|
||||
"""Render a rational B-spline as 3D :class:`~ezdxf.entities.Polyline`.
|
||||
Definition points are control points.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
weights: list of weights, requires a weight value (float) for each
|
||||
definition point.
|
||||
degree: degree of B-spline (order = `degree` + 1)
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
spline = closed_uniform_bspline(
|
||||
self.points, order=degree + 1, weights=weights
|
||||
)
|
||||
layout.add_polyline3d(
|
||||
list(spline.approximate(self.segments)), dxfattribs=dxfattribs
|
||||
)
|
||||
|
||||
|
||||
class EulerSpiral:
|
||||
"""Render an `euler spiral <https://en.wikipedia.org/wiki/Euler_spiral>`_
|
||||
as a 3D :class:`~ezdxf.entities.Polyline` or a
|
||||
:class:`~ezdxf.entities.Spline` entity.
|
||||
|
||||
This is a parametric curve, which always starts at the origin (0, 0).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, curvature: float = 1):
|
||||
"""
|
||||
Args:
|
||||
curvature: Radius of curvature
|
||||
|
||||
"""
|
||||
self.spiral = _EulerSpiral(float(curvature))
|
||||
|
||||
def render_polyline(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
length: float = 1,
|
||||
segments: int = 100,
|
||||
matrix: Optional[Matrix44] = None,
|
||||
dxfattribs=None,
|
||||
):
|
||||
"""Render curve as :class:`~ezdxf.entities.Polyline`.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
length: length measured along the spiral curve from its initial position
|
||||
segments: count of line segments to use, vertex count is `segments` + 1
|
||||
matrix: transformation matrix as :class:`~ezdxf.math.Matrix44`
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
Returns:
|
||||
:class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
points = self.spiral.approximate(length, segments)
|
||||
if matrix is not None:
|
||||
points = matrix.transform_vertices(points)
|
||||
return layout.add_polyline3d(list(points), dxfattribs=dxfattribs)
|
||||
|
||||
def render_spline(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
length: float = 1,
|
||||
fit_points: int = 10,
|
||||
degree: int = 3,
|
||||
matrix: Optional[Matrix44] = None,
|
||||
dxfattribs=None,
|
||||
):
|
||||
"""
|
||||
Render curve as :class:`~ezdxf.entities.Spline`.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
length: length measured along the spiral curve from its initial position
|
||||
fit_points: count of spline fit points to use
|
||||
degree: degree of B-spline
|
||||
matrix: transformation matrix as :class:`~ezdxf.math.Matrix44`
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Spline`
|
||||
|
||||
Returns:
|
||||
:class:`~ezdxf.entities.Spline`
|
||||
|
||||
"""
|
||||
spline = self.spiral.bspline(length, fit_points, degree=degree)
|
||||
points = spline.control_points
|
||||
if matrix is not None:
|
||||
points = matrix.transform_vertices(points)
|
||||
return layout.add_open_spline(
|
||||
control_points=points,
|
||||
degree=spline.degree,
|
||||
knots=spline.knots(),
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
1350
.venv/lib/python3.12/site-packages/ezdxf/render/dim_base.py
Normal file
1350
.venv/lib/python3.12/site-packages/ezdxf/render/dim_base.py
Normal file
File diff suppressed because it is too large
Load Diff
977
.venv/lib/python3.12/site-packages/ezdxf/render/dim_curved.py
Normal file
977
.venv/lib/python3.12/site-packages/ezdxf/render/dim_curved.py
Normal file
@@ -0,0 +1,977 @@
|
||||
# Copyright (c) 2021-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
import math
|
||||
|
||||
from ezdxf.math import (
|
||||
Vec2,
|
||||
Vec3,
|
||||
NULLVEC,
|
||||
UCS,
|
||||
decdeg2dms,
|
||||
arc_angle_span_rad,
|
||||
xround,
|
||||
)
|
||||
from ezdxf.entities import DimStyleOverride, Dimension, DXFEntity
|
||||
from .dim_base import (
|
||||
BaseDimensionRenderer,
|
||||
get_required_defpoint,
|
||||
format_text,
|
||||
apply_dimpost,
|
||||
Tolerance,
|
||||
Measurement,
|
||||
LengthMeasurement,
|
||||
compile_mtext,
|
||||
order_leader_points,
|
||||
get_center_leader_points,
|
||||
)
|
||||
from ezdxf.render.arrows import ARROWS, arrow_length
|
||||
from ezdxf.tools.text import is_upside_down_text_angle
|
||||
from ezdxf.math import intersection_line_line_2d
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.eztypes import GenericLayoutType
|
||||
|
||||
__all__ = ["AngularDimension", "Angular3PDimension", "ArcLengthDimension"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
ARC_PREFIX = "( "
|
||||
|
||||
|
||||
def has_required_attributes(entity: DXFEntity, attrib_names: list[str]):
|
||||
has = entity.dxf.hasattr
|
||||
return all(has(attrib_name) for attrib_name in attrib_names)
|
||||
|
||||
|
||||
GRAD = 200.0 / math.pi
|
||||
DEG = 180.0 / math.pi
|
||||
|
||||
|
||||
def format_angular_text(
|
||||
value: float,
|
||||
angle_units: int,
|
||||
dimrnd: Optional[float],
|
||||
dimdec: int,
|
||||
dimzin: int,
|
||||
dimdsep: str,
|
||||
) -> str:
|
||||
def decimal_format(_value: float) -> str:
|
||||
return format_text(
|
||||
_value,
|
||||
dimrnd=dimrnd,
|
||||
dimdec=dimdec,
|
||||
dimzin=dimzin,
|
||||
dimdsep=dimdsep,
|
||||
)
|
||||
|
||||
def dms_format(_value: float) -> str:
|
||||
if dimrnd is not None:
|
||||
_value = xround(_value, dimrnd)
|
||||
d, m, s = decdeg2dms(_value)
|
||||
if dimdec > 4:
|
||||
places = dimdec - 5
|
||||
s = round(s, places)
|
||||
return f"{d:.0f}°{m:.0f}'{decimal_format(s)}\""
|
||||
if dimdec > 2:
|
||||
return f"{d:.0f}°{m:.0f}'{s:.0f}\""
|
||||
if dimdec > 0:
|
||||
return f"{d:.0f}°{m:.0f}'"
|
||||
return f"{d:.0f}°"
|
||||
|
||||
# angular_unit:
|
||||
# 0 = Decimal degrees
|
||||
# 1 = Degrees/minutes/seconds
|
||||
# 2 = Grad
|
||||
# 3 = Radians
|
||||
text = ""
|
||||
if angle_units == 0:
|
||||
text = decimal_format(value * DEG) + "°"
|
||||
elif angle_units == 1:
|
||||
text = dms_format(value * DEG)
|
||||
elif angle_units == 2:
|
||||
text = decimal_format(value * GRAD) + "g"
|
||||
elif angle_units == 3:
|
||||
text = decimal_format(value) + "r"
|
||||
return text
|
||||
|
||||
|
||||
_ANGLE_UNITS = [
|
||||
DEG,
|
||||
DEG,
|
||||
GRAD,
|
||||
1.0,
|
||||
]
|
||||
|
||||
|
||||
def to_radians(value: float, dimaunit: int) -> float:
|
||||
try:
|
||||
return value / _ANGLE_UNITS[dimaunit]
|
||||
except IndexError:
|
||||
return value / DEG
|
||||
|
||||
|
||||
class AngularTolerance(Tolerance):
|
||||
def __init__(
|
||||
self,
|
||||
dim_style: DimStyleOverride,
|
||||
cap_height: float = 1.0,
|
||||
width_factor: float = 1.0,
|
||||
dim_scale: float = 1.0,
|
||||
angle_units: int = 0,
|
||||
):
|
||||
self.angular_units = angle_units
|
||||
super().__init__(dim_style, cap_height, width_factor, dim_scale)
|
||||
# Tolerance values are interpreted in dimaunit:
|
||||
# dimtp 1 means 1 degree for dimaunit = 0 or 1, but 1 radians for
|
||||
# dimaunit = 3
|
||||
# format_text() requires radians as input:
|
||||
self.update_tolerance_text(
|
||||
to_radians(self.maximum, angle_units),
|
||||
to_radians(self.minimum, angle_units),
|
||||
)
|
||||
|
||||
def format_text(self, value: float) -> str:
|
||||
"""Rounding and text formatting of tolerance `value`, removes leading
|
||||
and trailing zeros if necessary.
|
||||
|
||||
"""
|
||||
return format_angular_text(
|
||||
value=value,
|
||||
angle_units=self.angular_units,
|
||||
dimrnd=None,
|
||||
dimdec=self.decimal_places,
|
||||
dimzin=self.suppress_zeros,
|
||||
dimdsep=self.text_decimal_separator,
|
||||
)
|
||||
|
||||
def update_limits(self, measurement: float) -> None:
|
||||
# measurement is in radians, tolerance values are interpreted in
|
||||
# dimaunit: dimtp 1 means 1 degree for dimaunit = 0 or 1,
|
||||
# but 1 radians for dimaunit = 3
|
||||
# format_text() requires radians as input:
|
||||
upper_limit = measurement + to_radians(self.maximum, self.angular_units)
|
||||
lower_limit = measurement - to_radians(self.minimum, self.angular_units)
|
||||
self.text_upper = self.format_text(upper_limit)
|
||||
self.text_lower = self.format_text(lower_limit)
|
||||
self.text_width = self.get_text_width(self.text_upper, self.text_lower)
|
||||
|
||||
|
||||
class AngleMeasurement(Measurement):
|
||||
def update(self, raw_measurement_value: float) -> None:
|
||||
self.raw_value = raw_measurement_value
|
||||
self.value = raw_measurement_value
|
||||
self.text = self.text_override(raw_measurement_value)
|
||||
|
||||
def format_text(self, value: float) -> str:
|
||||
text = format_angular_text(
|
||||
value=value,
|
||||
angle_units=self.angle_units,
|
||||
dimrnd=None,
|
||||
dimdec=self.angular_decimal_places,
|
||||
dimzin=self.angular_suppress_zeros << 2, # convert to dimzin value
|
||||
dimdsep=self.decimal_separator,
|
||||
)
|
||||
if self.text_post_process_format:
|
||||
text = apply_dimpost(text, self.text_post_process_format)
|
||||
return text
|
||||
|
||||
|
||||
def fits_into_arc_span(length: float, radius: float, arc_span: float) -> bool:
|
||||
required_arc_span: float = length / radius
|
||||
return arc_span > required_arc_span
|
||||
|
||||
|
||||
class _CurvedDimensionLine(BaseDimensionRenderer):
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
super().__init__(dimension, ucs, override)
|
||||
# Common parameters for all sub-classes:
|
||||
# Use hidden line detection for dimension line:
|
||||
# Disable expensive hidden line calculation if possible!
|
||||
self.remove_hidden_lines_of_dimline = True
|
||||
self.center_of_arc: Vec2 = self.get_center_of_arc()
|
||||
self.dim_line_radius: float = self.get_dim_line_radius()
|
||||
self.ext1_dir: Vec2 = self.get_ext1_dir()
|
||||
self.start_angle_rad: float = self.ext1_dir.angle
|
||||
self.ext2_dir: Vec2 = self.get_ext2_dir()
|
||||
self.end_angle_rad: float = self.ext2_dir.angle
|
||||
|
||||
# Angle between extension lines for all curved dimensions:
|
||||
# equal to the angle measurement of angular dimensions
|
||||
self.arc_angle_span_rad: float = arc_angle_span_rad(
|
||||
self.start_angle_rad, self.end_angle_rad
|
||||
)
|
||||
self.center_angle_rad = (
|
||||
self.start_angle_rad + self.arc_angle_span_rad / 2.0
|
||||
)
|
||||
|
||||
# Additional required parameters but calculated later by sub-classes:
|
||||
self.ext1_start = Vec2() # start of 1st extension line
|
||||
self.ext2_start = Vec2() # start of 2nd extension line
|
||||
|
||||
# Class specific setup:
|
||||
self.update_measurement()
|
||||
if self.tol.has_limits:
|
||||
self.tol.update_limits(self.measurement.value)
|
||||
|
||||
# Text width and -height is required first, text location and -rotation
|
||||
# are not valid yet:
|
||||
self.text_box = self.init_text_box()
|
||||
|
||||
# Place arrows outside?
|
||||
self.arrows_outside = False
|
||||
|
||||
self.setup_text_and_arrow_fitting()
|
||||
self.setup_text_location()
|
||||
|
||||
# update text box location and -rotation:
|
||||
self.text_box.center = self.measurement.text_location
|
||||
self.text_box.angle = self.measurement.text_rotation
|
||||
self.geometry.set_text_box(self.text_box)
|
||||
|
||||
# Update final text location in the DIMENSION entity:
|
||||
self.dimension.dxf.text_midpoint = self.measurement.text_location
|
||||
|
||||
@property
|
||||
def ocs_center_of_arc(self) -> Vec3:
|
||||
return self.geometry.ucs.to_ocs(Vec3(self.center_of_arc))
|
||||
|
||||
@property
|
||||
def dim_midpoint(self) -> Vec2:
|
||||
"""Return the midpoint of the dimension line."""
|
||||
return self.center_of_arc + Vec2.from_angle(
|
||||
self.center_angle_rad, self.dim_line_radius
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def update_measurement(self) -> None:
|
||||
"""Setup measurement text."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_ext1_dir(self) -> Vec2:
|
||||
"""Return the direction of the 1st extension line == start angle."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_ext2_dir(self) -> Vec2:
|
||||
"""Return the direction of the 2nd extension line == end angle."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_center_of_arc(self) -> Vec2:
|
||||
"""Return the center of the arc."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_dim_line_radius(self) -> float:
|
||||
"""Return the distance from the center of the arc to the dimension line
|
||||
location
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_defpoints(self) -> list[Vec2]:
|
||||
...
|
||||
|
||||
def transform_ucs_to_wcs(self) -> None:
|
||||
"""Transforms dimension definition points into WCS or if required into
|
||||
OCS.
|
||||
"""
|
||||
|
||||
def from_ucs(attr, func):
|
||||
if dxf.is_supported(attr):
|
||||
point = dxf.get(attr, NULLVEC)
|
||||
dxf.set(attr, func(point))
|
||||
|
||||
dxf = self.dimension.dxf
|
||||
ucs = self.geometry.ucs
|
||||
from_ucs("defpoint", ucs.to_wcs)
|
||||
from_ucs("defpoint2", ucs.to_wcs)
|
||||
from_ucs("defpoint3", ucs.to_wcs)
|
||||
from_ucs("defpoint4", ucs.to_wcs)
|
||||
from_ucs("defpoint5", ucs.to_wcs)
|
||||
from_ucs("text_midpoint", ucs.to_ocs)
|
||||
|
||||
def default_location(self, shift: float = 0.0) -> Vec2:
|
||||
radius = (
|
||||
self.dim_line_radius
|
||||
+ self.measurement.text_vertical_distance()
|
||||
+ shift
|
||||
)
|
||||
text_radial_dir = Vec2.from_angle(self.center_angle_rad)
|
||||
return self.center_of_arc + text_radial_dir * radius
|
||||
|
||||
def setup_text_and_arrow_fitting(self) -> None:
|
||||
# self.text_box.width includes the gaps between text and dimension line
|
||||
# Is the measurement text without the arrows too wide to fit between the
|
||||
# extension lines?
|
||||
self.measurement.is_wide_text = not fits_into_arc_span(
|
||||
self.text_box.width, self.dim_line_radius, self.arc_angle_span_rad
|
||||
)
|
||||
|
||||
required_text_and_arrows_space: float = (
|
||||
# The suppression of the arrows is not taken into account:
|
||||
self.text_box.width
|
||||
+ 2.0 * self.arrows.arrow_size
|
||||
)
|
||||
|
||||
# dimatfit: measurement text fitting rule is ignored!
|
||||
# Place arrows outside?
|
||||
self.arrows_outside = not fits_into_arc_span(
|
||||
required_text_and_arrows_space,
|
||||
self.dim_line_radius,
|
||||
self.arc_angle_span_rad,
|
||||
)
|
||||
# Place measurement text outside?
|
||||
self.measurement.text_is_outside = not fits_into_arc_span(
|
||||
required_text_and_arrows_space * 1.1, # add some extra space
|
||||
self.dim_line_radius,
|
||||
self.arc_angle_span_rad,
|
||||
)
|
||||
|
||||
if (
|
||||
self.measurement.text_is_outside
|
||||
and self.measurement.user_text_rotation is None
|
||||
):
|
||||
# Intersection of the measurement text with the dimension line is
|
||||
# not possible:
|
||||
self.remove_hidden_lines_of_dimline = False
|
||||
|
||||
def setup_text_location(self) -> None:
|
||||
"""Setup geometric text properties (location, rotation) and the TextBox
|
||||
object.
|
||||
"""
|
||||
# dimtix: measurement.force_text_inside is ignored
|
||||
# dimtih: measurement.text_inside_horizontal is ignored
|
||||
# dimtoh: measurement.text_outside_horizontal is ignored
|
||||
|
||||
# text radial direction = center -> text
|
||||
text_radial_dir: Vec2 # text "vertical" direction
|
||||
measurement = self.measurement
|
||||
|
||||
# determine text location:
|
||||
at_default_location: bool = measurement.user_location is None
|
||||
has_text_shifting: bool = bool(
|
||||
measurement.text_shift_h or measurement.text_shift_v
|
||||
)
|
||||
if at_default_location:
|
||||
# place text in the "horizontal" center of the dimension line at the
|
||||
# default location defined by measurement.text_valign (dimtad):
|
||||
text_radial_dir = Vec2.from_angle(self.center_angle_rad)
|
||||
shift_text_upwards: float = 0.0
|
||||
if measurement.text_is_outside:
|
||||
# reset vertical alignment to "above"
|
||||
measurement.text_valign = 1
|
||||
if measurement.is_wide_text:
|
||||
# move measurement text "above" the extension line endings:
|
||||
shift_text_upwards = self.extension_lines.extension_above
|
||||
measurement.text_location = self.default_location(
|
||||
shift=shift_text_upwards
|
||||
)
|
||||
if (
|
||||
measurement.text_valign > 0 and not has_text_shifting
|
||||
): # not in the center and no text shifting is applied
|
||||
# disable expensive hidden line calculation
|
||||
self.remove_hidden_lines_of_dimline = False
|
||||
else:
|
||||
# apply dimtmove: measurement.text_movement_rule
|
||||
user_location = measurement.user_location
|
||||
assert isinstance(user_location, Vec2)
|
||||
if measurement.relative_user_location:
|
||||
user_location += self.dim_midpoint
|
||||
measurement.text_location = user_location
|
||||
if measurement.text_movement_rule == 0:
|
||||
# Moves the dimension line with dimension text and
|
||||
# aligns the text direction perpendicular to the connection
|
||||
# line from the arc center to the text center:
|
||||
self.dim_line_radius = (
|
||||
self.center_of_arc - user_location
|
||||
).magnitude
|
||||
# Attributes about the text and arrow fitting have to be
|
||||
# updated now:
|
||||
self.setup_text_and_arrow_fitting()
|
||||
elif measurement.text_movement_rule == 1:
|
||||
# Adds a leader when dimension text, text direction is
|
||||
# "horizontal" or user text rotation if given.
|
||||
# Leader location is defined by dimtad (text_valign):
|
||||
# "center" - connects to the left or right center of the text
|
||||
# "below" - add a line below the text
|
||||
if measurement.user_text_rotation is None:
|
||||
# override text rotation
|
||||
measurement.user_text_rotation = 0.0
|
||||
measurement.text_is_outside = True # by definition
|
||||
elif measurement.text_movement_rule == 2:
|
||||
# Allows text to be moved freely without a leader and
|
||||
# aligns the text direction perpendicular to the connection
|
||||
# line from the arc center to the text center:
|
||||
measurement.text_is_outside = True # by definition
|
||||
text_radial_dir = (
|
||||
measurement.text_location - self.center_of_arc
|
||||
).normalize()
|
||||
|
||||
# set text "horizontal":
|
||||
text_tangential_dir = text_radial_dir.orthogonal(ccw=False)
|
||||
|
||||
if at_default_location and has_text_shifting:
|
||||
# Apply text relative shift (ezdxf only feature)
|
||||
if measurement.text_shift_h:
|
||||
measurement.text_location += (
|
||||
text_tangential_dir * measurement.text_shift_h
|
||||
)
|
||||
if measurement.text_shift_v:
|
||||
measurement.text_location += (
|
||||
text_radial_dir * measurement.text_shift_v
|
||||
)
|
||||
|
||||
# apply user text rotation; rotation in degrees:
|
||||
if measurement.user_text_rotation is None:
|
||||
rotation = text_tangential_dir.angle_deg
|
||||
else:
|
||||
rotation = measurement.user_text_rotation
|
||||
|
||||
if not self.geometry.requires_extrusion:
|
||||
# todo: extrusion vector (0, 0, -1)?
|
||||
# Practically all DIMENSION entities are 2D entities,
|
||||
# where OCS == WCS, check WCS text orientation:
|
||||
wcs_angle = self.geometry.ucs.to_ocs_angle_deg(rotation)
|
||||
if is_upside_down_text_angle(wcs_angle):
|
||||
measurement.has_upside_down_correction = True
|
||||
rotation += 180.0 # apply to UCS rotation!
|
||||
measurement.text_rotation = rotation
|
||||
|
||||
def get_leader_points(self) -> tuple[Vec2, Vec2]:
|
||||
# Leader location is defined by dimtad (text_valign):
|
||||
# "center":
|
||||
# - connects to the left or right vertical center of the text
|
||||
# - distance between text and leader line is measurement.text_gap (dimgap)
|
||||
# and is already included in the text_box corner points
|
||||
# - length of "leg": arrows.arrow_size
|
||||
# "below" - add a line below the text
|
||||
if self.measurement.text_valign == 0: # "center"
|
||||
return get_center_leader_points(
|
||||
self.dim_midpoint, self.text_box, self.arrows.arrow_size
|
||||
)
|
||||
else: # "below"
|
||||
c0, c1, c2, c3 = self.text_box.corners
|
||||
if self.measurement.has_upside_down_correction:
|
||||
p1, p2 = c2, c3
|
||||
else:
|
||||
p1, p2 = c0, c1
|
||||
return order_leader_points(self.dim_midpoint, p1, p2)
|
||||
|
||||
def render(self, block: GenericLayoutType) -> None:
|
||||
"""Main method to create dimension geometry of basic DXF entities in the
|
||||
associated BLOCK layout.
|
||||
|
||||
Args:
|
||||
block: target BLOCK for rendering
|
||||
|
||||
"""
|
||||
super().render(block)
|
||||
self.add_extension_lines()
|
||||
adjust_start_angle, adjust_end_angle = self.add_arrows()
|
||||
|
||||
measurement = self.measurement
|
||||
if measurement.text:
|
||||
if self.geometry.supports_dxf_r2000:
|
||||
text = compile_mtext(measurement, self.tol)
|
||||
else:
|
||||
text = measurement.text
|
||||
self.add_measurement_text(
|
||||
text, measurement.text_location, measurement.text_rotation
|
||||
)
|
||||
if measurement.has_leader:
|
||||
p1, p2 = self.get_leader_points()
|
||||
self.add_leader(self.dim_midpoint, p1, p2)
|
||||
self.add_dimension_line(adjust_start_angle, adjust_end_angle)
|
||||
self.geometry.add_defpoints(self.get_defpoints())
|
||||
|
||||
def add_extension_lines(self) -> None:
|
||||
ext_lines = self.extension_lines
|
||||
if not ext_lines.suppress1:
|
||||
self._add_ext_line(
|
||||
self.ext1_start, self.ext1_dir, ext_lines.dxfattribs(1)
|
||||
)
|
||||
if not ext_lines.suppress2:
|
||||
self._add_ext_line(
|
||||
self.ext2_start, self.ext2_dir, ext_lines.dxfattribs(2)
|
||||
)
|
||||
|
||||
def _add_ext_line(self, start: Vec2, direction: Vec2, dxfattribs) -> None:
|
||||
ext_lines = self.extension_lines
|
||||
center = self.center_of_arc
|
||||
radius = self.dim_line_radius
|
||||
ext_above = ext_lines.extension_above
|
||||
is_inside = (start - center).magnitude > radius
|
||||
|
||||
if ext_lines.has_fixed_length:
|
||||
ext_below = ext_lines.length_below
|
||||
if is_inside:
|
||||
ext_below, ext_above = ext_above, ext_below
|
||||
start = center + direction * (radius - ext_below)
|
||||
else:
|
||||
offset = ext_lines.offset
|
||||
if is_inside:
|
||||
ext_above = -ext_above
|
||||
offset = -offset
|
||||
start += direction * offset
|
||||
end = center + direction * (radius + ext_above)
|
||||
self.add_line(start, end, dxfattribs=dxfattribs)
|
||||
|
||||
def add_arrows(self) -> tuple[float, float]:
|
||||
"""Add arrows or ticks to dimension.
|
||||
|
||||
Returns: dimension start- and end angle offsets to adjust the
|
||||
dimension line
|
||||
|
||||
"""
|
||||
arrows = self.arrows
|
||||
attribs = arrows.dxfattribs()
|
||||
radius = self.dim_line_radius
|
||||
if abs(radius) < 1e-12:
|
||||
return 0.0, 0.0
|
||||
|
||||
start = self.center_of_arc + self.ext1_dir * radius
|
||||
end = self.center_of_arc + self.ext2_dir * radius
|
||||
angle1 = self.ext1_dir.orthogonal().angle_deg
|
||||
angle2 = self.ext2_dir.orthogonal().angle_deg
|
||||
outside = self.arrows_outside
|
||||
arrow1 = not arrows.suppress1
|
||||
arrow2 = not arrows.suppress2
|
||||
start_angle_offset = 0.0
|
||||
end_angle_offset = 0.0
|
||||
if arrows.tick_size > 0.0: # oblique stroke, but double the size
|
||||
if arrow1:
|
||||
self.add_blockref(
|
||||
ARROWS.oblique,
|
||||
insert=start,
|
||||
rotation=angle1,
|
||||
scale=arrows.tick_size * 2.0,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
if arrow2:
|
||||
self.add_blockref(
|
||||
ARROWS.oblique,
|
||||
insert=end,
|
||||
rotation=angle2,
|
||||
scale=arrows.tick_size * 2.0,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
else:
|
||||
arrow_size = arrows.arrow_size
|
||||
# Note: The arrow blocks are correct as they are!
|
||||
# The arrow head is tilted to match the connection point of the
|
||||
# dimension line (even for datum arrows).
|
||||
# tilting angle = 1/2 of the arc angle defined by the arrow length
|
||||
arrow_tilt: float = arrow_size / radius * 0.5 * DEG
|
||||
start_angle = angle1 + 180.0
|
||||
end_angle = angle2
|
||||
if outside:
|
||||
start_angle += 180.0
|
||||
end_angle += 180.0
|
||||
arrow_tilt = -arrow_tilt
|
||||
scale = arrow_size
|
||||
if arrow1:
|
||||
self.add_blockref(
|
||||
arrows.arrow1_name,
|
||||
insert=start,
|
||||
scale=scale,
|
||||
rotation=start_angle + arrow_tilt,
|
||||
dxfattribs=attribs,
|
||||
) # reverse
|
||||
if arrow2:
|
||||
self.add_blockref(
|
||||
arrows.arrow2_name,
|
||||
insert=end,
|
||||
scale=scale,
|
||||
rotation=end_angle - arrow_tilt,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
if not outside:
|
||||
# arrows inside extension lines:
|
||||
# adjust angles for the remaining dimension line
|
||||
if arrow1:
|
||||
start_angle_offset = (
|
||||
arrow_length(arrows.arrow1_name, arrow_size) / radius
|
||||
)
|
||||
if arrow2:
|
||||
end_angle_offset = (
|
||||
arrow_length(arrows.arrow2_name, arrow_size) / radius
|
||||
)
|
||||
return start_angle_offset, end_angle_offset
|
||||
|
||||
def add_dimension_line(
|
||||
self,
|
||||
start_offset: float,
|
||||
end_offset: float,
|
||||
) -> None:
|
||||
# Start- and end angle adjustments have to be limited between the
|
||||
# extension lines.
|
||||
# Negative offset extends the dimension line outside!
|
||||
start_angle: float = self.start_angle_rad
|
||||
end_angle: float = self.end_angle_rad
|
||||
arrows = self.arrows
|
||||
size = arrows.arrow_size
|
||||
radius = self.dim_line_radius
|
||||
max_adjustment: float = abs(self.arc_angle_span_rad) / 2.0
|
||||
|
||||
if start_offset > max_adjustment:
|
||||
start_offset = 0.0
|
||||
if end_offset > max_adjustment:
|
||||
end_offset = 0.0
|
||||
|
||||
self.add_arc(
|
||||
self.center_of_arc,
|
||||
radius,
|
||||
start_angle + start_offset,
|
||||
end_angle - end_offset,
|
||||
dxfattribs=self.dimension_line.dxfattribs(),
|
||||
# hidden line detection if text is not placed outside:
|
||||
remove_hidden_lines=self.remove_hidden_lines_of_dimline,
|
||||
)
|
||||
if self.arrows_outside and not arrows.has_ticks:
|
||||
# add arrow extension lines
|
||||
start_offset, end_offset = arrow_offset_angles(
|
||||
arrows.arrow1_name, size, radius
|
||||
)
|
||||
self.add_arrow_extension_line(
|
||||
start_angle - end_offset,
|
||||
start_angle - start_offset,
|
||||
)
|
||||
start_offset, end_offset = arrow_offset_angles(
|
||||
arrows.arrow1_name, size, radius
|
||||
)
|
||||
self.add_arrow_extension_line(
|
||||
end_angle + start_offset,
|
||||
end_angle + end_offset,
|
||||
)
|
||||
|
||||
def add_arrow_extension_line(self, start_angle: float, end_angle: float):
|
||||
self.add_arc(
|
||||
self.center_of_arc,
|
||||
self.dim_line_radius,
|
||||
start_angle=start_angle,
|
||||
end_angle=end_angle,
|
||||
dxfattribs=self.dimension_line.dxfattribs(),
|
||||
)
|
||||
|
||||
def add_measurement_text(
|
||||
self, dim_text: str, pos: Vec2, rotation: float
|
||||
) -> None:
|
||||
"""Add measurement text to dimension BLOCK.
|
||||
|
||||
Args:
|
||||
dim_text: dimension text
|
||||
pos: text location
|
||||
rotation: text rotation in degrees
|
||||
|
||||
"""
|
||||
attribs = self.measurement.dxfattribs()
|
||||
self.add_text(dim_text, pos=pos, rotation=rotation, dxfattribs=attribs)
|
||||
|
||||
|
||||
class _AngularCommonBase(_CurvedDimensionLine):
|
||||
def init_tolerance(
|
||||
self, scale: float, measurement: Measurement
|
||||
) -> Tolerance:
|
||||
return AngularTolerance(
|
||||
self.dim_style,
|
||||
cap_height=measurement.text_height,
|
||||
width_factor=measurement.text_width_factor,
|
||||
dim_scale=scale,
|
||||
angle_units=measurement.angle_units,
|
||||
)
|
||||
|
||||
def init_measurement(self, color: int, scale: float) -> Measurement:
|
||||
return AngleMeasurement(
|
||||
self.dim_style, self.default_color, self.dim_scale
|
||||
)
|
||||
|
||||
def update_measurement(self) -> None:
|
||||
self.measurement.update(self.arc_angle_span_rad)
|
||||
|
||||
|
||||
class AngularDimension(_AngularCommonBase):
|
||||
"""
|
||||
Angular dimension line renderer. The dimension line is defined by two lines.
|
||||
|
||||
Supported render types:
|
||||
|
||||
- default location above
|
||||
- default location center
|
||||
- user defined location, text aligned with dimension line
|
||||
- user defined location horizontal text
|
||||
|
||||
Args:
|
||||
dimension: DIMENSION entity
|
||||
ucs: user defined coordinate system
|
||||
override: dimension style override management object
|
||||
|
||||
"""
|
||||
|
||||
# Required defpoints:
|
||||
# defpoint = start point of 1st leg (group code 10)
|
||||
# defpoint4 = end point of 1st leg (group code 15)
|
||||
# defpoint3 = start point of 2nd leg (group code 14)
|
||||
# defpoint2 = end point of 2nd leg (group code 13)
|
||||
# defpoint5 = location of dimension line (group code 16)
|
||||
|
||||
# unsupported or ignored features (at least by BricsCAD):
|
||||
# dimtih: text inside horizontal
|
||||
# dimtoh: text outside horizontal
|
||||
# dimjust: text position horizontal
|
||||
# dimdle: dimline extension
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
self.leg1_start = get_required_defpoint(dimension, "defpoint")
|
||||
self.leg1_end = get_required_defpoint(dimension, "defpoint4")
|
||||
self.leg2_start = get_required_defpoint(dimension, "defpoint3")
|
||||
self.leg2_end = get_required_defpoint(dimension, "defpoint2")
|
||||
self.dim_line_location = get_required_defpoint(dimension, "defpoint5")
|
||||
super().__init__(dimension, ucs, override)
|
||||
# The extension line parameters depending on the location of the
|
||||
# dimension line related to the definition point.
|
||||
# Detect the extension start point.
|
||||
# Which definition point is closer to the dimension line:
|
||||
self.ext1_start = detect_closer_defpoint(
|
||||
direction=self.ext1_dir,
|
||||
base=self.dim_line_location,
|
||||
p1=self.leg1_start,
|
||||
p2=self.leg1_end,
|
||||
)
|
||||
self.ext2_start = detect_closer_defpoint(
|
||||
direction=self.ext2_dir,
|
||||
base=self.dim_line_location,
|
||||
p1=self.leg2_start,
|
||||
p2=self.leg2_end,
|
||||
)
|
||||
|
||||
def get_defpoints(self) -> list[Vec2]:
|
||||
return [
|
||||
self.leg1_start,
|
||||
self.leg1_end,
|
||||
self.leg2_start,
|
||||
self.leg2_end,
|
||||
self.dim_line_location,
|
||||
]
|
||||
|
||||
def get_center_of_arc(self) -> Vec2:
|
||||
center = intersection_line_line_2d(
|
||||
(self.leg1_start, self.leg1_end),
|
||||
(self.leg2_start, self.leg2_end),
|
||||
)
|
||||
if center is None:
|
||||
logger.warning(
|
||||
f"Invalid colinear or parallel angle legs found in {self.dimension})"
|
||||
)
|
||||
# This case can not be created by the GUI in BricsCAD, but DXF
|
||||
# files can contain any shit!
|
||||
# The interpolation of the end-points is an arbitrary choice and
|
||||
# maybe not the best choice!
|
||||
center = self.leg1_end.lerp(self.leg2_end)
|
||||
return center
|
||||
|
||||
def get_dim_line_radius(self) -> float:
|
||||
return (self.dim_line_location - self.center_of_arc).magnitude
|
||||
|
||||
def get_ext1_dir(self) -> Vec2:
|
||||
center = self.center_of_arc
|
||||
start = (
|
||||
self.leg1_end
|
||||
if self.leg1_start.isclose(center)
|
||||
else self.leg1_start
|
||||
)
|
||||
return (start - center).normalize()
|
||||
|
||||
def get_ext2_dir(self) -> Vec2:
|
||||
center = self.center_of_arc
|
||||
start = (
|
||||
self.leg2_end
|
||||
if self.leg2_start.isclose(center)
|
||||
else self.leg2_start
|
||||
)
|
||||
return (start - center).normalize()
|
||||
|
||||
|
||||
class Angular3PDimension(_AngularCommonBase):
|
||||
"""
|
||||
Angular dimension line renderer. The dimension line is defined by three
|
||||
points.
|
||||
|
||||
Supported render types:
|
||||
|
||||
- default location above
|
||||
- default location center
|
||||
- user defined location, text aligned with dimension line
|
||||
- user defined location horizontal text
|
||||
|
||||
Args:
|
||||
dimension: DIMENSION entity
|
||||
ucs: user defined coordinate system
|
||||
override: dimension style override management object
|
||||
|
||||
"""
|
||||
|
||||
# Required defpoints:
|
||||
# defpoint = location of dimension line (group code 10)
|
||||
# defpoint2 = 1st leg (group code 13)
|
||||
# defpoint3 = 2nd leg (group code 14)
|
||||
# defpoint4 = center of angle (group code 15)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
self.dim_line_location = get_required_defpoint(dimension, "defpoint")
|
||||
self.leg1_start = get_required_defpoint(dimension, "defpoint2")
|
||||
self.leg2_start = get_required_defpoint(dimension, "defpoint3")
|
||||
self.center_of_arc = get_required_defpoint(dimension, "defpoint4")
|
||||
super().__init__(dimension, ucs, override)
|
||||
self.ext1_start = self.leg1_start
|
||||
self.ext2_start = self.leg2_start
|
||||
|
||||
def get_defpoints(self) -> list[Vec2]:
|
||||
return [
|
||||
self.dim_line_location,
|
||||
self.leg1_start,
|
||||
self.leg2_start,
|
||||
self.center_of_arc,
|
||||
]
|
||||
|
||||
def get_center_of_arc(self) -> Vec2:
|
||||
return self.center_of_arc
|
||||
|
||||
def get_dim_line_radius(self) -> float:
|
||||
return (self.dim_line_location - self.center_of_arc).magnitude
|
||||
|
||||
def get_ext1_dir(self) -> Vec2:
|
||||
return (self.leg1_start - self.center_of_arc).normalize()
|
||||
|
||||
def get_ext2_dir(self) -> Vec2:
|
||||
return (self.leg2_start - self.center_of_arc).normalize()
|
||||
|
||||
|
||||
class ArcLengthMeasurement(LengthMeasurement):
|
||||
def format_text(self, value: float) -> str:
|
||||
text = format_text(
|
||||
value=value,
|
||||
dimrnd=self.text_round,
|
||||
dimdec=self.decimal_places,
|
||||
dimzin=self.suppress_zeros,
|
||||
dimdsep=self.decimal_separator,
|
||||
)
|
||||
if self.has_arc_length_prefix:
|
||||
text = ARC_PREFIX + text
|
||||
if self.text_post_process_format:
|
||||
text = apply_dimpost(text, self.text_post_process_format)
|
||||
return text
|
||||
|
||||
|
||||
class ArcLengthDimension(_CurvedDimensionLine):
|
||||
"""Arc length dimension line renderer.
|
||||
Requires DXF R2004.
|
||||
|
||||
Supported render types:
|
||||
|
||||
- default location above
|
||||
- default location center
|
||||
- user defined location, text aligned with dimension line
|
||||
- user defined location horizontal text
|
||||
|
||||
Args:
|
||||
dimension: DXF entity DIMENSION
|
||||
ucs: user defined coordinate system
|
||||
override: dimension style override management object
|
||||
|
||||
"""
|
||||
|
||||
# Required defpoints:
|
||||
# defpoint = location of dimension line (group code 10)
|
||||
# defpoint2 = 1st arc point (group code 13)
|
||||
# defpoint3 = 2nd arc point (group code 14)
|
||||
# defpoint4 = center of arc (group code 15)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
self.dim_line_location = get_required_defpoint(dimension, "defpoint")
|
||||
self.leg1_start = get_required_defpoint(dimension, "defpoint2")
|
||||
self.leg2_start = get_required_defpoint(dimension, "defpoint3")
|
||||
self.center_of_arc = get_required_defpoint(dimension, "defpoint4")
|
||||
self.arc_radius = (self.leg1_start - self.center_of_arc).magnitude
|
||||
super().__init__(dimension, ucs, override)
|
||||
self.ext1_start = self.leg1_start
|
||||
self.ext2_start = self.leg2_start
|
||||
|
||||
def get_defpoints(self) -> list[Vec2]:
|
||||
return [
|
||||
self.dim_line_location,
|
||||
self.leg1_start,
|
||||
self.leg2_start,
|
||||
self.center_of_arc,
|
||||
]
|
||||
|
||||
def init_measurement(self, color: int, scale: float) -> Measurement:
|
||||
return ArcLengthMeasurement(
|
||||
self.dim_style, self.default_color, self.dim_scale
|
||||
)
|
||||
|
||||
def get_center_of_arc(self) -> Vec2:
|
||||
return self.center_of_arc
|
||||
|
||||
def get_dim_line_radius(self) -> float:
|
||||
return (self.dim_line_location - self.center_of_arc).magnitude
|
||||
|
||||
def get_ext1_dir(self) -> Vec2:
|
||||
return (self.leg1_start - self.center_of_arc).normalize()
|
||||
|
||||
def get_ext2_dir(self) -> Vec2:
|
||||
return (self.leg2_start - self.center_of_arc).normalize()
|
||||
|
||||
def update_measurement(self) -> None:
|
||||
angle = arc_angle_span_rad(self.start_angle_rad, self.end_angle_rad)
|
||||
arc_length = angle * self.arc_radius
|
||||
self.measurement.update(arc_length)
|
||||
|
||||
|
||||
def detect_closer_defpoint(
|
||||
direction: Vec2, base: Vec2, p1: Vec2, p2: Vec2
|
||||
) -> Vec2:
|
||||
# Calculate the projected distance onto the (normalized) direction vector:
|
||||
d0 = direction.dot(base)
|
||||
d1 = direction.dot(p1)
|
||||
d2 = direction.dot(p2)
|
||||
# Which defpoint is closer to the base point (d0)?
|
||||
if abs(d1 - d0) <= abs(d2 - d0):
|
||||
return p1
|
||||
return p2
|
||||
|
||||
|
||||
def arrow_offset_angles(
|
||||
arrow_name: str, size: float, radius: float
|
||||
) -> tuple[float, float]:
|
||||
start_offset: float = 0.0
|
||||
end_offset: float = size / radius
|
||||
length = arrow_length(arrow_name, size)
|
||||
if length > 0.0:
|
||||
start_offset = length / radius
|
||||
end_offset *= 2.0
|
||||
return start_offset, end_offset
|
||||
175
.venv/lib/python3.12/site-packages/ezdxf/render/dim_diameter.py
Normal file
175
.venv/lib/python3.12/site-packages/ezdxf/render/dim_diameter.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# Copyright (c) 2018-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.math import Vec2, UCS
|
||||
from ezdxf.entities.dimstyleoverride import DimStyleOverride
|
||||
|
||||
from .dim_radius import (
|
||||
RadiusDimension,
|
||||
add_center_mark,
|
||||
Measurement,
|
||||
RadiusMeasurement,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import Dimension
|
||||
|
||||
PREFIX = "Ø"
|
||||
|
||||
|
||||
class DiameterDimension(RadiusDimension):
|
||||
"""
|
||||
Diameter dimension line renderer.
|
||||
|
||||
Supported render types:
|
||||
- default location inside, text aligned with diameter dimension line
|
||||
- default location inside horizontal text
|
||||
- default location outside, text aligned with diameter dimension line
|
||||
- default location outside horizontal text
|
||||
- user defined location, text aligned with diameter dimension line
|
||||
- user defined location horizontal text
|
||||
|
||||
Args:
|
||||
dimension: DXF entity DIMENSION
|
||||
ucs: user defined coordinate system
|
||||
override: dimension style override management object
|
||||
|
||||
"""
|
||||
|
||||
def init_measurement(self, color: int, scale: float) -> Measurement:
|
||||
return RadiusMeasurement(self.dim_style, color, scale, PREFIX)
|
||||
|
||||
def _center(self):
|
||||
return Vec2(self.dimension.dxf.defpoint).lerp(
|
||||
self.dimension.dxf.defpoint4
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
# Diameter dimension has the same styles for inside text as radius dimension, except for the
|
||||
# measurement text
|
||||
super().__init__(dimension, ucs, override)
|
||||
self.point_on_circle2 = Vec2(self.dimension.dxf.defpoint)
|
||||
|
||||
def add_text(
|
||||
self, text: str, pos: Vec2, rotation: float, dxfattribs
|
||||
) -> None:
|
||||
# escape diameter sign
|
||||
super().add_text(text.replace(PREFIX, "%%c"), pos, rotation, dxfattribs)
|
||||
|
||||
def get_default_text_location(self) -> Vec2:
|
||||
"""Returns default text midpoint based on `text_valign` and
|
||||
`text_outside`.
|
||||
"""
|
||||
measurement = self.measurement
|
||||
if measurement.text_is_outside and measurement.text_outside_horizontal:
|
||||
return super().get_default_text_location()
|
||||
|
||||
text_direction = Vec2.from_deg_angle(measurement.text_rotation)
|
||||
vertical_direction = text_direction.orthogonal(ccw=True)
|
||||
vertical_distance = measurement.text_vertical_distance()
|
||||
if measurement.text_is_inside:
|
||||
text_midpoint = self.center
|
||||
else:
|
||||
hdist = (
|
||||
self._total_text_width / 2.0
|
||||
+ self.arrows.arrow_size
|
||||
+ measurement.text_gap
|
||||
)
|
||||
text_midpoint = self.point_on_circle + (self.dim_line_vec * hdist)
|
||||
return text_midpoint + (vertical_direction * vertical_distance)
|
||||
|
||||
def _add_arrow_1(self, rotate=False):
|
||||
if not self.arrows.suppress1:
|
||||
return self.add_arrow(self.point_on_circle, rotate=rotate)
|
||||
else:
|
||||
return self.point_on_circle
|
||||
|
||||
def _add_arrow_2(self, rotate=True):
|
||||
if not self.arrows.suppress2:
|
||||
return self.add_arrow(self.point_on_circle2, rotate=rotate)
|
||||
else:
|
||||
return self.point_on_circle2
|
||||
|
||||
def render_default_location(self) -> None:
|
||||
"""Create dimension geometry at the default dimension line locations."""
|
||||
measurement = self.measurement
|
||||
if measurement.text_is_outside:
|
||||
connection_point1 = self._add_arrow_1(rotate=True)
|
||||
if self.outside_text_force_dimline:
|
||||
self.add_diameter_dim_line(
|
||||
connection_point1, self._add_arrow_2()
|
||||
)
|
||||
else:
|
||||
add_center_mark(self)
|
||||
if measurement.text_outside_horizontal:
|
||||
self.add_horiz_ext_line_default(connection_point1)
|
||||
else:
|
||||
self.add_radial_ext_line_default(connection_point1)
|
||||
else:
|
||||
connection_point1 = self._add_arrow_1(rotate=False)
|
||||
if measurement.text_movement_rule == 1:
|
||||
# move text, add leader -> dimline from text to point on circle
|
||||
self.add_radial_dim_line_from_text(
|
||||
self.center, connection_point1
|
||||
)
|
||||
add_center_mark(self)
|
||||
else:
|
||||
# dimline from center to point on circle
|
||||
self.add_diameter_dim_line(
|
||||
connection_point1, self._add_arrow_2()
|
||||
)
|
||||
|
||||
def render_user_location(self) -> None:
|
||||
"""Create dimension geometry at user defined dimension locations."""
|
||||
measurement = self.measurement
|
||||
preserve_outside = measurement.text_is_outside
|
||||
leader = measurement.text_movement_rule != 2
|
||||
if not leader:
|
||||
# render dimension line like text inside
|
||||
measurement.text_is_outside = False
|
||||
# add arrow symbol (block references)
|
||||
connection_point1 = self._add_arrow_1(
|
||||
rotate=measurement.text_is_outside
|
||||
)
|
||||
|
||||
if measurement.text_is_outside:
|
||||
if self.outside_text_force_dimline:
|
||||
self.add_radial_dim_line(self.point_on_circle)
|
||||
else:
|
||||
add_center_mark(self)
|
||||
if measurement.text_outside_horizontal:
|
||||
self.add_horiz_ext_line_user(connection_point1)
|
||||
else:
|
||||
self.add_radial_ext_line_user(connection_point1)
|
||||
else:
|
||||
if measurement.text_inside_horizontal:
|
||||
self.add_horiz_ext_line_user(connection_point1)
|
||||
else:
|
||||
if measurement.text_movement_rule == 2: # move text, no leader!
|
||||
# dimline across the circle
|
||||
connection_point2 = self._add_arrow_2(rotate=True)
|
||||
self.add_line(
|
||||
connection_point1,
|
||||
connection_point2,
|
||||
dxfattribs=self.dimension_line.dxfattribs(),
|
||||
remove_hidden_lines=True,
|
||||
)
|
||||
else:
|
||||
# move text, add leader -> dimline from text to point on circle
|
||||
self.add_radial_dim_line_from_text(
|
||||
measurement.user_location, connection_point1
|
||||
)
|
||||
add_center_mark(self)
|
||||
|
||||
measurement.text_is_outside = preserve_outside
|
||||
|
||||
def add_diameter_dim_line(self, start: Vec2, end: Vec2) -> None:
|
||||
"""Add diameter dimension line."""
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
self.add_line(start, end, dxfattribs=attribs, remove_hidden_lines=True)
|
||||
649
.venv/lib/python3.12/site-packages/ezdxf/render/dim_linear.py
Normal file
649
.venv/lib/python3.12/site-packages/ezdxf/render/dim_linear.py
Normal file
@@ -0,0 +1,649 @@
|
||||
# Copyright (c) 2018-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, cast, Optional
|
||||
import math
|
||||
from ezdxf.math import Vec3, Vec2, UVec, ConstructionRay, UCS
|
||||
from ezdxf.render.arrows import ARROWS, connection_point
|
||||
from ezdxf.entities.dimstyleoverride import DimStyleOverride
|
||||
|
||||
from .dim_base import (
|
||||
BaseDimensionRenderer,
|
||||
LengthMeasurement,
|
||||
Measurement,
|
||||
compile_mtext,
|
||||
order_leader_points,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import Dimension
|
||||
from ezdxf.eztypes import GenericLayoutType
|
||||
|
||||
|
||||
class LinearDimension(BaseDimensionRenderer):
|
||||
"""Linear dimension line renderer, used for horizontal, vertical, rotated
|
||||
and aligned DIMENSION entities.
|
||||
|
||||
Args:
|
||||
dimension: DXF entity DIMENSION
|
||||
ucs: user defined coordinate system
|
||||
override: dimension style override management object
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
super().__init__(dimension, ucs, override)
|
||||
measurement = self.measurement
|
||||
if measurement.text_movement_rule == 0:
|
||||
# moves the dimension line with dimension text, this makes no sense
|
||||
# for ezdxf (just set `base` argument)
|
||||
measurement.text_movement_rule = 2
|
||||
|
||||
self.oblique_angle: float = self.dimension.get_dxf_attrib(
|
||||
"oblique_angle", 90
|
||||
)
|
||||
self.dim_line_angle: float = self.dimension.get_dxf_attrib("angle", 0)
|
||||
self.dim_line_angle_rad: float = math.radians(self.dim_line_angle)
|
||||
self.ext_line_angle: float = self.dim_line_angle + self.oblique_angle
|
||||
self.ext_line_angle_rad: float = math.radians(self.ext_line_angle)
|
||||
|
||||
# text is aligned to dimension line
|
||||
measurement.text_rotation = self.dim_line_angle
|
||||
# text above extension line, is always aligned with extension lines
|
||||
if measurement.text_halign in (3, 4):
|
||||
measurement.text_rotation = self.ext_line_angle
|
||||
|
||||
self.ext1_line_start = Vec2(self.dimension.dxf.defpoint2)
|
||||
self.ext2_line_start = Vec2(self.dimension.dxf.defpoint3)
|
||||
|
||||
ext1_ray = ConstructionRay(
|
||||
self.ext1_line_start, angle=self.ext_line_angle_rad
|
||||
)
|
||||
ext2_ray = ConstructionRay(
|
||||
self.ext2_line_start, angle=self.ext_line_angle_rad
|
||||
)
|
||||
dim_line_ray = ConstructionRay(
|
||||
self.dimension.dxf.defpoint, angle=self.dim_line_angle_rad
|
||||
)
|
||||
|
||||
self.dim_line_start: Vec2 = dim_line_ray.intersect(ext1_ray)
|
||||
self.dim_line_end: Vec2 = dim_line_ray.intersect(ext2_ray)
|
||||
self.dim_line_center: Vec2 = self.dim_line_start.lerp(self.dim_line_end)
|
||||
|
||||
if self.dim_line_start == self.dim_line_end:
|
||||
self.dim_line_vec = Vec2.from_angle(self.dim_line_angle_rad)
|
||||
else:
|
||||
self.dim_line_vec = (
|
||||
self.dim_line_end - self.dim_line_start
|
||||
).normalize()
|
||||
|
||||
# set dimension defpoint to expected location - 3D vertex required!
|
||||
self.dimension.dxf.defpoint = Vec3(self.dim_line_start)
|
||||
|
||||
raw_measurement = (self.dim_line_end - self.dim_line_start).magnitude
|
||||
measurement.update(raw_measurement)
|
||||
|
||||
# only for linear dimension in multi point mode
|
||||
self.multi_point_mode = self.dim_style.pop("multi_point_mode", False)
|
||||
|
||||
# 1 .. move wide text up
|
||||
# 2 .. move wide text down
|
||||
# None .. ignore
|
||||
self.move_wide_text: Optional[bool] = self.dim_style.pop(
|
||||
"move_wide_text", None
|
||||
)
|
||||
|
||||
# actual text width in drawing units
|
||||
self._total_text_width: float = 0
|
||||
|
||||
# arrows
|
||||
self.required_arrows_space: float = (
|
||||
2 * self.arrows.arrow_size + measurement.text_gap
|
||||
)
|
||||
self.arrows_outside: bool = self.required_arrows_space > raw_measurement
|
||||
|
||||
# text location and rotation
|
||||
if measurement.text:
|
||||
# text width and required space
|
||||
self._total_text_width = self.total_text_width()
|
||||
if self.tol.has_limits:
|
||||
# limits show the upper and lower limit of the measurement as
|
||||
# stacked values and with the size of tolerances
|
||||
self.tol.update_limits(self.measurement.value)
|
||||
|
||||
if self.multi_point_mode:
|
||||
# ezdxf has total control about vertical text position in multi
|
||||
# point mode
|
||||
measurement.text_vertical_position = 0.0
|
||||
|
||||
if (
|
||||
measurement.text_valign == 0
|
||||
and abs(measurement.text_vertical_position) < 0.7
|
||||
):
|
||||
# vertical centered text needs also space for arrows
|
||||
required_space = (
|
||||
self._total_text_width + 2 * self.arrows.arrow_size
|
||||
)
|
||||
else:
|
||||
required_space = self._total_text_width
|
||||
measurement.is_wide_text = required_space > raw_measurement
|
||||
|
||||
if not measurement.force_text_inside:
|
||||
# place text outside if wide text and not forced inside
|
||||
measurement.text_is_outside = measurement.is_wide_text
|
||||
elif measurement.is_wide_text and measurement.text_halign < 3:
|
||||
# center wide text horizontal
|
||||
measurement.text_halign = 0
|
||||
|
||||
# use relative text shift to move wide text up or down in multi
|
||||
# point mode
|
||||
if (
|
||||
self.multi_point_mode
|
||||
and measurement.is_wide_text
|
||||
and self.move_wide_text
|
||||
):
|
||||
shift_value = measurement.text_height + measurement.text_gap
|
||||
if self.move_wide_text == 1: # move text up
|
||||
measurement.text_shift_v = shift_value
|
||||
if (
|
||||
measurement.vertical_placement == -1
|
||||
): # text below dimension line
|
||||
# shift again
|
||||
measurement.text_shift_v += shift_value
|
||||
elif self.move_wide_text == 2: # move text down
|
||||
measurement.text_shift_v = -shift_value
|
||||
if (
|
||||
measurement.vertical_placement == 1
|
||||
): # text above dimension line
|
||||
# shift again
|
||||
measurement.text_shift_v -= shift_value
|
||||
|
||||
# get final text location - no altering after this line
|
||||
measurement.text_location = self.get_text_location()
|
||||
|
||||
# text rotation override
|
||||
rotation: float = measurement.text_rotation
|
||||
if measurement.user_text_rotation is not None:
|
||||
rotation = measurement.user_text_rotation
|
||||
elif (
|
||||
measurement.text_is_outside
|
||||
and measurement.text_outside_horizontal
|
||||
):
|
||||
rotation = 0.0
|
||||
elif (
|
||||
measurement.text_is_inside
|
||||
and measurement.text_inside_horizontal
|
||||
):
|
||||
rotation = 0.0
|
||||
measurement.text_rotation = rotation
|
||||
|
||||
text_box = self.init_text_box()
|
||||
self.geometry.set_text_box(text_box)
|
||||
if measurement.has_leader:
|
||||
p1, p2, *_ = text_box.corners
|
||||
self.leader1, self.leader2 = order_leader_points(
|
||||
self.dim_line_center, p1, p2
|
||||
)
|
||||
# not exact what BricsCAD (AutoCAD) expect, but close enough
|
||||
self.dimension.dxf.text_midpoint = self.leader1
|
||||
else:
|
||||
# write final text location into DIMENSION entity
|
||||
self.dimension.dxf.text_midpoint = measurement.text_location
|
||||
|
||||
def init_measurement(self, color: int, scale: float) -> Measurement:
|
||||
return LengthMeasurement(
|
||||
self.dim_style, self.default_color, self.dim_scale
|
||||
)
|
||||
|
||||
def render(self, block: GenericLayoutType) -> None:
|
||||
"""Main method to create dimension geometry of basic DXF entities in the
|
||||
associated BLOCK layout.
|
||||
|
||||
Args:
|
||||
block: target BLOCK for rendering
|
||||
|
||||
"""
|
||||
# call required to setup some requirements
|
||||
super().render(block)
|
||||
|
||||
# add extension line 1
|
||||
ext_lines = self.extension_lines
|
||||
measurement = self.measurement
|
||||
if not ext_lines.suppress1:
|
||||
above_ext_line1 = measurement.text_halign == 3
|
||||
start, end = self.extension_line_points(
|
||||
self.ext1_line_start, self.dim_line_start, above_ext_line1
|
||||
)
|
||||
self.add_line(start, end, dxfattribs=ext_lines.dxfattribs(1))
|
||||
|
||||
# add extension line 2
|
||||
if not ext_lines.suppress2:
|
||||
above_ext_line2 = measurement.text_halign == 4
|
||||
start, end = self.extension_line_points(
|
||||
self.ext2_line_start, self.dim_line_end, above_ext_line2
|
||||
)
|
||||
self.add_line(start, end, dxfattribs=ext_lines.dxfattribs(2))
|
||||
|
||||
# add arrow symbols (block references), also adjust dimension line start
|
||||
# and end point
|
||||
dim_line_start, dim_line_end = self.add_arrows()
|
||||
|
||||
# add dimension line
|
||||
self.add_dimension_line(dim_line_start, dim_line_end)
|
||||
|
||||
# add measurement text as last entity to see text fill properly
|
||||
if measurement.text:
|
||||
if self.geometry.supports_dxf_r2000:
|
||||
text = compile_mtext(measurement, self.tol)
|
||||
else:
|
||||
text = measurement.text
|
||||
self.add_measurement_text(
|
||||
text, measurement.text_location, measurement.text_rotation
|
||||
)
|
||||
if measurement.has_leader:
|
||||
self.add_leader(
|
||||
self.dim_line_center, self.leader1, self.leader2
|
||||
)
|
||||
|
||||
# add POINT entities at definition points
|
||||
self.geometry.add_defpoints(
|
||||
[self.dim_line_start, self.ext1_line_start, self.ext2_line_start]
|
||||
)
|
||||
|
||||
def get_text_location(self) -> Vec2:
|
||||
"""Get text midpoint in UCS from user defined location or default text
|
||||
location.
|
||||
|
||||
"""
|
||||
# apply relative text shift as user location override without leader
|
||||
measurement = self.measurement
|
||||
if measurement.has_relative_text_movement:
|
||||
location = self.default_text_location()
|
||||
location = measurement.apply_text_shift(
|
||||
location, measurement.text_rotation
|
||||
)
|
||||
self.location_override(location)
|
||||
|
||||
if measurement.user_location is not None:
|
||||
location = measurement.user_location
|
||||
if measurement.relative_user_location:
|
||||
location = self.dim_line_center + location
|
||||
# define overridden text location as outside
|
||||
measurement.text_is_outside = True
|
||||
else:
|
||||
location = self.default_text_location()
|
||||
|
||||
return location
|
||||
|
||||
def default_text_location(self) -> Vec2:
|
||||
"""Calculate default text location in UCS based on `self.text_halign`,
|
||||
`self.text_valign` and `self.text_outside`
|
||||
|
||||
"""
|
||||
start = self.dim_line_start
|
||||
end = self.dim_line_end
|
||||
measurement = self.measurement
|
||||
halign = measurement.text_halign
|
||||
# positions the text above and aligned with the first/second extension line
|
||||
ext_lines = self.extension_lines
|
||||
if halign in (3, 4):
|
||||
# horizontal location
|
||||
hdist = measurement.text_gap + measurement.text_height / 2.0
|
||||
hvec = self.dim_line_vec * hdist
|
||||
location = (start if halign == 3 else end) - hvec
|
||||
# vertical location
|
||||
vdist = ext_lines.extension_above + self._total_text_width / 2.0
|
||||
location += Vec2.from_deg_angle(self.ext_line_angle).normalize(
|
||||
vdist
|
||||
)
|
||||
else:
|
||||
# relocate outside text to center location
|
||||
if measurement.text_is_outside:
|
||||
halign = 0
|
||||
|
||||
if halign == 0:
|
||||
location = self.dim_line_center # center of dimension line
|
||||
else:
|
||||
hdist = (
|
||||
self._total_text_width / 2.0
|
||||
+ self.arrows.arrow_size
|
||||
+ measurement.text_gap
|
||||
)
|
||||
if (
|
||||
halign == 1
|
||||
): # positions the text next to the first extension line
|
||||
location = start + (self.dim_line_vec * hdist)
|
||||
else: # positions the text next to the second extension line
|
||||
location = end - (self.dim_line_vec * hdist)
|
||||
|
||||
if measurement.text_is_outside: # move text up
|
||||
vdist = (
|
||||
ext_lines.extension_above
|
||||
+ measurement.text_gap
|
||||
+ measurement.text_height / 2.0
|
||||
)
|
||||
else:
|
||||
# distance from extension line to text midpoint
|
||||
vdist = measurement.text_vertical_distance()
|
||||
location += self.dim_line_vec.orthogonal().normalize(vdist)
|
||||
|
||||
return location
|
||||
|
||||
def add_arrows(self) -> tuple[Vec2, Vec2]:
|
||||
"""
|
||||
Add arrows or ticks to dimension.
|
||||
|
||||
Returns: dimension line connection points
|
||||
|
||||
"""
|
||||
arrows = self.arrows
|
||||
attribs = arrows.dxfattribs()
|
||||
start = self.dim_line_start
|
||||
end = self.dim_line_end
|
||||
outside = self.arrows_outside
|
||||
arrow1 = not arrows.suppress1
|
||||
arrow2 = not arrows.suppress2
|
||||
|
||||
if arrows.tick_size > 0.0: # oblique stroke, but double the size
|
||||
if arrow1:
|
||||
self.add_blockref(
|
||||
ARROWS.oblique,
|
||||
insert=start,
|
||||
rotation=self.dim_line_angle,
|
||||
scale=arrows.tick_size * 2.0,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
if arrow2:
|
||||
self.add_blockref(
|
||||
ARROWS.oblique,
|
||||
insert=end,
|
||||
rotation=self.dim_line_angle,
|
||||
scale=arrows.tick_size * 2.0,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
else:
|
||||
scale = arrows.arrow_size
|
||||
start_angle = self.dim_line_angle + 180.0
|
||||
end_angle = self.dim_line_angle
|
||||
if outside:
|
||||
start_angle, end_angle = end_angle, start_angle
|
||||
|
||||
if arrow1:
|
||||
self.add_blockref(
|
||||
arrows.arrow1_name,
|
||||
insert=start,
|
||||
scale=scale,
|
||||
rotation=start_angle,
|
||||
dxfattribs=attribs,
|
||||
) # reverse
|
||||
if arrow2:
|
||||
self.add_blockref(
|
||||
arrows.arrow2_name,
|
||||
insert=end,
|
||||
scale=scale,
|
||||
rotation=end_angle,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
|
||||
if not outside:
|
||||
# arrows inside extension lines: adjust connection points for
|
||||
# the remaining dimension line
|
||||
if arrow1:
|
||||
start = connection_point(
|
||||
arrows.arrow1_name, start, scale, start_angle
|
||||
)
|
||||
if arrow2:
|
||||
end = connection_point(
|
||||
arrows.arrow2_name, end, scale, end_angle
|
||||
)
|
||||
else:
|
||||
# add additional extension lines to arrows placed outside of
|
||||
# dimension extension lines
|
||||
self.add_arrow_extension_lines()
|
||||
return start, end
|
||||
|
||||
def add_arrow_extension_lines(self):
|
||||
"""Add extension lines to arrows placed outside of dimension extension
|
||||
lines. Called by `self.add_arrows()`.
|
||||
|
||||
"""
|
||||
|
||||
def has_arrow_extension(name: str) -> bool:
|
||||
return (
|
||||
(name is not None)
|
||||
and (name in ARROWS)
|
||||
and (name not in ARROWS.ORIGIN_ZERO)
|
||||
)
|
||||
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
arrows = self.arrows
|
||||
start = self.dim_line_start
|
||||
end = self.dim_line_end
|
||||
arrow_size = arrows.arrow_size
|
||||
|
||||
if not arrows.suppress1 and has_arrow_extension(arrows.arrow1_name):
|
||||
self.add_line(
|
||||
start - self.dim_line_vec * arrow_size,
|
||||
start - self.dim_line_vec * (2 * arrow_size),
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
|
||||
if not arrows.suppress2 and has_arrow_extension(arrows.arrow2_name):
|
||||
self.add_line(
|
||||
end + self.dim_line_vec * arrow_size,
|
||||
end + self.dim_line_vec * (2 * arrow_size),
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
|
||||
def add_measurement_text(
|
||||
self, dim_text: str, pos: Vec2, rotation: float
|
||||
) -> None:
|
||||
"""Add measurement text to dimension BLOCK.
|
||||
|
||||
Args:
|
||||
dim_text: dimension text
|
||||
pos: text location
|
||||
rotation: text rotation in degrees
|
||||
|
||||
"""
|
||||
self.add_text(dim_text, pos, rotation, dict())
|
||||
|
||||
def add_dimension_line(self, start: Vec2, end: Vec2) -> None:
|
||||
"""Add dimension line to dimension BLOCK, adds extension DIMDLE if
|
||||
required, and uses DIMSD1 or DIMSD2 to suppress first or second part of
|
||||
dimension line. Removes line parts hidden by dimension text.
|
||||
|
||||
Args:
|
||||
start: dimension line start
|
||||
end: dimension line end
|
||||
|
||||
"""
|
||||
dim_line = self.dimension_line
|
||||
arrows = self.arrows
|
||||
extension = self.dim_line_vec * dim_line.extension
|
||||
ticks = arrows.has_ticks
|
||||
if ticks or ARROWS.has_extension_line(arrows.arrow1_name):
|
||||
start = start - extension
|
||||
if ticks or ARROWS.has_extension_line(arrows.arrow2_name):
|
||||
end = end + extension
|
||||
|
||||
attribs = dim_line.dxfattribs()
|
||||
|
||||
if dim_line.suppress1 or dim_line.suppress2:
|
||||
# TODO: results not as expected, but good enough
|
||||
# center should take into account text location
|
||||
center = start.lerp(end)
|
||||
if not dim_line.suppress1:
|
||||
self.add_line(
|
||||
start, center, dxfattribs=attribs, remove_hidden_lines=True
|
||||
)
|
||||
if not dim_line.suppress2:
|
||||
self.add_line(
|
||||
center, end, dxfattribs=attribs, remove_hidden_lines=True
|
||||
)
|
||||
else:
|
||||
self.add_line(
|
||||
start, end, dxfattribs=attribs, remove_hidden_lines=True
|
||||
)
|
||||
|
||||
def extension_line_points(
|
||||
self, start: Vec2, end: Vec2, text_above_extline=False
|
||||
) -> tuple[Vec2, Vec2]:
|
||||
"""Adjust start and end point of extension line by dimension variables
|
||||
DIMEXE, DIMEXO, DIMEXFIX, DIMEXLEN.
|
||||
|
||||
Args:
|
||||
start: start point of extension line (measurement point)
|
||||
end: end point at dimension line
|
||||
text_above_extline: True if text is above and aligned with extension line
|
||||
|
||||
Returns: adjusted start and end point
|
||||
|
||||
"""
|
||||
if start == end:
|
||||
direction = Vec2.from_deg_angle(self.ext_line_angle)
|
||||
else:
|
||||
direction = (end - start).normalize()
|
||||
if self.extension_lines.has_fixed_length:
|
||||
start = end - (direction * self.extension_lines.length_below)
|
||||
else:
|
||||
start = start + direction * self.extension_lines.offset
|
||||
extension = self.extension_lines.extension_above
|
||||
if text_above_extline:
|
||||
extension += self._total_text_width
|
||||
end = end + direction * extension
|
||||
return start, end
|
||||
|
||||
def transform_ucs_to_wcs(self) -> None:
|
||||
"""Transforms dimension definition points into WCS or if required into
|
||||
OCS.
|
||||
|
||||
Can not be called in __init__(), because inherited classes may be need
|
||||
unmodified values.
|
||||
|
||||
"""
|
||||
|
||||
def from_ucs(attr, func):
|
||||
point = self.dimension.get_dxf_attrib(attr)
|
||||
self.dimension.set_dxf_attrib(attr, func(point))
|
||||
|
||||
ucs = self.geometry.ucs
|
||||
from_ucs("defpoint", ucs.to_wcs)
|
||||
from_ucs("defpoint2", ucs.to_wcs)
|
||||
from_ucs("defpoint3", ucs.to_wcs)
|
||||
from_ucs("text_midpoint", ucs.to_ocs)
|
||||
self.dimension.dxf.angle = ucs.to_ocs_angle_deg(
|
||||
self.dimension.dxf.angle
|
||||
)
|
||||
|
||||
|
||||
CAN_SUPPRESS_ARROW1 = {
|
||||
ARROWS.dot,
|
||||
ARROWS.dot_small,
|
||||
ARROWS.dot_blank,
|
||||
ARROWS.origin_indicator,
|
||||
ARROWS.origin_indicator_2,
|
||||
ARROWS.dot_smallblank,
|
||||
ARROWS.none,
|
||||
ARROWS.oblique,
|
||||
ARROWS.box_filled,
|
||||
ARROWS.box,
|
||||
ARROWS.integral,
|
||||
ARROWS.architectural_tick,
|
||||
}
|
||||
|
||||
|
||||
def sort_projected_points(
|
||||
points: Iterable[UVec], angle: float = 0
|
||||
) -> list[Vec2]:
|
||||
direction = Vec2.from_deg_angle(angle)
|
||||
projected_vectors = [(direction.project(Vec2(p)), p) for p in points]
|
||||
return [p for projection, p in sorted(projected_vectors)]
|
||||
|
||||
|
||||
def multi_point_linear_dimension(
|
||||
layout: GenericLayoutType,
|
||||
base: UVec,
|
||||
points: Iterable[UVec],
|
||||
angle: float = 0,
|
||||
ucs: Optional[UCS] = None,
|
||||
avoid_double_rendering: bool = True,
|
||||
dimstyle: str = "EZDXF",
|
||||
override: Optional[dict] = None,
|
||||
dxfattribs=None,
|
||||
discard=False,
|
||||
) -> None:
|
||||
"""Creates multiple DIMENSION entities for each point pair in `points`.
|
||||
Measurement points will be sorted by appearance on the dimension line
|
||||
vector.
|
||||
|
||||
Args:
|
||||
layout: target layout (model space, paper space or block)
|
||||
base: base point, any point on the dimension line vector will do
|
||||
points: iterable of measurement points
|
||||
angle: dimension line rotation in degrees (0=horizontal, 90=vertical)
|
||||
ucs: user defined coordinate system
|
||||
avoid_double_rendering: removes first extension line and arrow of
|
||||
following DIMENSION entity
|
||||
dimstyle: dimension style name
|
||||
override: dictionary of overridden dimension style attributes
|
||||
dxfattribs: DXF attributes for DIMENSION entities
|
||||
discard: discard rendering result for friendly CAD applications like
|
||||
BricsCAD to get a native and likely better rendering result.
|
||||
(does not work with AutoCAD)
|
||||
|
||||
"""
|
||||
|
||||
def suppress_arrow1(dimstyle_override) -> bool:
|
||||
arrow_name1, arrow_name2 = dimstyle_override.get_arrow_names()
|
||||
if (arrow_name1 is None) or (arrow_name1 in CAN_SUPPRESS_ARROW1):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
points = sort_projected_points(points, angle)
|
||||
base = Vec2(base)
|
||||
override = override or {}
|
||||
override["dimtix"] = 1 # do not place measurement text outside
|
||||
override["dimtvp"] = 0 # do not place measurement text outside
|
||||
override["multi_point_mode"] = True
|
||||
# 1 .. move wide text up; 2 .. move wide text down; None .. ignore
|
||||
# moving text down, looks best combined with text fill bg: DIMTFILL = 1
|
||||
move_wide_text = 1
|
||||
_suppress_arrow1 = False
|
||||
first_run = True
|
||||
|
||||
for p1, p2 in zip(points[:-1], points[1:]):
|
||||
_override = dict(override)
|
||||
_override["move_wide_text"] = move_wide_text
|
||||
if avoid_double_rendering and not first_run:
|
||||
_override["dimse1"] = 1
|
||||
_override["suppress_arrow1"] = _suppress_arrow1
|
||||
|
||||
style = layout.add_linear_dim(
|
||||
Vec3(base),
|
||||
Vec3(p1),
|
||||
Vec3(p2),
|
||||
angle=angle,
|
||||
dimstyle=dimstyle,
|
||||
override=_override,
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
if first_run:
|
||||
_suppress_arrow1 = suppress_arrow1(style)
|
||||
|
||||
renderer = cast(LinearDimension, style.render(ucs, discard=discard))
|
||||
if renderer.measurement.is_wide_text:
|
||||
# after wide text switch moving direction
|
||||
if move_wide_text == 1:
|
||||
move_wide_text = 2
|
||||
else:
|
||||
move_wide_text = 1
|
||||
else: # reset to move text up
|
||||
move_wide_text = 1
|
||||
first_run = False
|
||||
209
.venv/lib/python3.12/site-packages/ezdxf/render/dim_ordinate.py
Normal file
209
.venv/lib/python3.12/site-packages/ezdxf/render/dim_ordinate.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# Copyright (c) 2021-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import logging
|
||||
import math
|
||||
|
||||
from ezdxf.math import Vec2, UCS, NULLVEC
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.entities import DimStyleOverride, Dimension
|
||||
from .dim_base import (
|
||||
BaseDimensionRenderer,
|
||||
get_required_defpoint,
|
||||
compile_mtext,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.eztypes import GenericLayoutType
|
||||
|
||||
__all__ = ["OrdinateDimension"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
class OrdinateDimension(BaseDimensionRenderer):
|
||||
# Required defpoints:
|
||||
# defpoint = origin (group code 10)
|
||||
# defpoint2 = feature location (group code 13)
|
||||
# defpoint3 = end of leader (group code 14)
|
||||
# user text location is ignored (group code 11) and replaced by default
|
||||
# location calculated by the ezdxf renderer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
# The local coordinate system is defined by origin and the
|
||||
# horizontal_direction in OCS:
|
||||
self.origin_ocs: Vec2 = get_required_defpoint(dimension, "defpoint")
|
||||
self.feature_location_ocs: Vec2 = get_required_defpoint(
|
||||
dimension, "defpoint2"
|
||||
)
|
||||
self.end_of_leader_ocs: Vec2 = get_required_defpoint(
|
||||
dimension, "defpoint3"
|
||||
)
|
||||
# Horizontal direction in clockwise orientation, see DXF reference
|
||||
# for group code 51:
|
||||
self.horizontal_dir = -dimension.dxf.get("horizontal_direction", 0.0)
|
||||
self.rotation = math.radians(self.horizontal_dir)
|
||||
self.local_x_axis = Vec2.from_angle(self.rotation)
|
||||
self.local_y_axis = self.local_x_axis.orthogonal()
|
||||
self.x_type = bool( # x-type is set!
|
||||
dimension.dxf.get("dimtype", 0) & const.DIM_ORDINATE_TYPE
|
||||
)
|
||||
super().__init__(dimension, ucs, override)
|
||||
|
||||
# Measurement directions can be opposite to local x- or y-axis
|
||||
self.leader_vec_ocs = self.end_of_leader_ocs - self.feature_location_ocs
|
||||
leader_x_vec = self.local_x_axis.project(self.leader_vec_ocs)
|
||||
leader_y_vec = self.local_y_axis.project(self.leader_vec_ocs)
|
||||
try:
|
||||
self.measurement_direction: Vec2 = leader_x_vec.normalize()
|
||||
except ZeroDivisionError:
|
||||
self.measurement_direction = Vec2(1, 0)
|
||||
try:
|
||||
self.measurement_orthogonal: Vec2 = leader_y_vec.normalize()
|
||||
except ZeroDivisionError:
|
||||
self.measurement_orthogonal = Vec2(0, 1)
|
||||
|
||||
if not self.x_type:
|
||||
self.measurement_direction, self.measurement_orthogonal = (
|
||||
self.measurement_orthogonal,
|
||||
self.measurement_direction,
|
||||
)
|
||||
|
||||
self.update_measurement()
|
||||
if self.tol.has_limits:
|
||||
self.tol.update_limits(self.measurement.value)
|
||||
|
||||
# Text width and -height is required first, text location and -rotation
|
||||
# are not valid yet:
|
||||
self.text_box = self.init_text_box()
|
||||
|
||||
# Set text location and rotation:
|
||||
self.measurement.text_location = self.get_default_text_location()
|
||||
self.measurement.text_rotation = self.get_default_text_rotation()
|
||||
|
||||
# Update text box location and -rotation:
|
||||
self.text_box.center = self.measurement.text_location
|
||||
self.text_box.angle = self.measurement.text_rotation
|
||||
self.geometry.set_text_box(self.text_box)
|
||||
|
||||
# Update final text location in the DIMENSION entity:
|
||||
self.dimension.dxf.text_midpoint = self.measurement.text_location
|
||||
|
||||
def get_default_text_location(self) -> Vec2:
|
||||
if self.x_type:
|
||||
text_vertical_shifting_dir = -self.local_x_axis
|
||||
else:
|
||||
text_vertical_shifting_dir = self.local_y_axis
|
||||
|
||||
# user text location is not supported and ignored:
|
||||
return (
|
||||
self.end_of_leader_ocs
|
||||
+ self.measurement_orthogonal * (self.text_box.width * 0.5)
|
||||
+ text_vertical_shifting_dir
|
||||
* self.measurement.text_vertical_distance()
|
||||
)
|
||||
|
||||
def get_default_text_rotation(self) -> float:
|
||||
# user text rotation is not supported and ignored:
|
||||
return (90.0 if self.x_type else 0.0) + self.horizontal_dir
|
||||
|
||||
def update_measurement(self) -> None:
|
||||
feature_location_vec: Vec2 = self.feature_location_ocs - self.origin_ocs
|
||||
# ordinate measurement is always absolute:
|
||||
self.measurement.update(
|
||||
self.local_x_axis.project(feature_location_vec).magnitude
|
||||
if self.x_type
|
||||
else self.local_y_axis.project(feature_location_vec).magnitude
|
||||
)
|
||||
|
||||
def get_defpoints(self) -> list[Vec2]:
|
||||
return [
|
||||
self.origin_ocs,
|
||||
self.feature_location_ocs,
|
||||
self.end_of_leader_ocs,
|
||||
]
|
||||
|
||||
def transform_ucs_to_wcs(self) -> None:
|
||||
"""Transforms dimension definition points into WCS or if required into
|
||||
OCS.
|
||||
"""
|
||||
|
||||
def from_ucs(attr, func):
|
||||
point = dxf.get(attr, NULLVEC)
|
||||
dxf.set(attr, func(point))
|
||||
|
||||
dxf = self.dimension.dxf
|
||||
ucs = self.geometry.ucs
|
||||
from_ucs("defpoint", ucs.to_wcs)
|
||||
from_ucs("defpoint2", ucs.to_wcs)
|
||||
from_ucs("defpoint3", ucs.to_wcs)
|
||||
from_ucs("text_midpoint", ucs.to_ocs)
|
||||
|
||||
# Horizontal direction in clockwise orientation, see DXF reference
|
||||
# for group code 51:
|
||||
dxf.horizontal_direction = -ucs.to_ocs_angle_deg(self.horizontal_dir)
|
||||
|
||||
def render(self, block: GenericLayoutType) -> None:
|
||||
"""Main method to create dimension geometry of basic DXF entities in the
|
||||
associated BLOCK layout.
|
||||
|
||||
Args:
|
||||
block: target BLOCK for rendering
|
||||
|
||||
"""
|
||||
super().render(block)
|
||||
self.add_ordinate_leader()
|
||||
measurement = self.measurement
|
||||
if measurement.text:
|
||||
if self.geometry.supports_dxf_r2000:
|
||||
text = compile_mtext(measurement, self.tol)
|
||||
else:
|
||||
text = measurement.text
|
||||
self.add_measurement_text(
|
||||
text, measurement.text_location, measurement.text_rotation
|
||||
)
|
||||
self.geometry.add_defpoints(self.get_defpoints())
|
||||
|
||||
def add_ordinate_leader(self) -> None:
|
||||
# DXF attributes from first extension line not from dimension line!
|
||||
attribs = self.extension_lines.dxfattribs(1)
|
||||
# The ordinate leader is normal to the measurement direction.
|
||||
# leader direction and text direction:
|
||||
direction = self.measurement_orthogonal
|
||||
leg_size = self.arrows.arrow_size * 2.0
|
||||
# /---1---TEXT
|
||||
# x----0----/
|
||||
# d0 = distance from feature location (x) to 1st upward junction
|
||||
d0 = direction.project(self.leader_vec_ocs).magnitude - 2.0 * leg_size
|
||||
|
||||
start0 = (
|
||||
self.feature_location_ocs + direction * self.extension_lines.offset
|
||||
)
|
||||
end0 = self.feature_location_ocs + direction * max(leg_size, d0)
|
||||
start1 = self.end_of_leader_ocs - direction * leg_size
|
||||
end1 = self.end_of_leader_ocs
|
||||
if self.measurement.vertical_placement != 0:
|
||||
end1 += direction * self.text_box.width
|
||||
|
||||
self.add_line(start0, end0, dxfattribs=attribs)
|
||||
self.add_line(end0, start1, dxfattribs=attribs)
|
||||
self.add_line(start1, end1, dxfattribs=attribs)
|
||||
|
||||
def add_measurement_text(
|
||||
self, dim_text: str, pos: Vec2, rotation: float
|
||||
) -> None:
|
||||
"""Add measurement text to dimension BLOCK.
|
||||
|
||||
Args:
|
||||
dim_text: dimension text
|
||||
pos: text location
|
||||
rotation: text rotation in degrees
|
||||
|
||||
"""
|
||||
attribs = self.measurement.dxfattribs()
|
||||
self.add_text(dim_text, pos=pos, rotation=rotation, dxfattribs=attribs)
|
||||
488
.venv/lib/python3.12/site-packages/ezdxf/render/dim_radius.py
Normal file
488
.venv/lib/python3.12/site-packages/ezdxf/render/dim_radius.py
Normal file
@@ -0,0 +1,488 @@
|
||||
# Copyright (c) 2018-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ezdxf.math import Vec2, UCS, UVec
|
||||
from ezdxf.tools import normalize_text_angle
|
||||
from ezdxf.render.arrows import ARROWS, connection_point
|
||||
from ezdxf.entities.dimstyleoverride import DimStyleOverride
|
||||
from ezdxf.lldxf.const import DXFInternalEzdxfError
|
||||
from ezdxf.render.dim_base import (
|
||||
BaseDimensionRenderer,
|
||||
Measurement,
|
||||
LengthMeasurement,
|
||||
compile_mtext,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import Dimension
|
||||
from ezdxf.eztypes import GenericLayoutType
|
||||
|
||||
|
||||
class RadiusMeasurement(LengthMeasurement):
|
||||
def __init__(
|
||||
self, dim_style: DimStyleOverride, color: int, scale: float, prefix: str
|
||||
):
|
||||
super().__init__(dim_style, color, scale)
|
||||
self.text_prefix = prefix
|
||||
|
||||
def text_override(self, measurement: float) -> str:
|
||||
text = super().text_override(measurement)
|
||||
if text and text[0] != self.text_prefix:
|
||||
text = self.text_prefix + text
|
||||
return text
|
||||
|
||||
|
||||
class RadiusDimension(BaseDimensionRenderer):
|
||||
"""
|
||||
Radial dimension line renderer.
|
||||
|
||||
Supported render types:
|
||||
- default location inside, text aligned with radial dimension line
|
||||
- default location inside horizontal text
|
||||
- default location outside, text aligned with radial dimension line
|
||||
- default location outside horizontal text
|
||||
- user defined location, text aligned with radial dimension line
|
||||
- user defined location horizontal text
|
||||
|
||||
Args:
|
||||
dimension: DXF entity DIMENSION
|
||||
ucs: user defined coordinate system
|
||||
override: dimension style override management object
|
||||
|
||||
"""
|
||||
|
||||
# Super class of DiameterDimension
|
||||
def _center(self):
|
||||
return Vec2(self.dimension.dxf.defpoint)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
super().__init__(dimension, ucs, override)
|
||||
dimtype = self.dimension.dimtype
|
||||
measurement = self.measurement
|
||||
if dimtype == 3:
|
||||
self.is_diameter_dim = True
|
||||
elif dimtype == 4:
|
||||
self.is_radius_dim = True
|
||||
else:
|
||||
raise DXFInternalEzdxfError(f"Invalid dimension type {dimtype}")
|
||||
|
||||
self.center: Vec2 = self._center() # override in diameter dimension
|
||||
self.point_on_circle: Vec2 = Vec2(self.dimension.dxf.defpoint4)
|
||||
# modify parameters for special scenarios
|
||||
if measurement.user_location is None:
|
||||
if (
|
||||
measurement.text_is_inside
|
||||
and measurement.text_inside_horizontal
|
||||
and measurement.text_movement_rule == 1
|
||||
): # move text, add leader
|
||||
# use algorithm for user define dimension line location
|
||||
measurement.user_location = self.center.lerp(
|
||||
self.point_on_circle
|
||||
)
|
||||
measurement.text_valign = 0 # text vertical centered
|
||||
|
||||
direction = self.point_on_circle - self.center
|
||||
self.dim_line_vec = direction.normalize()
|
||||
self.dim_line_angle = self.dim_line_vec.angle_deg
|
||||
self.radius = direction.magnitude
|
||||
# get_measurement() works for radius and diameter dimension
|
||||
measurement.update(self.dimension.get_measurement())
|
||||
self.outside_default_distance = self.radius + 2 * self.arrows.arrow_size
|
||||
self.outside_default_defpoint = self.center + (
|
||||
self.dim_line_vec * self.outside_default_distance
|
||||
)
|
||||
self.outside_text_force_dimline = bool(self.dim_style.get("dimtofl", 1))
|
||||
# final dimension text (without limits or tolerance)
|
||||
|
||||
# default location is outside, if not forced to be inside
|
||||
measurement.text_is_outside = not measurement.force_text_inside
|
||||
# text_outside: user defined location, overrides default location
|
||||
if measurement.user_location is not None:
|
||||
measurement.text_is_outside = self.is_location_outside(
|
||||
measurement.user_location
|
||||
)
|
||||
|
||||
self._total_text_width: float = 0.0
|
||||
if measurement.text:
|
||||
# text width and required space
|
||||
self._total_text_width = self.total_text_width()
|
||||
if self.tol.has_limits:
|
||||
# limits show the upper and lower limit of the measurement as
|
||||
# stacked values and with the size of tolerances
|
||||
self.tol.update_limits(measurement.value)
|
||||
|
||||
# default rotation is angle of dimension line, from center to point on circle.
|
||||
rotation = self.dim_line_angle
|
||||
if measurement.text_is_outside and measurement.text_outside_horizontal:
|
||||
rotation = 0.0
|
||||
elif measurement.text_is_inside and measurement.text_inside_horizontal:
|
||||
rotation = 0.0
|
||||
|
||||
# final absolute text rotation (x-axis=0)
|
||||
measurement.text_rotation = normalize_text_angle(
|
||||
rotation, fix_upside_down=True
|
||||
)
|
||||
|
||||
# final text location
|
||||
measurement.text_location = self.get_text_location()
|
||||
self.geometry.set_text_box(self.init_text_box())
|
||||
# write final text location into DIMENSION entity
|
||||
if measurement.user_location:
|
||||
self.dimension.dxf.text_midpoint = measurement.user_location
|
||||
# default locations
|
||||
elif (
|
||||
measurement.text_is_outside and measurement.text_outside_horizontal
|
||||
):
|
||||
self.dimension.dxf.text_midpoint = self.outside_default_defpoint
|
||||
else:
|
||||
self.dimension.dxf.text_midpoint = measurement.text_location
|
||||
|
||||
def init_measurement(self, color: int, scale: float) -> Measurement:
|
||||
return RadiusMeasurement(self.dim_style, color, scale, "R")
|
||||
|
||||
def get_text_location(self) -> Vec2:
|
||||
"""Returns text midpoint from user defined location or default text
|
||||
location.
|
||||
"""
|
||||
if self.measurement.user_location is not None:
|
||||
return self.get_user_defined_text_location()
|
||||
else:
|
||||
return self.get_default_text_location()
|
||||
|
||||
def get_default_text_location(self) -> Vec2:
|
||||
"""Returns default text midpoint based on `text_valign` and
|
||||
`text_outside`
|
||||
"""
|
||||
measurement = self.measurement
|
||||
if measurement.text_is_outside and measurement.text_outside_horizontal:
|
||||
hdist = self._total_text_width / 2.0
|
||||
if (
|
||||
measurement.vertical_placement == 0
|
||||
): # shift text horizontal if vertical centered
|
||||
hdist += self.arrows.arrow_size
|
||||
angle = self.dim_line_angle % 360.0 # normalize 0 .. 360
|
||||
if 90.0 < angle <= 270.0:
|
||||
hdist = -hdist
|
||||
return self.outside_default_defpoint + Vec2(
|
||||
(hdist, measurement.text_vertical_distance())
|
||||
)
|
||||
|
||||
text_direction = Vec2.from_deg_angle(measurement.text_rotation)
|
||||
vertical_direction = text_direction.orthogonal(ccw=True)
|
||||
vertical_distance = measurement.text_vertical_distance()
|
||||
if measurement.text_is_inside:
|
||||
hdist = (self.radius - self.arrows.arrow_size) / 2.0
|
||||
text_midpoint = self.center + (self.dim_line_vec * hdist)
|
||||
else:
|
||||
hdist = (
|
||||
self._total_text_width / 2.0
|
||||
+ self.arrows.arrow_size
|
||||
+ measurement.text_gap
|
||||
)
|
||||
text_midpoint = self.point_on_circle + (self.dim_line_vec * hdist)
|
||||
return text_midpoint + (vertical_direction * vertical_distance)
|
||||
|
||||
def get_user_defined_text_location(self) -> Vec2:
|
||||
"""Returns text midpoint for user defined dimension location."""
|
||||
measurement = self.measurement
|
||||
assert isinstance(measurement.user_location, Vec2)
|
||||
text_outside_horiz = (
|
||||
measurement.text_is_outside and measurement.text_outside_horizontal
|
||||
)
|
||||
text_inside_horiz = (
|
||||
measurement.text_is_inside and measurement.text_inside_horizontal
|
||||
)
|
||||
if text_outside_horiz or text_inside_horiz:
|
||||
hdist = self._total_text_width / 2.0
|
||||
if (
|
||||
measurement.vertical_placement == 0
|
||||
): # shift text horizontal if vertical centered
|
||||
hdist += self.arrows.arrow_size
|
||||
if measurement.user_location.x <= self.point_on_circle.x:
|
||||
hdist = -hdist
|
||||
vdist = measurement.text_vertical_distance()
|
||||
return measurement.user_location + Vec2((hdist, vdist))
|
||||
else:
|
||||
text_normal_vec = Vec2.from_deg_angle(
|
||||
measurement.text_rotation
|
||||
).orthogonal()
|
||||
return (
|
||||
measurement.user_location
|
||||
+ text_normal_vec * measurement.text_vertical_distance()
|
||||
)
|
||||
|
||||
def is_location_outside(self, location: Vec2) -> bool:
|
||||
radius = (location - self.center).magnitude
|
||||
return radius > self.radius
|
||||
|
||||
def render(self, block: GenericLayoutType) -> None:
|
||||
"""Create dimension geometry of basic DXF entities in the associated
|
||||
BLOCK layout.
|
||||
"""
|
||||
# call required to setup some requirements
|
||||
super().render(block)
|
||||
measurement = self.measurement
|
||||
if not self.dimension_line.suppress1:
|
||||
if measurement.user_location is not None:
|
||||
self.render_user_location()
|
||||
else:
|
||||
self.render_default_location()
|
||||
|
||||
# add measurement text as last entity to see text fill properly
|
||||
if measurement.text:
|
||||
if self.geometry.supports_dxf_r2000:
|
||||
text = compile_mtext(self.measurement, self.tol)
|
||||
else:
|
||||
text = measurement.text
|
||||
self.add_measurement_text(
|
||||
text, measurement.text_location, measurement.text_rotation
|
||||
)
|
||||
|
||||
# add POINT entities at definition points
|
||||
self.geometry.add_defpoints([self.center, self.point_on_circle])
|
||||
|
||||
def render_default_location(self) -> None:
|
||||
"""Create dimension geometry at the default dimension line locations."""
|
||||
measurement = self.measurement
|
||||
if not self.arrows.suppress1:
|
||||
arrow_connection_point = self.add_arrow(
|
||||
self.point_on_circle, rotate=measurement.text_is_outside
|
||||
)
|
||||
else:
|
||||
arrow_connection_point = self.point_on_circle
|
||||
|
||||
if measurement.text_is_outside:
|
||||
if self.outside_text_force_dimline:
|
||||
self.add_radial_dim_line(self.point_on_circle)
|
||||
else:
|
||||
add_center_mark(self)
|
||||
if measurement.text_outside_horizontal:
|
||||
self.add_horiz_ext_line_default(arrow_connection_point)
|
||||
else:
|
||||
self.add_radial_ext_line_default(arrow_connection_point)
|
||||
else:
|
||||
if measurement.text_movement_rule == 1:
|
||||
# move text, add leader -> dimline from text to point on circle
|
||||
self.add_radial_dim_line_from_text(
|
||||
self.center.lerp(self.point_on_circle),
|
||||
arrow_connection_point,
|
||||
)
|
||||
add_center_mark(self)
|
||||
else:
|
||||
# dimline from center to point on circle
|
||||
self.add_radial_dim_line(arrow_connection_point)
|
||||
|
||||
def render_user_location(self) -> None:
|
||||
"""Create dimension geometry at user defined dimension locations."""
|
||||
measurement = self.measurement
|
||||
preserve_outside = measurement.text_is_outside
|
||||
leader = measurement.text_movement_rule != 2
|
||||
if not leader:
|
||||
measurement.text_is_outside = (
|
||||
False # render dimension line like text inside
|
||||
)
|
||||
# add arrow symbol (block references)
|
||||
if not self.arrows.suppress1:
|
||||
arrow_connection_point = self.add_arrow(
|
||||
self.point_on_circle, rotate=measurement.text_is_outside
|
||||
)
|
||||
else:
|
||||
arrow_connection_point = self.point_on_circle
|
||||
if measurement.text_is_outside:
|
||||
if self.outside_text_force_dimline:
|
||||
self.add_radial_dim_line(self.point_on_circle)
|
||||
else:
|
||||
add_center_mark(self)
|
||||
if measurement.text_outside_horizontal:
|
||||
self.add_horiz_ext_line_user(arrow_connection_point)
|
||||
else:
|
||||
self.add_radial_ext_line_user(arrow_connection_point)
|
||||
else:
|
||||
if measurement.text_inside_horizontal:
|
||||
self.add_horiz_ext_line_user(arrow_connection_point)
|
||||
else:
|
||||
if measurement.text_movement_rule == 2: # move text, no leader!
|
||||
# dimline from center to point on circle
|
||||
self.add_radial_dim_line(arrow_connection_point)
|
||||
else:
|
||||
# move text, add leader -> dimline from text to point on circle
|
||||
self.add_radial_dim_line_from_text(
|
||||
measurement.user_location, arrow_connection_point
|
||||
)
|
||||
add_center_mark(self)
|
||||
|
||||
measurement.text_is_outside = preserve_outside
|
||||
|
||||
def add_arrow(self, location, rotate: bool) -> Vec2:
|
||||
"""Add arrow or tick to dimension line, returns dimension line connection point."""
|
||||
arrows = self.arrows
|
||||
attribs = arrows.dxfattribs()
|
||||
|
||||
arrow_name = arrows.arrow1_name
|
||||
if arrows.tick_size > 0.0: # oblique stroke, but double the size
|
||||
self.add_blockref(
|
||||
ARROWS.oblique,
|
||||
insert=location,
|
||||
rotation=self.dim_line_angle,
|
||||
scale=arrows.tick_size * 2.0,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
else:
|
||||
scale = arrows.arrow_size
|
||||
angle = self.dim_line_angle
|
||||
if rotate:
|
||||
angle += 180.0
|
||||
|
||||
self.add_blockref(
|
||||
arrow_name,
|
||||
insert=location,
|
||||
scale=scale,
|
||||
rotation=angle,
|
||||
dxfattribs=attribs,
|
||||
)
|
||||
location = connection_point(arrow_name, location, scale, angle)
|
||||
return location
|
||||
|
||||
def add_radial_dim_line(self, end: UVec) -> None:
|
||||
"""Add radial dimension line."""
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
self.add_line(
|
||||
self.center, end, dxfattribs=attribs, remove_hidden_lines=True
|
||||
)
|
||||
|
||||
def add_radial_dim_line_from_text(self, start, end: UVec) -> None:
|
||||
"""Add radial dimension line, starting point at the measurement text."""
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
hshift = self._total_text_width / 2
|
||||
if self.measurement.vertical_placement != 0: # not center
|
||||
hshift = -hshift
|
||||
self.add_line(
|
||||
start + self.dim_line_vec * hshift,
|
||||
end,
|
||||
dxfattribs=attribs,
|
||||
remove_hidden_lines=False,
|
||||
)
|
||||
|
||||
def add_horiz_ext_line_default(self, start: UVec) -> None:
|
||||
"""Add horizontal outside extension line from start for default
|
||||
locations.
|
||||
"""
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
self.add_line(start, self.outside_default_defpoint, dxfattribs=attribs)
|
||||
if self.measurement.vertical_placement == 0:
|
||||
hdist = self.arrows.arrow_size
|
||||
else:
|
||||
hdist = self._total_text_width
|
||||
angle = self.dim_line_angle % 360.0 # normalize 0 .. 360
|
||||
if 90 < angle <= 270:
|
||||
hdist = -hdist
|
||||
end = self.outside_default_defpoint + Vec2((hdist, 0))
|
||||
self.add_line(self.outside_default_defpoint, end, dxfattribs=attribs)
|
||||
|
||||
def add_horiz_ext_line_user(self, start: UVec) -> None:
|
||||
"""Add horizontal extension line from start for user defined locations."""
|
||||
measurement = self.measurement
|
||||
assert isinstance(measurement.user_location, Vec2)
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
self.add_line(start, measurement.user_location, dxfattribs=attribs)
|
||||
if measurement.vertical_placement == 0:
|
||||
hdist = self.arrows.arrow_size
|
||||
else:
|
||||
hdist = self._total_text_width
|
||||
if measurement.user_location.x <= self.point_on_circle.x:
|
||||
hdist = -hdist
|
||||
end = measurement.user_location + Vec2((hdist, 0))
|
||||
self.add_line(measurement.user_location, end, dxfattribs=attribs)
|
||||
|
||||
def add_radial_ext_line_default(self, start: UVec) -> None:
|
||||
"""Add radial outside extension line from start for default locations."""
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
length = self.measurement.text_gap + self._total_text_width
|
||||
end = start + self.dim_line_vec * length
|
||||
self.add_line(start, end, dxfattribs=attribs, remove_hidden_lines=True)
|
||||
|
||||
def add_radial_ext_line_user(self, start: UVec) -> None:
|
||||
"""Add radial outside extension line from start for user defined location."""
|
||||
attribs = self.dimension_line.dxfattribs()
|
||||
length = self._total_text_width / 2.0
|
||||
if self.measurement.vertical_placement == 0:
|
||||
length = -length
|
||||
end = self.measurement.user_location + self.dim_line_vec * length
|
||||
self.add_line(start, end, dxfattribs=attribs)
|
||||
|
||||
def add_measurement_text(
|
||||
self, dim_text: str, pos: Vec2, rotation: float
|
||||
) -> None:
|
||||
"""Add measurement text to dimension BLOCK."""
|
||||
attribs = self.measurement.dxfattribs()
|
||||
self.add_text(dim_text, pos=pos, rotation=rotation, dxfattribs=attribs)
|
||||
|
||||
def transform_ucs_to_wcs(self) -> None:
|
||||
"""
|
||||
Transforms dimension definition points into WCS or if required into OCS.
|
||||
|
||||
Can not be called in __init__(), because inherited classes may be need unmodified values.
|
||||
|
||||
"""
|
||||
|
||||
def from_ucs(attr, func):
|
||||
point = self.dimension.get_dxf_attrib(attr)
|
||||
self.dimension.set_dxf_attrib(attr, func(point))
|
||||
|
||||
ucs = self.geometry.ucs
|
||||
from_ucs("defpoint", ucs.to_wcs)
|
||||
from_ucs("defpoint4", ucs.to_wcs)
|
||||
from_ucs("text_midpoint", ucs.to_ocs)
|
||||
|
||||
|
||||
def add_center_mark(dim: RadiusDimension) -> None:
|
||||
"""Add center mark/lines to radius and diameter dimensions.
|
||||
|
||||
Args:
|
||||
dim: RadiusDimension or DiameterDimension renderer
|
||||
"""
|
||||
dim_type = dim.dimension.dimtype
|
||||
if dim_type == 4: # Radius Dimension
|
||||
radius = dim.measurement.raw_value
|
||||
elif dim_type == 3: # Diameter Dimension
|
||||
radius = dim.measurement.raw_value / 2.0
|
||||
else:
|
||||
raise TypeError(f"Invalid dimension type: {dim_type}")
|
||||
|
||||
mark_size = dim.dim_style.get("dimcen", 0)
|
||||
if mark_size == 0:
|
||||
return
|
||||
|
||||
center_lines = False
|
||||
if mark_size < 0:
|
||||
mark_size = abs(mark_size)
|
||||
center_lines = True
|
||||
center = Vec2(dim.center)
|
||||
|
||||
# draw center mark
|
||||
mark_x_vec = Vec2((mark_size, 0))
|
||||
mark_y_vec = Vec2((0, mark_size))
|
||||
# use only color and ignore linetype!
|
||||
dxfattribs = {"color": dim.dimension_line.color}
|
||||
dim.add_line(center - mark_x_vec, center + mark_x_vec, dxfattribs)
|
||||
dim.add_line(center - mark_y_vec, center + mark_y_vec, dxfattribs)
|
||||
|
||||
if center_lines:
|
||||
size = mark_size + radius
|
||||
if size < 2 * mark_size:
|
||||
return # not enough space for center lines
|
||||
start_x_vec = mark_x_vec * 2
|
||||
start_y_vec = mark_y_vec * 2
|
||||
end_x_vec = Vec2((size, 0))
|
||||
end_y_vec = Vec2((0, size))
|
||||
dim.add_line(center + start_x_vec, center + end_x_vec, dxfattribs)
|
||||
dim.add_line(center - start_x_vec, center - end_x_vec, dxfattribs)
|
||||
dim.add_line(center + start_y_vec, center + end_y_vec, dxfattribs)
|
||||
dim.add_line(center - start_y_vec, center - end_y_vec, dxfattribs)
|
||||
118
.venv/lib/python3.12/site-packages/ezdxf/render/dimension.py
Normal file
118
.venv/lib/python3.12/site-packages/ezdxf/render/dimension.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2018-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ezdxf.math import UCS
|
||||
from ezdxf.lldxf.const import DXFValueError
|
||||
from ezdxf.entities.dimstyleoverride import DimStyleOverride
|
||||
|
||||
from .dim_base import BaseDimensionRenderer
|
||||
from .dim_curved import AngularDimension, Angular3PDimension, ArcLengthDimension
|
||||
from .dim_diameter import DiameterDimension
|
||||
from .dim_linear import LinearDimension
|
||||
from .dim_ordinate import OrdinateDimension
|
||||
from .dim_radius import RadiusDimension
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import Dimension
|
||||
|
||||
|
||||
class DimensionRenderer:
|
||||
def dispatch(
|
||||
self, override: DimStyleOverride, ucs: Optional[UCS] = None
|
||||
) -> BaseDimensionRenderer:
|
||||
dimension = override.dimension
|
||||
dim_type = dimension.dimtype
|
||||
dxf_type = dimension.dxftype()
|
||||
if dxf_type == "ARC_DIMENSION":
|
||||
return self.arc_length(dimension, ucs, override)
|
||||
elif dxf_type == "LARGE_RADIAL_DIMENSION":
|
||||
return self.large_radial(dimension, ucs, override)
|
||||
elif dim_type in (0, 1):
|
||||
return self.linear(dimension, ucs, override)
|
||||
elif dim_type == 2:
|
||||
return self.angular(dimension, ucs, override)
|
||||
elif dim_type == 3:
|
||||
return self.diameter(dimension, ucs, override)
|
||||
elif dim_type == 4:
|
||||
return self.radius(dimension, ucs, override)
|
||||
elif dim_type == 5:
|
||||
return self.angular3p(dimension, ucs, override)
|
||||
elif dim_type == 6:
|
||||
return self.ordinate(dimension, ucs, override)
|
||||
else:
|
||||
raise DXFValueError(f"Unknown DIMENSION type: {dim_type}")
|
||||
|
||||
def linear(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for linear dimension lines: horizontal, vertical and rotated"""
|
||||
return LinearDimension(dimension, ucs, override)
|
||||
|
||||
def angular(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for angular dimension defined by two lines."""
|
||||
return AngularDimension(dimension, ucs, override)
|
||||
|
||||
def diameter(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for diameter dimension"""
|
||||
return DiameterDimension(dimension, ucs, override)
|
||||
|
||||
def radius(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for radius dimension"""
|
||||
return RadiusDimension(dimension, ucs, override)
|
||||
|
||||
def large_radial(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for large radial dimension"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def angular3p(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for angular dimension defined by three points."""
|
||||
return Angular3PDimension(dimension, ucs, override)
|
||||
|
||||
def ordinate(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for ordinate dimension."""
|
||||
return OrdinateDimension(dimension, ucs, override)
|
||||
|
||||
def arc_length(
|
||||
self,
|
||||
dimension: Dimension,
|
||||
ucs: Optional[UCS] = None,
|
||||
override: Optional[DimStyleOverride] = None,
|
||||
):
|
||||
"""Call renderer for arc length dimension."""
|
||||
return ArcLengthDimension(dimension, ucs, override)
|
||||
1434
.venv/lib/python3.12/site-packages/ezdxf/render/forms.py
Normal file
1434
.venv/lib/python3.12/site-packages/ezdxf/render/forms.py
Normal file
File diff suppressed because it is too large
Load Diff
700
.venv/lib/python3.12/site-packages/ezdxf/render/hatching.py
Normal file
700
.venv/lib/python3.12/site-packages/ezdxf/render/hatching.py
Normal file
@@ -0,0 +1,700 @@
|
||||
# Copyright (c) 2022-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Iterator,
|
||||
Sequence,
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Any,
|
||||
Union,
|
||||
Optional,
|
||||
Tuple,
|
||||
)
|
||||
from typing_extensions import TypeAlias
|
||||
from collections import defaultdict
|
||||
import enum
|
||||
import math
|
||||
import dataclasses
|
||||
import random
|
||||
from ezdxf.math import (
|
||||
Vec2,
|
||||
Vec3,
|
||||
Bezier3P,
|
||||
Bezier4P,
|
||||
intersection_ray_cubic_bezier_2d,
|
||||
quadratic_to_cubic_bezier,
|
||||
)
|
||||
from ezdxf import const
|
||||
from ezdxf.path import Path, LineTo, MoveTo, Curve3To, Curve4To
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities.polygon import DXFPolygon
|
||||
|
||||
MIN_HATCH_LINE_DISTANCE = 1e-4 # ??? what's a good choice
|
||||
NONE_VEC2 = Vec2(math.nan, math.nan)
|
||||
KEY_NDIGITS = 4
|
||||
SORT_NDIGITS = 10
|
||||
|
||||
|
||||
class IntersectionType(enum.IntEnum):
|
||||
NONE = 0
|
||||
REGULAR = 1
|
||||
START = 2
|
||||
END = 3
|
||||
COLLINEAR = 4
|
||||
|
||||
|
||||
class HatchingError(Exception):
|
||||
"""Base exception class of the :mod:`hatching` module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HatchLineDirectionError(HatchingError):
|
||||
"""Hatching direction is undefined or a (0, 0) vector."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DenseHatchingLinesError(HatchingError):
|
||||
"""Very small hatching distance which creates too many hatching lines."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Line:
|
||||
start: Vec2
|
||||
end: Vec2
|
||||
distance: float # normal distance to the hatch baseline
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Intersection:
|
||||
"""Represents an intersection."""
|
||||
|
||||
type: IntersectionType = IntersectionType.NONE
|
||||
p0: Vec2 = NONE_VEC2
|
||||
p1: Vec2 = NONE_VEC2
|
||||
|
||||
|
||||
def side_of_line(distance: float, abs_tol=1e-12) -> int:
|
||||
if abs(distance) < abs_tol:
|
||||
return 0
|
||||
if distance > 0.0:
|
||||
return +1
|
||||
return -1
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class HatchLine:
|
||||
"""Represents a single hatch line.
|
||||
|
||||
Args:
|
||||
origin: the origin of the hatch line as :class:`~ezdxf.math.Vec2` instance
|
||||
direction: the hatch line direction as :class:`~ezdxf.math.Vec2` instance, must not (0, 0)
|
||||
distance: the normal distance to the base hatch line as float
|
||||
|
||||
"""
|
||||
|
||||
origin: Vec2
|
||||
direction: Vec2
|
||||
distance: float
|
||||
|
||||
def intersect_line(
|
||||
self,
|
||||
a: Vec2,
|
||||
b: Vec2,
|
||||
dist_a: float,
|
||||
dist_b: float,
|
||||
) -> Intersection:
|
||||
"""Returns the :class:`Intersection` of this hatch line and the line
|
||||
defined by the points `a` and `b`.
|
||||
The arguments `dist_a` and `dist_b` are the signed normal distances of
|
||||
the points `a` and `b` from the hatch baseline.
|
||||
The normal distances from the baseline are easy to calculate by the
|
||||
:meth:`HatchBaseLine.signed_distance` method and allow a fast
|
||||
intersection calculation by a simple point interpolation.
|
||||
|
||||
Args:
|
||||
a: start point of the line as :class:`~ezdxf.math.Vec2` instance
|
||||
b: end point of the line as :class:`~ezdxf.math.Vec2` instance
|
||||
dist_a: normal distance of point `a` to the hatch baseline as float
|
||||
dist_b: normal distance of point `b` to the hatch baseline as float
|
||||
|
||||
"""
|
||||
# all distances are normal distances to the hatch baseline
|
||||
line_distance = self.distance
|
||||
side_a = side_of_line(dist_a - line_distance)
|
||||
side_b = side_of_line(dist_b - line_distance)
|
||||
if side_a == 0:
|
||||
if side_b == 0:
|
||||
return Intersection(IntersectionType.COLLINEAR, a, b)
|
||||
else:
|
||||
return Intersection(IntersectionType.START, a)
|
||||
elif side_b == 0:
|
||||
return Intersection(IntersectionType.END, b)
|
||||
elif side_a != side_b:
|
||||
factor = abs((dist_a - line_distance) / (dist_a - dist_b))
|
||||
return Intersection(IntersectionType.REGULAR, a.lerp(b, factor))
|
||||
return Intersection() # no intersection
|
||||
|
||||
def intersect_cubic_bezier_curve(self, curve: Bezier4P) -> Sequence[Intersection]:
|
||||
"""Returns 0 to 3 :class:`Intersection` points of this hatch line with
|
||||
a cubic Bèzier curve.
|
||||
|
||||
Args:
|
||||
curve: the cubic Bèzier curve as :class:`ezdxf.math.Bezier4P` instance
|
||||
|
||||
"""
|
||||
return [
|
||||
Intersection(IntersectionType.REGULAR, p, NONE_VEC2)
|
||||
for p in intersection_ray_cubic_bezier_2d(
|
||||
self.origin, self.origin + self.direction, curve
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class PatternRenderer:
|
||||
"""
|
||||
The hatch pattern of a DXF entity has one or more :class:`HatchBaseLine`
|
||||
instances with an origin, direction, offset and line pattern.
|
||||
The :class:`PatternRenderer` for a certain distance from the
|
||||
baseline has to be acquired from the :class:`HatchBaseLine` by the
|
||||
:meth:`~HatchBaseLine.pattern_renderer` method.
|
||||
|
||||
The origin of the hatch line is the starting point of the line
|
||||
pattern. The offset defines the origin of the adjacent
|
||||
hatch line and doesn't have to be orthogonal to the hatch line direction.
|
||||
|
||||
**Line Pattern**
|
||||
|
||||
The line pattern is a sequence of floats, where a value > 0.0 is a dash, a
|
||||
value < 0.0 is a gap and value of 0.0 is a point.
|
||||
|
||||
Args:
|
||||
hatch_line: :class:`HatchLine`
|
||||
pattern: the line pattern as sequence of float values
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, hatch_line: HatchLine, pattern: Sequence[float]):
|
||||
self.origin = hatch_line.origin
|
||||
self.direction = hatch_line.direction
|
||||
self.pattern = pattern
|
||||
self.pattern_length = math.fsum([abs(e) for e in pattern])
|
||||
|
||||
def sequence_origin(self, index: float) -> Vec2:
|
||||
return self.origin + self.direction * (self.pattern_length * index)
|
||||
|
||||
def render(self, start: Vec2, end: Vec2) -> Iterator[tuple[Vec2, Vec2]]:
|
||||
"""Yields the pattern lines as pairs of :class:`~ezdxf.math.Vec2`
|
||||
instances from the start- to the end point on the hatch line.
|
||||
For points the start- and end point are the same :class:`~ezdxf.math.Vec2`
|
||||
instance and can be tested by the ``is`` operator.
|
||||
|
||||
The start- and end points should be located collinear at the hatch line
|
||||
of this instance, otherwise the points a projected onto this hatch line.
|
||||
|
||||
"""
|
||||
if start.isclose(end):
|
||||
return
|
||||
length = self.pattern_length
|
||||
if length < 1e-9:
|
||||
yield start, end
|
||||
return
|
||||
|
||||
direction = self.direction
|
||||
if direction.dot(end - start) < 0.0:
|
||||
# Line direction is reversed to the pattern line direction!
|
||||
start, end = end, start
|
||||
origin = self.origin
|
||||
s_dist = direction.dot(start - origin)
|
||||
e_dist = direction.dot(end - origin)
|
||||
s_index, s_offset = divmod(s_dist, length)
|
||||
e_index, e_offset = divmod(e_dist, length)
|
||||
|
||||
if s_index == e_index:
|
||||
yield from self.render_offset_to_offset(s_index, s_offset, e_offset)
|
||||
return
|
||||
# line crosses pattern border
|
||||
if s_offset > 0.0:
|
||||
yield from self.render_offset_to_offset(
|
||||
s_index,
|
||||
s_offset,
|
||||
length,
|
||||
)
|
||||
s_index += 1
|
||||
|
||||
while s_index < e_index:
|
||||
yield from self.render_full_pattern(s_index)
|
||||
s_index += 1
|
||||
|
||||
if e_offset > 0.0:
|
||||
yield from self.render_offset_to_offset(
|
||||
s_index,
|
||||
0.0,
|
||||
e_offset,
|
||||
)
|
||||
|
||||
def render_full_pattern(self, index: float) -> Iterator[tuple[Vec2, Vec2]]:
|
||||
# fast pattern rendering
|
||||
direction = self.direction
|
||||
start_point = self.sequence_origin(index)
|
||||
for dash in self.pattern:
|
||||
if dash == 0.0:
|
||||
yield start_point, start_point
|
||||
else:
|
||||
end_point = start_point + direction * abs(dash)
|
||||
if dash > 0.0:
|
||||
yield start_point, end_point
|
||||
start_point = end_point
|
||||
|
||||
def render_offset_to_offset(
|
||||
self, index: float, s_offset: float, e_offset: float
|
||||
) -> Iterator[tuple[Vec2, Vec2]]:
|
||||
direction = self.direction
|
||||
origin = self.sequence_origin(index)
|
||||
start_point = origin + direction * s_offset
|
||||
distance = 0.0
|
||||
for dash in self.pattern:
|
||||
distance += abs(dash)
|
||||
if distance < s_offset:
|
||||
continue
|
||||
if dash == 0.0:
|
||||
yield start_point, start_point
|
||||
else:
|
||||
end_point = origin + direction * min(distance, e_offset)
|
||||
if dash > 0.0:
|
||||
yield start_point, end_point
|
||||
if distance >= e_offset:
|
||||
return
|
||||
start_point = end_point
|
||||
|
||||
|
||||
class HatchBaseLine:
|
||||
"""A hatch baseline defines the source line for hatching a geometry.
|
||||
A complete hatch pattern of a DXF entity can consist of one or more hatch
|
||||
baselines.
|
||||
|
||||
Args:
|
||||
origin: the origin of the hatch line as :class:`~ezdxf.math.Vec2` instance
|
||||
direction: the hatch line direction as :class:`~ezdxf.math.Vec2` instance, must not (0, 0)
|
||||
offset: the offset of the hatch line origin to the next or to the previous hatch line
|
||||
line_pattern: line pattern as sequence of floats, see also :class:`PatternRenderer`
|
||||
min_hatch_line_distance: minimum hatch line distance to render, raises an
|
||||
:class:`DenseHatchingLinesError` exception if the distance between hatch
|
||||
lines is smaller than this value
|
||||
|
||||
Raises:
|
||||
HatchLineDirectionError: hatch baseline has no direction, (0, 0) vector
|
||||
DenseHatchingLinesError: hatching lines are too narrow
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
origin: Vec2,
|
||||
direction: Vec2,
|
||||
offset: Vec2,
|
||||
line_pattern: Optional[list[float]] = None,
|
||||
min_hatch_line_distance=MIN_HATCH_LINE_DISTANCE,
|
||||
):
|
||||
self.origin = origin
|
||||
try:
|
||||
self.direction = direction.normalize()
|
||||
except ZeroDivisionError:
|
||||
raise HatchLineDirectionError("hatch baseline has no direction")
|
||||
self.offset = offset
|
||||
self.normal_distance: float = (-offset).det(self.direction - offset)
|
||||
if abs(self.normal_distance) < min_hatch_line_distance:
|
||||
raise DenseHatchingLinesError("hatching lines are too narrow")
|
||||
self._end = self.origin + self.direction
|
||||
self.line_pattern: list[float] = line_pattern if line_pattern else []
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"{self.__class__.__name__}(origin={self.origin!r}, "
|
||||
f"direction={self.direction!r}, offset={self.offset!r})"
|
||||
)
|
||||
|
||||
def hatch_line(self, distance: float) -> HatchLine:
|
||||
"""Returns the :class:`HatchLine` at the given signed `distance`."""
|
||||
factor = distance / self.normal_distance
|
||||
return HatchLine(self.origin + self.offset * factor, self.direction, distance)
|
||||
|
||||
def signed_distance(self, point: Vec2) -> float:
|
||||
"""Returns the signed normal distance of the given `point` from this
|
||||
hatch baseline.
|
||||
"""
|
||||
# denominator (_end - origin).magnitude is 1.0 !!!
|
||||
return (self.origin - point).det(self._end - point)
|
||||
|
||||
def pattern_renderer(self, distance: float) -> PatternRenderer:
|
||||
"""Returns the :class:`PatternRenderer` for the given signed `distance`."""
|
||||
return PatternRenderer(self.hatch_line(distance), self.line_pattern)
|
||||
|
||||
|
||||
def hatch_line_distances(
|
||||
point_distances: Sequence[float], normal_distance: float
|
||||
) -> list[float]:
|
||||
"""Returns all hatch line distances in the range of the given point
|
||||
distances.
|
||||
"""
|
||||
assert normal_distance != 0.0
|
||||
normal_factors = [d / normal_distance for d in point_distances]
|
||||
max_line_number = int(math.ceil(max(normal_factors)))
|
||||
min_line_number = int(math.ceil(min(normal_factors)))
|
||||
return [normal_distance * num for num in range(min_line_number, max_line_number)]
|
||||
|
||||
|
||||
def intersect_polygon(
|
||||
baseline: HatchBaseLine, polygon: Sequence[Vec2]
|
||||
) -> Iterator[tuple[Intersection, float]]:
|
||||
"""Yields all intersection points of the hatch defined by the `baseline` and
|
||||
the given `polygon`.
|
||||
|
||||
Returns the intersection point and the normal-distance from the baseline,
|
||||
intersection points with the same normal-distance lay on the same hatch
|
||||
line.
|
||||
|
||||
"""
|
||||
count = len(polygon)
|
||||
if count < 3:
|
||||
return
|
||||
if polygon[0].isclose(polygon[-1]):
|
||||
count -= 1
|
||||
if count < 3:
|
||||
return
|
||||
|
||||
prev_point = polygon[count - 1] # last point
|
||||
dist_prev = baseline.signed_distance(prev_point)
|
||||
for index in range(count):
|
||||
point = polygon[index]
|
||||
dist_point = baseline.signed_distance(point)
|
||||
for hatch_line_distance in hatch_line_distances(
|
||||
(dist_prev, dist_point), baseline.normal_distance
|
||||
):
|
||||
hatch_line = baseline.hatch_line(hatch_line_distance)
|
||||
ip = hatch_line.intersect_line(
|
||||
prev_point,
|
||||
point,
|
||||
dist_prev,
|
||||
dist_point,
|
||||
)
|
||||
if (
|
||||
ip.type != IntersectionType.NONE
|
||||
and ip.type != IntersectionType.COLLINEAR
|
||||
):
|
||||
yield ip, hatch_line_distance
|
||||
|
||||
prev_point = point
|
||||
dist_prev = dist_point
|
||||
|
||||
|
||||
def hatch_polygons(
|
||||
baseline: HatchBaseLine,
|
||||
polygons: Sequence[Sequence[Vec2]],
|
||||
terminate: Optional[Callable[[], bool]] = None,
|
||||
) -> Iterator[Line]:
|
||||
"""Yields all pattern lines for all hatch lines generated by the given
|
||||
:class:`HatchBaseLine`, intersecting the given 2D polygons as :class:`Line`
|
||||
instances.
|
||||
The `polygons` should represent a single entity with or without holes, the
|
||||
order of the polygons and their winding orientation (cw or ccw) is not
|
||||
important. Entities which do not intersect or overlap should be handled
|
||||
separately!
|
||||
|
||||
Each polygon is a sequence of :class:`~ezdxf.math.Vec2` instances, they are
|
||||
treated as closed polygons even if the last vertex is not equal to the
|
||||
first vertex.
|
||||
|
||||
The hole detection is done by a simple inside/outside counting algorithm and
|
||||
far from perfect, but is able to handle ordinary polygons well.
|
||||
|
||||
The terminate function WILL BE CALLED PERIODICALLY AND should return
|
||||
``True`` to terminate execution. This can be used to implement a timeout,
|
||||
which can be required if using a very small hatching distance, especially
|
||||
if you get the data from untrusted sources.
|
||||
|
||||
Args:
|
||||
baseline: :class:`HatchBaseLine`
|
||||
polygons: multiple sequences of :class:`~ezdxf.path.Vec2` instances of
|
||||
a single entity, the order of exterior- and hole paths and the
|
||||
winding orientation (cw or ccw) of paths is not important
|
||||
terminate: callback function which is called periodically and should
|
||||
return ``True`` to terminate the hatching function
|
||||
|
||||
"""
|
||||
yield from _hatch_geometry(baseline, polygons, intersect_polygon, terminate)
|
||||
|
||||
|
||||
def intersect_path(
|
||||
baseline: HatchBaseLine, path: Path
|
||||
) -> Iterator[tuple[Intersection, float]]:
|
||||
"""Yields all intersection points of the hatch defined by the `baseline` and
|
||||
the given single `path`.
|
||||
|
||||
Returns the intersection point and the normal-distance from the baseline,
|
||||
intersection points with the same normal-distance lay on the same hatch
|
||||
line.
|
||||
|
||||
"""
|
||||
for path_element in _path_elements(path):
|
||||
if isinstance(path_element, Bezier4P):
|
||||
distances = [
|
||||
baseline.signed_distance(p) for p in path_element.control_points
|
||||
]
|
||||
for hatch_line_distance in hatch_line_distances(
|
||||
distances, baseline.normal_distance
|
||||
):
|
||||
hatch_line = baseline.hatch_line(hatch_line_distance)
|
||||
for ip in hatch_line.intersect_cubic_bezier_curve(path_element):
|
||||
yield ip, hatch_line_distance
|
||||
else: # line
|
||||
a, b = Vec2.generate(path_element)
|
||||
dist_a = baseline.signed_distance(a)
|
||||
dist_b = baseline.signed_distance(b)
|
||||
for hatch_line_distance in hatch_line_distances(
|
||||
(dist_a, dist_b), baseline.normal_distance
|
||||
):
|
||||
hatch_line = baseline.hatch_line(hatch_line_distance)
|
||||
ip = hatch_line.intersect_line(a, b, dist_a, dist_b)
|
||||
if (
|
||||
ip.type != IntersectionType.NONE
|
||||
and ip.type != IntersectionType.COLLINEAR
|
||||
):
|
||||
yield ip, hatch_line_distance
|
||||
|
||||
|
||||
def _path_elements(path: Path) -> Union[Bezier4P, tuple[Vec2, Vec2]]:
|
||||
if len(path) == 0:
|
||||
return
|
||||
start = path.start
|
||||
path_start = start
|
||||
for command in path.commands():
|
||||
end = command.end
|
||||
if isinstance(command, MoveTo):
|
||||
if not path_start.isclose(start):
|
||||
yield start, path_start # close sub-path
|
||||
path_start = end
|
||||
elif isinstance(command, LineTo) and not start.isclose(end):
|
||||
yield start, end
|
||||
elif isinstance(command, Curve4To):
|
||||
yield Bezier4P((start, command.ctrl1, command.ctrl2, end))
|
||||
elif isinstance(command, Curve3To):
|
||||
curve3 = Bezier3P((start, command.ctrl, end))
|
||||
yield quadratic_to_cubic_bezier(curve3)
|
||||
start = end
|
||||
|
||||
if not path_start.isclose(start): # close path
|
||||
yield start, path_start
|
||||
|
||||
|
||||
def hatch_paths(
|
||||
baseline: HatchBaseLine,
|
||||
paths: Sequence[Path],
|
||||
terminate: Optional[Callable[[], bool]] = None,
|
||||
) -> Iterator[Line]:
|
||||
"""Yields all pattern lines for all hatch lines generated by the given
|
||||
:class:`HatchBaseLine`, intersecting the given 2D :class:`~ezdxf.path.Path`
|
||||
instances as :class:`Line` instances. The paths are handled as projected
|
||||
into the xy-plane the z-axis of path vertices will be ignored if present.
|
||||
|
||||
Same as the :func:`hatch_polygons` function, but for :class:`~ezdxf.path.Path`
|
||||
instances instead of polygons build of vertices. This function **does not
|
||||
flatten** the paths into vertices, instead the real intersections of the
|
||||
Bézier curves and the hatch lines are calculated.
|
||||
|
||||
For more information see the docs of the :func:`hatch_polygons` function.
|
||||
|
||||
Args:
|
||||
baseline: :class:`HatchBaseLine`
|
||||
paths: sequence of :class:`~ezdxf.path.Path` instances of a single
|
||||
entity, the order of exterior- and hole paths and the winding
|
||||
orientation (cw or ccw) of the paths is not important
|
||||
terminate: callback function which is called periodically and should
|
||||
return ``True`` to terminate the hatching function
|
||||
|
||||
"""
|
||||
yield from _hatch_geometry(baseline, paths, intersect_path, terminate)
|
||||
|
||||
|
||||
IFuncType: TypeAlias = Callable[
|
||||
[HatchBaseLine, Any], Iterator[Tuple[Intersection, float]]
|
||||
]
|
||||
|
||||
|
||||
def _hatch_geometry(
|
||||
baseline: HatchBaseLine,
|
||||
geometries: Sequence[Any],
|
||||
intersection_func: IFuncType,
|
||||
terminate: Optional[Callable[[], bool]] = None,
|
||||
) -> Iterator[Line]:
|
||||
"""Returns all pattern lines intersecting the given geometries.
|
||||
|
||||
The intersection_func() should yield all intersection points between a
|
||||
HatchBaseLine() and as given geometry.
|
||||
|
||||
The terminate function should return ``True`` to terminate execution
|
||||
otherwise ``False``. Can be used to implement a timeout.
|
||||
|
||||
"""
|
||||
points: dict[float, list[Intersection]] = defaultdict(list)
|
||||
for geometry in geometries:
|
||||
if terminate and terminate():
|
||||
return
|
||||
for ip, distance in intersection_func(baseline, geometry):
|
||||
assert ip.type != IntersectionType.NONE
|
||||
points[round(distance, KEY_NDIGITS)].append(ip)
|
||||
|
||||
for distance, vertices in points.items():
|
||||
if terminate and terminate():
|
||||
return
|
||||
start = NONE_VEC2
|
||||
end = NONE_VEC2
|
||||
for line in _line_segments(vertices, distance):
|
||||
if start is NONE_VEC2:
|
||||
start = line.start
|
||||
end = line.end
|
||||
continue
|
||||
if line.start.isclose(end):
|
||||
end = line.end
|
||||
else:
|
||||
yield Line(start, end, distance)
|
||||
start = line.start
|
||||
end = line.end
|
||||
|
||||
if start is not NONE_VEC2:
|
||||
yield Line(start, end, distance)
|
||||
|
||||
|
||||
def _line_segments(vertices: list[Intersection], distance: float) -> Iterator[Line]:
|
||||
if len(vertices) < 2:
|
||||
return
|
||||
vertices.sort(key=lambda p: p.p0.round(SORT_NDIGITS))
|
||||
inside = False
|
||||
prev_point = NONE_VEC2
|
||||
for ip in vertices:
|
||||
if ip.type == IntersectionType.NONE or ip.type == IntersectionType.COLLINEAR:
|
||||
continue
|
||||
# REGULAR, START, END
|
||||
point = ip.p0
|
||||
if prev_point is NONE_VEC2:
|
||||
inside = True
|
||||
prev_point = point
|
||||
continue
|
||||
if inside:
|
||||
yield Line(prev_point, point, distance)
|
||||
|
||||
inside = not inside
|
||||
prev_point = point
|
||||
|
||||
|
||||
def hatch_entity(
|
||||
polygon: DXFPolygon,
|
||||
filter_text_boxes=True,
|
||||
jiggle_origin: bool = True,
|
||||
) -> Iterator[tuple[Vec3, Vec3]]:
|
||||
"""Yields the hatch pattern of the given HATCH or MPOLYGON entity as 3D lines.
|
||||
Each line is a pair of :class:`~ezdxf.math.Vec3` instances as start- and end
|
||||
vertex, points are represented as lines of zero length, which means the
|
||||
start vertex is equal to the end vertex.
|
||||
|
||||
The function yields nothing if `polygon` has a solid- or gradient filling
|
||||
or does not have a usable pattern assigned.
|
||||
|
||||
Args:
|
||||
polygon: :class:`~ezdxf.entities.Hatch` or :class:`~ezdxf.entities.MPolygon`
|
||||
entity
|
||||
filter_text_boxes: ignore text boxes if ``True``
|
||||
jiggle_origin: move pattern line origins a small amount to avoid intersections
|
||||
in corner points which causes errors in patterns
|
||||
|
||||
"""
|
||||
if polygon.pattern is None or polygon.dxf.solid_fill:
|
||||
return
|
||||
if len(polygon.pattern.lines) == 0:
|
||||
return
|
||||
ocs = polygon.ocs()
|
||||
elevation = polygon.dxf.elevation.z
|
||||
paths = hatch_boundary_paths(polygon, filter_text_boxes)
|
||||
# todo: MPOLYGON offset
|
||||
# All paths in OCS!
|
||||
for baseline in pattern_baselines(polygon, jiggle_origin=jiggle_origin):
|
||||
for line in hatch_paths(baseline, paths):
|
||||
line_pattern = baseline.pattern_renderer(line.distance)
|
||||
for s, e in line_pattern.render(line.start, line.end):
|
||||
if ocs.transform:
|
||||
yield ocs.to_wcs((s.x, s.y, elevation)), ocs.to_wcs(
|
||||
(e.x, e.y, elevation)
|
||||
)
|
||||
yield Vec3(s), Vec3(e)
|
||||
|
||||
|
||||
def hatch_boundary_paths(polygon: DXFPolygon, filter_text_boxes=True) -> list[Path]:
|
||||
"""Returns the hatch boundary paths as :class:`ezdxf.path.Path` instances
|
||||
of HATCH and MPOLYGON entities. Ignores text boxes if argument
|
||||
`filter_text_boxes` is ``True``.
|
||||
"""
|
||||
from ezdxf.path import from_hatch_boundary_path
|
||||
|
||||
loops = []
|
||||
for boundary in polygon.paths.rendering_paths(polygon.dxf.hatch_style):
|
||||
if filter_text_boxes and boundary.path_type_flags & const.BOUNDARY_PATH_TEXTBOX:
|
||||
continue
|
||||
path = from_hatch_boundary_path(boundary)
|
||||
for sub_path in path.sub_paths():
|
||||
if len(sub_path):
|
||||
sub_path.close()
|
||||
loops.append(sub_path)
|
||||
return loops
|
||||
|
||||
|
||||
def _jiggle_factor():
|
||||
# range 0.0003 .. 0.0010
|
||||
return random.random() * 0.0007 + 0.0003
|
||||
|
||||
|
||||
def pattern_baselines(
|
||||
polygon: DXFPolygon,
|
||||
min_hatch_line_distance: float = MIN_HATCH_LINE_DISTANCE,
|
||||
*,
|
||||
jiggle_origin: bool = False,
|
||||
) -> Iterator[HatchBaseLine]:
|
||||
"""Yields the hatch pattern baselines of HATCH and MPOLYGON entities as
|
||||
:class:`HatchBaseLine` instances. Set `jiggle_origin` to ``True`` to move pattern
|
||||
line origins a small amount to avoid intersections in corner points which causes
|
||||
errors in patterns.
|
||||
|
||||
"""
|
||||
pattern = polygon.pattern
|
||||
if not pattern:
|
||||
return
|
||||
# The hatch pattern parameters are already scaled and rotated for direct
|
||||
# usage!
|
||||
# The stored scale and angle is just for reconstructing the base pattern
|
||||
# when applying a new scaling or rotation.
|
||||
|
||||
jiggle_offset = Vec2()
|
||||
if jiggle_origin:
|
||||
# move origin of base pattern lines a small amount to avoid intersections with
|
||||
# boundary corner points
|
||||
offsets: list[float] = [line.offset.magnitude for line in pattern.lines]
|
||||
if len(offsets):
|
||||
# calculate the same random jiggle offset for all pattern base lines
|
||||
mean = sum(offsets) / len(offsets)
|
||||
x = _jiggle_factor() * mean
|
||||
y = _jiggle_factor() * mean
|
||||
jiggle_offset = Vec2(x, y)
|
||||
|
||||
for line in pattern.lines:
|
||||
direction = Vec2.from_deg_angle(line.angle)
|
||||
yield HatchBaseLine(
|
||||
origin=line.base_point + jiggle_offset,
|
||||
direction=direction,
|
||||
offset=line.offset,
|
||||
line_pattern=line.dash_length_items,
|
||||
min_hatch_line_distance=min_hatch_line_distance,
|
||||
)
|
||||
126
.venv/lib/python3.12/site-packages/ezdxf/render/leader.py
Normal file
126
.venv/lib/python3.12/site-packages/ezdxf/render/leader.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) 2020-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterator, cast
|
||||
|
||||
from ezdxf import ARROWS
|
||||
from ezdxf.entities import factory
|
||||
from ezdxf.lldxf.const import BYBLOCK
|
||||
from ezdxf.math import Vec3, fit_points_to_cad_cv
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFGraphic, Leader, Insert, Spline, Dimension, Line
|
||||
|
||||
|
||||
def virtual_entities(leader: Leader) -> Iterator[DXFGraphic]:
|
||||
# Source: https://atlight.github.io/formats/dxf-leader.html
|
||||
# GDAL: DXF LEADER implementation:
|
||||
# https://github.com/OSGeo/gdal/blob/master/gdal/ogr/ogrsf_frmts/dxf/ogrdxf_leader.cpp
|
||||
# LEADER DXF Reference:
|
||||
# http://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-396B2369-F89F-47D7-8223-8B7FB794F9F3
|
||||
assert leader.dxftype() == "LEADER"
|
||||
|
||||
vertices = Vec3.list(leader.vertices) # WCS
|
||||
if len(vertices) < 2:
|
||||
# This LEADER entities should be removed by the auditor if loaded or
|
||||
# ignored at exporting, if created by an ezdxf-user (log).
|
||||
raise ValueError("More than 1 vertex required.")
|
||||
dxf = leader.dxf
|
||||
doc = leader.doc
|
||||
|
||||
# Some default values depend on the measurement system
|
||||
# 0/1 = imperial/metric
|
||||
if doc:
|
||||
measurement = doc.header.get("$MEASUREMENT", 0)
|
||||
else:
|
||||
measurement = 0
|
||||
|
||||
# Set default styling attributes values:
|
||||
dimtad = 1
|
||||
dimgap = 0.625 if measurement else 0.0625
|
||||
dimscale = 1.0
|
||||
dimclrd = dxf.color
|
||||
dimltype = dxf.linetype
|
||||
dimlwd = dxf.lineweight
|
||||
override = None
|
||||
|
||||
if doc:
|
||||
# get styling attributes from associated DIMSTYLE and/or XDATA override
|
||||
override = leader.override()
|
||||
dimtad = override.get("dimtad", dimtad)
|
||||
dimgap = override.get("dimgap", dimgap)
|
||||
dimscale = override.get("dimscale", dimscale)
|
||||
if dimscale == 0.0: # special but unknown meaning
|
||||
dimscale = 1.0
|
||||
dimclrd = override.get("dimclrd", dimclrd)
|
||||
dimltype = override.get("dimltype", dimltype)
|
||||
dimlwd = override.get("dimlwd", dimlwd)
|
||||
|
||||
text_width = dxf.text_width
|
||||
hook_line_vector = Vec3(dxf.horizontal_direction)
|
||||
has_text_annotation = dxf.annotation_type == 0
|
||||
|
||||
if has_text_annotation and dxf.has_hookline:
|
||||
if dxf.hookline_direction == 1:
|
||||
hook_line_vector = -hook_line_vector
|
||||
if dimtad != 0 and text_width > 0:
|
||||
hook_line = hook_line_vector * (dimgap * dimscale + text_width)
|
||||
vertices.append(vertices[-1] + hook_line)
|
||||
|
||||
dxfattribs = leader.graphic_properties()
|
||||
dxfattribs["color"] = dimclrd
|
||||
dxfattribs["linetype"] = dimltype
|
||||
dxfattribs["lineweight"] = dimlwd
|
||||
|
||||
if dxfattribs.get("color") == BYBLOCK:
|
||||
dxfattribs["color"] = dxf.block_color
|
||||
|
||||
if dxf.path_type == 1: # Spline
|
||||
start_tangent = vertices[1] - vertices[0]
|
||||
end_tangent = vertices[-1] - vertices[-2]
|
||||
bspline = fit_points_to_cad_cv(vertices, tangents=[start_tangent, end_tangent])
|
||||
spline = cast("Spline", factory.new("SPLINE", doc=doc))
|
||||
spline.apply_construction_tool(bspline)
|
||||
yield spline
|
||||
else:
|
||||
attribs = dict(dxfattribs)
|
||||
prev = vertices[0]
|
||||
for vertex in vertices[1:]:
|
||||
attribs["start"] = prev
|
||||
attribs["end"] = vertex
|
||||
yield cast(
|
||||
"Line",
|
||||
factory.new(dxftype="LINE", dxfattribs=attribs, doc=doc),
|
||||
)
|
||||
prev = vertex
|
||||
|
||||
if dxf.has_arrowhead and override:
|
||||
arrow_name = override.get("dimldrblk", "")
|
||||
if arrow_name is None:
|
||||
return
|
||||
size = override.get("dimasz", 2.5 if measurement else 0.1875) * dimscale
|
||||
rotation = (vertices[0] - vertices[1]).angle_deg
|
||||
if doc and arrow_name in doc.blocks:
|
||||
dxfattribs.update(
|
||||
{
|
||||
"name": arrow_name,
|
||||
"insert": vertices[0],
|
||||
"rotation": rotation,
|
||||
"xscale": size,
|
||||
"yscale": size,
|
||||
"zscale": size,
|
||||
}
|
||||
)
|
||||
# create a virtual block reference
|
||||
insert = cast(
|
||||
"Insert", factory.new("INSERT", dxfattribs=dxfattribs, doc=doc)
|
||||
)
|
||||
yield from insert.virtual_entities()
|
||||
else: # render standard arrows
|
||||
yield from ARROWS.virtual_entities(
|
||||
name=arrow_name,
|
||||
insert=vertices[0],
|
||||
size=size,
|
||||
rotation=rotation,
|
||||
dxfattribs=dxfattribs,
|
||||
)
|
||||
21
.venv/lib/python3.12/site-packages/ezdxf/render/linetypes.py
Normal file
21
.venv/lib/python3.12/site-packages/ezdxf/render/linetypes.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from typing import Iterable, Iterator
|
||||
import ezdxf
|
||||
from ezdxf.math import UVec
|
||||
from ._linetypes import _LineTypeRenderer, LineSegment
|
||||
|
||||
if ezdxf.options.use_c_ext:
|
||||
try:
|
||||
from ezdxf.acc.linetypes import _LineTypeRenderer # type: ignore
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class LineTypeRenderer(_LineTypeRenderer):
|
||||
def line_segments(self, vertices: Iterable[UVec]) -> Iterator[LineSegment]:
|
||||
last = None
|
||||
for vertex in vertices:
|
||||
if last is not None:
|
||||
yield from self.line_segment(last, vertex)
|
||||
last = vertex
|
||||
1803
.venv/lib/python3.12/site-packages/ezdxf/render/mesh.py
Normal file
1803
.venv/lib/python3.12/site-packages/ezdxf/render/mesh.py
Normal file
File diff suppressed because it is too large
Load Diff
1637
.venv/lib/python3.12/site-packages/ezdxf/render/mleader.py
Normal file
1637
.venv/lib/python3.12/site-packages/ezdxf/render/mleader.py
Normal file
File diff suppressed because it is too large
Load Diff
233
.venv/lib/python3.12/site-packages/ezdxf/render/mline.py
Normal file
233
.venv/lib/python3.12/site-packages/ezdxf/render/mline.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, cast, Sequence, Any
|
||||
from itertools import chain
|
||||
from ezdxf.entities import factory, MLineStyle
|
||||
from ezdxf.math import Vec3, OCS
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import MLine, DXFGraphic, Hatch, Line, Arc
|
||||
|
||||
__all__ = ["virtual_entities"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
# The MLINE geometry stored in vertices, is the final geometry,
|
||||
# scaling factor, justification and MLineStyle settings are already
|
||||
# applied.
|
||||
|
||||
|
||||
def _dxfattribs(mline) -> dict[str, Any]:
|
||||
attribs = mline.graphic_properties()
|
||||
# True color value of MLINE is ignored by CAD applications:
|
||||
if "true_color" in attribs:
|
||||
del attribs["true_color"]
|
||||
return attribs
|
||||
|
||||
|
||||
def virtual_entities(mline: MLine) -> list[DXFGraphic]:
|
||||
"""Yields 'virtual' parts of MLINE as LINE, ARC and HATCH entities.
|
||||
|
||||
These entities are located at the original positions, but are not stored
|
||||
in the entity database, have no handle and are not assigned to any
|
||||
layout.
|
||||
"""
|
||||
|
||||
def filling() -> Hatch:
|
||||
attribs = _dxfattribs(mline)
|
||||
attribs["color"] = style.dxf.fill_color
|
||||
attribs["elevation"] = Vec3(ocs.from_wcs(bottom_border[0])).replace(
|
||||
x=0.0, y=0.0
|
||||
)
|
||||
attribs["extrusion"] = mline.dxf.extrusion
|
||||
hatch = cast("Hatch", factory.new("HATCH", dxfattribs=attribs, doc=doc))
|
||||
bulges: list[float] = [0.0] * (len(bottom_border) * 2)
|
||||
points = chain(
|
||||
Vec3.generate(ocs.points_from_wcs(bottom_border)),
|
||||
Vec3.generate(ocs.points_from_wcs(reversed(top_border))),
|
||||
)
|
||||
if not closed:
|
||||
if style.get_flag_state(style.END_ROUND):
|
||||
bulges[len(bottom_border) - 1] = 1.0
|
||||
if style.get_flag_state(style.START_ROUND):
|
||||
bulges[-1] = 1.0
|
||||
lwpoints = ((v.x, v.y, bulge) for v, bulge in zip(points, bulges))
|
||||
hatch.paths.add_polyline_path(lwpoints, is_closed=True)
|
||||
return hatch
|
||||
|
||||
def start_cap() -> list[DXFGraphic]:
|
||||
entities: list[DXFGraphic] = []
|
||||
if style.get_flag_state(style.START_SQUARE):
|
||||
entities.extend(create_miter(miter_points[0]))
|
||||
if style.get_flag_state(style.START_ROUND):
|
||||
entities.extend(round_caps(0, top_index, bottom_index))
|
||||
if (
|
||||
style.get_flag_state(style.START_INNER_ARC)
|
||||
and len(style.elements) > 3
|
||||
):
|
||||
start_index = ordered_indices[-2]
|
||||
end_index = ordered_indices[1]
|
||||
entities.extend(round_caps(0, start_index, end_index))
|
||||
return entities
|
||||
|
||||
def end_cap() -> list[DXFGraphic]:
|
||||
entities: list[DXFGraphic] = []
|
||||
if style.get_flag_state(style.END_SQUARE):
|
||||
entities.extend(create_miter(miter_points[-1]))
|
||||
if style.get_flag_state(style.END_ROUND):
|
||||
entities.extend(round_caps(-1, bottom_index, top_index))
|
||||
if (
|
||||
style.get_flag_state(style.END_INNER_ARC)
|
||||
and len(style.elements) > 3
|
||||
):
|
||||
start_index = ordered_indices[1]
|
||||
end_index = ordered_indices[-2]
|
||||
entities.extend(round_caps(-1, start_index, end_index))
|
||||
return entities
|
||||
|
||||
def round_caps(miter_index: int, start_index: int, end_index: int):
|
||||
color1 = style.elements[start_index].color
|
||||
color2 = style.elements[end_index].color
|
||||
start = ocs.from_wcs(miter_points[miter_index][start_index])
|
||||
end = ocs.from_wcs(miter_points[miter_index][end_index])
|
||||
return _arc_caps(start, end, color1, color2)
|
||||
|
||||
def _arc_caps(
|
||||
start: Vec3, end: Vec3, color1: int, color2: int
|
||||
) -> Sequence[Arc]:
|
||||
attribs = _dxfattribs(mline)
|
||||
center = start.lerp(end)
|
||||
radius = (end - start).magnitude / 2.0
|
||||
angle = (start - center).angle_deg
|
||||
attribs["center"] = center
|
||||
attribs["radius"] = radius
|
||||
attribs["color"] = color1
|
||||
attribs["start_angle"] = angle
|
||||
attribs["end_angle"] = angle + (180 if color1 == color2 else 90)
|
||||
arc1 = cast("Arc", factory.new("ARC", dxfattribs=attribs, doc=doc))
|
||||
if color1 == color2:
|
||||
return (arc1,)
|
||||
attribs["start_angle"] = angle + 90
|
||||
attribs["end_angle"] = angle + 180
|
||||
attribs["color"] = color2
|
||||
arc2 = cast("Arc", factory.new("ARC", dxfattribs=attribs, doc=doc))
|
||||
return arc1, arc2
|
||||
|
||||
def lines() -> list[Line]:
|
||||
prev = None
|
||||
_lines: list[Line] = []
|
||||
attribs = _dxfattribs(mline)
|
||||
|
||||
for miter in miter_points:
|
||||
if prev is not None:
|
||||
for index, element in enumerate(style.elements):
|
||||
attribs["start"] = prev[index]
|
||||
attribs["end"] = miter[index]
|
||||
attribs["color"] = element.color
|
||||
attribs["linetype"] = element.linetype
|
||||
_lines.append(
|
||||
cast(
|
||||
"Line",
|
||||
factory.new("LINE", dxfattribs=attribs, doc=doc),
|
||||
)
|
||||
)
|
||||
prev = miter
|
||||
return _lines
|
||||
|
||||
def display_miter():
|
||||
_lines = []
|
||||
skip = set()
|
||||
skip.add(len(miter_points) - 1)
|
||||
if not closed:
|
||||
skip.add(0)
|
||||
for index, miter in enumerate(miter_points):
|
||||
if index not in skip:
|
||||
_lines.extend(create_miter(miter))
|
||||
return _lines
|
||||
|
||||
def create_miter(miter) -> list[Line]:
|
||||
_lines: list[Line] = []
|
||||
attribs = _dxfattribs(mline)
|
||||
top = miter[top_index]
|
||||
bottom = miter[bottom_index]
|
||||
zero = bottom.lerp(top)
|
||||
element = style.elements[top_index]
|
||||
attribs["start"] = top
|
||||
attribs["end"] = zero
|
||||
attribs["color"] = element.color
|
||||
attribs["linetype"] = element.linetype
|
||||
_lines.append(
|
||||
cast("Line", factory.new("LINE", dxfattribs=attribs, doc=doc))
|
||||
)
|
||||
element = style.elements[bottom_index]
|
||||
attribs["start"] = bottom
|
||||
attribs["end"] = zero
|
||||
attribs["color"] = element.color
|
||||
attribs["linetype"] = element.linetype
|
||||
_lines.append(
|
||||
cast("Line", factory.new("LINE", dxfattribs=attribs, doc=doc))
|
||||
)
|
||||
return _lines
|
||||
|
||||
entities: list[DXFGraphic] = []
|
||||
if not mline.is_alive or mline.doc is None or len(mline.vertices) < 2:
|
||||
return entities
|
||||
|
||||
style: MLineStyle = mline.style # type: ignore
|
||||
if style is None:
|
||||
return entities
|
||||
|
||||
doc = mline.doc
|
||||
ocs = OCS(mline.dxf.extrusion)
|
||||
element_count = len(style.elements)
|
||||
closed = mline.is_closed
|
||||
ordered_indices = style.ordered_indices()
|
||||
bottom_index = ordered_indices[0]
|
||||
top_index = ordered_indices[-1]
|
||||
bottom_border: list[Vec3] = []
|
||||
top_border: list[Vec3] = []
|
||||
miter_points: list[list[Vec3]] = []
|
||||
|
||||
for vertex in mline.vertices:
|
||||
offsets = vertex.line_params
|
||||
if len(offsets) != element_count:
|
||||
logger.debug(
|
||||
f"Invalid line parametrization for vertex {len(miter_points)} "
|
||||
f"in {str(mline)}."
|
||||
)
|
||||
return entities
|
||||
location = vertex.location
|
||||
miter_direction = vertex.miter_direction
|
||||
miter = []
|
||||
for offset in offsets:
|
||||
try:
|
||||
length = offset[0]
|
||||
except IndexError: # DXFStructureError?
|
||||
length = 0
|
||||
miter.append(location + miter_direction * length)
|
||||
miter_points.append(miter)
|
||||
top_border.append(miter[top_index])
|
||||
bottom_border.append(miter[bottom_index])
|
||||
|
||||
if closed:
|
||||
miter_points.append(miter_points[0])
|
||||
top_border.append(top_border[0])
|
||||
bottom_border.append(bottom_border[0])
|
||||
|
||||
if not closed:
|
||||
entities.extend(start_cap())
|
||||
|
||||
entities.extend(lines())
|
||||
|
||||
if style.get_flag_state(style.MITER):
|
||||
entities.extend(display_miter())
|
||||
|
||||
if not closed:
|
||||
entities.extend(end_cap())
|
||||
|
||||
if style.get_flag_state(style.FILL):
|
||||
entities.insert(0, filling())
|
||||
|
||||
return entities
|
||||
88
.venv/lib/python3.12/site-packages/ezdxf/render/point.py
Normal file
88
.venv/lib/python3.12/site-packages/ezdxf/render/point.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import cast
|
||||
import math
|
||||
from ezdxf.entities import factory, Point, DXFGraphic
|
||||
from ezdxf.math import Vec3, UCS, NULLVEC
|
||||
|
||||
|
||||
def virtual_entities(
|
||||
point: Point, pdsize: float = 1, pdmode: int = 0
|
||||
) -> list[DXFGraphic]:
|
||||
"""Yields point graphic as DXF primitives LINE and CIRCLE entities.
|
||||
The dimensionless point is rendered as zero-length line!
|
||||
|
||||
Check for this condition::
|
||||
|
||||
e.dxftype() == 'LINE' and e.dxf.start.isclose(e.dxf.end)
|
||||
|
||||
if the rendering engine can't handle zero-length lines.
|
||||
|
||||
|
||||
Args:
|
||||
point: DXF POINT entity
|
||||
pdsize: point size in drawing units
|
||||
pdmode: point styling mode, see :class:`~ezdxf.entities.Point` class
|
||||
|
||||
"""
|
||||
|
||||
def add_line_symmetrical(offset: Vec3):
|
||||
dxfattribs["start"] = ucs.to_wcs(-offset)
|
||||
dxfattribs["end"] = ucs.to_wcs(offset)
|
||||
entities.append(cast(DXFGraphic, factory.new("LINE", dxfattribs)))
|
||||
|
||||
def add_line(s: Vec3, e: Vec3):
|
||||
dxfattribs["start"] = ucs.to_wcs(s)
|
||||
dxfattribs["end"] = ucs.to_wcs(e)
|
||||
entities.append(cast(DXFGraphic, factory.new("LINE", dxfattribs)))
|
||||
|
||||
center = point.dxf.location
|
||||
# This is not a real OCS! Defines just the point orientation,
|
||||
# location is in WCS!
|
||||
ocs = point.ocs()
|
||||
ucs = UCS(origin=center, ux=ocs.ux, uz=ocs.uz)
|
||||
|
||||
# The point angle is clockwise oriented:
|
||||
ucs = ucs.rotate_local_z(math.radians(-point.dxf.angle))
|
||||
|
||||
entities: list[DXFGraphic] = []
|
||||
gfx = point.graphic_properties()
|
||||
|
||||
radius = pdsize * 0.5
|
||||
has_circle = bool(pdmode & 32)
|
||||
has_square = bool(pdmode & 64)
|
||||
style = pdmode & 7
|
||||
|
||||
dxfattribs = dict(gfx)
|
||||
if style == 0: # . dimensionless point as zero-length line
|
||||
add_line_symmetrical(NULLVEC)
|
||||
# style == 1: no point symbol
|
||||
elif style == 2: # + cross
|
||||
add_line_symmetrical(Vec3(pdsize, 0))
|
||||
add_line_symmetrical(Vec3(0, pdsize))
|
||||
elif style == 3: # x cross
|
||||
add_line_symmetrical(Vec3(pdsize, pdsize))
|
||||
add_line_symmetrical(Vec3(pdsize, -pdsize))
|
||||
elif style == 4: # ' tick
|
||||
add_line(NULLVEC, Vec3(0, radius))
|
||||
if has_square:
|
||||
x1 = -radius
|
||||
x2 = radius
|
||||
y1 = -radius
|
||||
y2 = radius
|
||||
add_line(Vec3(x1, y1), Vec3(x2, y1))
|
||||
add_line(Vec3(x2, y1), Vec3(x2, y2))
|
||||
add_line(Vec3(x2, y2), Vec3(x1, y2))
|
||||
add_line(Vec3(x1, y2), Vec3(x1, y1))
|
||||
if has_circle:
|
||||
dxfattribs = dict(gfx)
|
||||
if point.dxf.hasattr("extrusion"):
|
||||
dxfattribs["extrusion"] = ocs.uz
|
||||
dxfattribs["center"] = ocs.from_wcs(center)
|
||||
else:
|
||||
dxfattribs["center"] = center
|
||||
dxfattribs["radius"] = radius
|
||||
entities.append(cast(DXFGraphic, factory.new("CIRCLE", dxfattribs)))
|
||||
|
||||
return entities
|
||||
248
.venv/lib/python3.12/site-packages/ezdxf/render/polyline.py
Normal file
248
.venv/lib/python3.12/site-packages/ezdxf/render/polyline.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Union
|
||||
import logging
|
||||
import math
|
||||
|
||||
from ezdxf.entities import factory
|
||||
from ezdxf.lldxf.const import VERTEXNAMES
|
||||
from ezdxf.math import Vec3, bulge_to_arc, OCS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import LWPolyline, Polyline, Line, Arc, Face3d, Polymesh
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
def virtual_lwpolyline_entities(
|
||||
lwpolyline: LWPolyline,
|
||||
) -> Iterable[Union[Line, Arc]]:
|
||||
"""Yields 'virtual' entities of LWPOLYLINE as LINE or ARC objects.
|
||||
|
||||
These entities are located at the original positions, but are not stored in
|
||||
the entity database, have no handle and are not assigned to any layout.
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
assert lwpolyline.dxftype() == "LWPOLYLINE"
|
||||
|
||||
points = lwpolyline.get_points("xyb")
|
||||
if len(points) < 2:
|
||||
return
|
||||
|
||||
if lwpolyline.closed:
|
||||
points.append(points[0])
|
||||
|
||||
yield from _virtual_polyline_entities(
|
||||
points=points,
|
||||
elevation=lwpolyline.dxf.elevation,
|
||||
extrusion=lwpolyline.dxf.get("extrusion", None),
|
||||
dxfattribs=lwpolyline.graphic_properties(),
|
||||
doc=lwpolyline.doc,
|
||||
)
|
||||
|
||||
|
||||
def virtual_polyline_entities(
|
||||
polyline: Polyline,
|
||||
) -> Iterable[Union[Line, Arc, Face3d]]:
|
||||
"""Yields 'virtual' entities of POLYLINE as LINE, ARC or 3DFACE objects.
|
||||
|
||||
These entities are located at the original positions, but are not stored in
|
||||
the entity database, have no handle and are not assigned to any layout.
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
assert polyline.dxftype() == "POLYLINE"
|
||||
if polyline.is_2d_polyline:
|
||||
return virtual_polyline2d_entities(polyline)
|
||||
elif polyline.is_3d_polyline:
|
||||
return virtual_polyline3d_entities(polyline)
|
||||
elif polyline.is_polygon_mesh:
|
||||
return virtual_polymesh_entities(polyline)
|
||||
elif polyline.is_poly_face_mesh:
|
||||
return virtual_polyface_entities(polyline)
|
||||
return []
|
||||
|
||||
|
||||
def virtual_polyline2d_entities(
|
||||
polyline: Polyline,
|
||||
) -> Iterable[Union[Line, Arc]]:
|
||||
"""Yields 'virtual' entities of 2D POLYLINE as LINE or ARC objects.
|
||||
|
||||
These entities are located at the original positions, but are not stored in
|
||||
the entity database, have no handle and are not assigned to any layout.
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
assert polyline.dxftype() == "POLYLINE"
|
||||
assert polyline.is_2d_polyline
|
||||
if len(polyline.vertices) < 2:
|
||||
return
|
||||
|
||||
points = [
|
||||
(v.dxf.location.x, v.dxf.location.y, v.dxf.bulge)
|
||||
for v in polyline.vertices
|
||||
]
|
||||
if polyline.is_closed:
|
||||
points.append(points[0])
|
||||
|
||||
yield from _virtual_polyline_entities(
|
||||
points=points,
|
||||
elevation=Vec3(polyline.dxf.get("elevation", (0, 0, 0))).z,
|
||||
extrusion=polyline.dxf.get("extrusion", None),
|
||||
dxfattribs=polyline.graphic_properties(),
|
||||
doc=polyline.doc,
|
||||
)
|
||||
|
||||
|
||||
def _virtual_polyline_entities(
|
||||
points, elevation: float, extrusion: Vec3, dxfattribs: dict, doc
|
||||
) -> Iterable[Union[Line, Arc]]:
|
||||
ocs = OCS(extrusion) if extrusion else OCS()
|
||||
prev_point = None
|
||||
prev_bulge = None
|
||||
|
||||
for x, y, bulge in points:
|
||||
point = Vec3(x, y, elevation)
|
||||
if prev_point is None:
|
||||
prev_point = point
|
||||
prev_bulge = bulge
|
||||
continue
|
||||
|
||||
attribs = dict(dxfattribs)
|
||||
if prev_bulge != 0:
|
||||
center, start_angle, end_angle, radius = bulge_to_arc(
|
||||
prev_point, point, prev_bulge
|
||||
)
|
||||
if radius > 0:
|
||||
attribs["center"] = Vec3(center.x, center.y, elevation)
|
||||
attribs["radius"] = radius
|
||||
attribs["start_angle"] = math.degrees(start_angle)
|
||||
attribs["end_angle"] = math.degrees(end_angle)
|
||||
if extrusion:
|
||||
attribs["extrusion"] = extrusion
|
||||
yield factory.new(dxftype="ARC", dxfattribs=attribs, doc=doc)
|
||||
else:
|
||||
attribs["start"] = ocs.to_wcs(prev_point)
|
||||
attribs["end"] = ocs.to_wcs(point)
|
||||
yield factory.new(dxftype="LINE", dxfattribs=attribs, doc=doc)
|
||||
prev_point = point
|
||||
prev_bulge = bulge
|
||||
|
||||
|
||||
def virtual_polyline3d_entities(polyline: Polyline) -> Iterable[Line]:
|
||||
"""Yields 'virtual' entities of 3D POLYLINE as LINE objects.
|
||||
|
||||
This entities are located at the original positions, but are not stored in
|
||||
the entity database, have no handle and are not assigned to any layout.
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
assert polyline.dxftype() == "POLYLINE"
|
||||
assert polyline.is_3d_polyline
|
||||
if len(polyline.vertices) < 2:
|
||||
return
|
||||
doc = polyline.doc
|
||||
vertices = polyline.vertices
|
||||
dxfattribs = polyline.graphic_properties()
|
||||
start = -1 if polyline.is_closed else 0
|
||||
for index in range(start, len(vertices) - 1):
|
||||
dxfattribs["start"] = vertices[index].dxf.location
|
||||
dxfattribs["end"] = vertices[index + 1].dxf.location
|
||||
yield factory.new(dxftype="LINE", dxfattribs=dxfattribs, doc=doc) # type: ignore
|
||||
|
||||
|
||||
def virtual_polymesh_entities(polyline: Polyline) -> Iterable[Face3d]:
|
||||
"""Yields 'virtual' entities of POLYMESH as 3DFACE objects.
|
||||
|
||||
This entities are located at the original positions, but are not stored in
|
||||
the entity database, have no handle and are not assigned to any layout.
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
polymesh: "Polymesh" = polyline # type: ignore
|
||||
assert polymesh.dxftype() == "POLYLINE"
|
||||
assert polymesh.is_polygon_mesh
|
||||
|
||||
doc = polymesh.doc
|
||||
mesh = polymesh.get_mesh_vertex_cache()
|
||||
dxfattribs = polymesh.graphic_properties()
|
||||
m_count = polymesh.dxf.m_count
|
||||
n_count = polymesh.dxf.n_count
|
||||
m_range = m_count - int(not polymesh.is_m_closed)
|
||||
n_range = n_count - int(not polymesh.is_n_closed)
|
||||
|
||||
for m in range(m_range):
|
||||
for n in range(n_range):
|
||||
next_m = (m + 1) % m_count
|
||||
next_n = (n + 1) % n_count
|
||||
|
||||
dxfattribs["vtx0"] = mesh[m, n]
|
||||
dxfattribs["vtx1"] = mesh[next_m, n]
|
||||
dxfattribs["vtx2"] = mesh[next_m, next_n]
|
||||
dxfattribs["vtx3"] = mesh[m, next_n]
|
||||
yield factory.new(dxftype="3DFACE", dxfattribs=dxfattribs, doc=doc) # type: ignore
|
||||
|
||||
|
||||
def virtual_polyface_entities(polyline: Polyline) -> Iterable[Face3d]:
|
||||
"""Yields 'virtual' entities of POLYFACE as 3DFACE objects.
|
||||
|
||||
This entities are located at the original positions, but are not stored in
|
||||
the entity database, have no handle and are not assigned to any layout.
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
assert polyline.dxftype() == "POLYLINE"
|
||||
assert polyline.is_poly_face_mesh
|
||||
|
||||
doc = polyline.doc
|
||||
vertices = polyline.vertices
|
||||
base_attribs = polyline.graphic_properties()
|
||||
|
||||
face_records = (v for v in vertices if v.is_face_record)
|
||||
for face in face_records:
|
||||
# check if vtx0, vtx1 and vtx2 exist
|
||||
for name in VERTEXNAMES[:-1]:
|
||||
if not face.dxf.hasattr(name):
|
||||
logger.info(
|
||||
f"skipped face {str(face)} with less than 3 vertices"
|
||||
f"in PolyFaceMesh(#{str(polyline.dxf.handle)})"
|
||||
)
|
||||
continue
|
||||
# Alternate solutions: return a face with less than 3 vertices
|
||||
# as LINE (breaks the method signature) or as degenerated 3DFACE
|
||||
# (vtx0, vtx1, vtx1, vtx1)
|
||||
|
||||
face3d_attribs = dict(base_attribs)
|
||||
face3d_attribs.update(face.graphic_properties())
|
||||
invisible = 0
|
||||
pos = 1
|
||||
indices = (
|
||||
(face.dxf.get(name), name)
|
||||
for name in VERTEXNAMES
|
||||
if face.dxf.hasattr(name)
|
||||
)
|
||||
for index, name in indices:
|
||||
# vertex indices are 1-based, negative indices indicate invisible edges
|
||||
if index < 0:
|
||||
index = abs(index)
|
||||
invisible += pos
|
||||
# python list `vertices` is 0-based
|
||||
face3d_attribs[name] = vertices[index - 1].dxf.location
|
||||
# vertex index bit encoded: 1=0b0001, 2=0b0010, 3=0b0100, 4=0b1000
|
||||
pos <<= 1
|
||||
|
||||
if "vtx3" not in face3d_attribs:
|
||||
# A triangle face ends with two identical vertices vtx2 and vtx3.
|
||||
# This is a requirement defined by AutoCAD.
|
||||
face3d_attribs["vtx3"] = face3d_attribs["vtx2"]
|
||||
|
||||
face3d_attribs["invisible"] = invisible
|
||||
yield factory.new(dxftype="3DFACE", dxfattribs=face3d_attribs, doc=doc) # type: ignore
|
||||
236
.venv/lib/python3.12/site-packages/ezdxf/render/r12spline.py
Normal file
236
.venv/lib/python3.12/site-packages/ezdxf/render/r12spline.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# Copyright (c) 2018-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
"""
|
||||
DXF R12 Splines
|
||||
===============
|
||||
|
||||
DXF R12 supports 2d B-splines, but Autodesk do not document the usage in the
|
||||
DXF Reference. The base entity for splines in DXF R12 is the POLYLINE entity.
|
||||
|
||||
Transformed Into 3D Space
|
||||
-------------------------
|
||||
|
||||
The spline itself is always in a plane, but as any 2D entity, the spline can be
|
||||
transformed into the 3D object by elevation, extrusion and thickness/width.
|
||||
|
||||
Open Quadratic Spline with Fit Vertices
|
||||
-------------------------------------
|
||||
|
||||
Example: 2D_SPLINE_QUADRATIC.dxf
|
||||
expected knot vector: open uniform
|
||||
degree: 2
|
||||
order: 3
|
||||
|
||||
POLYLINE:
|
||||
flags (70): 4 = SPLINE_FIT_VERTICES_ADDED
|
||||
smooth type (75): 5 = QUADRATIC_BSPLINE
|
||||
|
||||
Sequence of VERTEX
|
||||
flags (70): SPLINE_VERTEX_CREATED = 8 # Spline vertex created by spline-fitting
|
||||
|
||||
This vertices are the curve vertices of the spline (fitted).
|
||||
|
||||
Frame control vertices appear after the curve vertices.
|
||||
|
||||
Sequence of VERTEX
|
||||
flags (70): SPLINE_FRAME_CONTROL_POINT = 16
|
||||
|
||||
No control point at the starting point, but a control point at the end point,
|
||||
last control point == last fit vertex
|
||||
|
||||
Closed Quadratic Spline with Fit Vertices
|
||||
-----------------------------------------
|
||||
|
||||
Example: 2D_SPLINE_QUADRATIC_CLOSED.dxf
|
||||
expected knot vector: closed uniform
|
||||
degree: 2
|
||||
order: 3
|
||||
|
||||
POLYLINE:
|
||||
flags (70): 5 = CLOSED | SPLINE_FIT_VERTICES_ADDED
|
||||
smooth type (75): 5 = QUADRATIC_BSPLINE
|
||||
|
||||
Sequence of VERTEX
|
||||
flags (70): SPLINE_VERTEX_CREATED = 8 # Spline vertex created by spline-fitting
|
||||
|
||||
Frame control vertices appear after the curve vertices.
|
||||
|
||||
Sequence of VERTEX
|
||||
flags (70): SPLINE_FRAME_CONTROL_POINT = 16
|
||||
|
||||
|
||||
Open Cubic Spline with Fit Vertices
|
||||
-----------------------------------
|
||||
|
||||
Example: 2D_SPLINE_CUBIC.dxf
|
||||
expected knot vector: open uniform
|
||||
degree: 3
|
||||
order: 4
|
||||
|
||||
POLYLINE:
|
||||
flags (70): 4 = SPLINE_FIT_VERTICES_ADDED
|
||||
smooth type (75): 6 = CUBIC_BSPLINE
|
||||
|
||||
Sequence of VERTEX
|
||||
flags (70): SPLINE_VERTEX_CREATED = 8 # Spline vertex created by spline-fitting
|
||||
|
||||
This vertices are the curve vertices of the spline (fitted).
|
||||
|
||||
Frame control vertices appear after the curve vertices.
|
||||
|
||||
Sequence of VERTEX
|
||||
flags (70): SPLINE_FRAME_CONTROL_POINT = 16
|
||||
|
||||
No control point at the starting point, but a control point at the end point,
|
||||
last control point == last fit vertex
|
||||
|
||||
Closed Curve With Extra Vertices Created
|
||||
----------------------------------------
|
||||
|
||||
Example: 2D_FIT_CURVE_CLOSED.dxf
|
||||
|
||||
POLYLINE:
|
||||
flags (70): 3 = CLOSED | CURVE_FIT_VERTICES_ADDED
|
||||
|
||||
Vertices with bulge values:
|
||||
|
||||
flags (70): 1 = EXTRA_VERTEX_CREATED
|
||||
Vertex 70=0, Vertex 70=1, Vertex 70=0, Vertex 70=1
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.math import BSpline, closed_uniform_bspline, Vec3, UCS, UVec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.layouts import BaseLayout
|
||||
from ezdxf.entities import Polyline
|
||||
|
||||
|
||||
class R12Spline:
|
||||
"""DXF R12 supports 2D B-splines, but Autodesk do not document the usage
|
||||
in the DXF Reference. The base entity for splines in DXF R12 is the POLYLINE
|
||||
entity. The spline itself is always in a plane, but as any 2D entity, the
|
||||
spline can be transformed into the 3D object by elevation and extrusion
|
||||
(:ref:`OCS`, :ref:`UCS`).
|
||||
|
||||
This way it was possible to store the spline parameters in the DXF R12 file,
|
||||
to allow CAD applications to modify the spline parameters and rerender the
|
||||
B-spline afterward again as polyline approximation. Therefore, the result is
|
||||
not better than an approximation by the :class:`Spline` class, it is also
|
||||
just a POLYLINE entity, but maybe someone need exact this tool in the
|
||||
future.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
control_points: Iterable[UVec],
|
||||
degree: int = 2,
|
||||
closed: bool = True,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
control_points: B-spline control frame vertices
|
||||
degree: degree of B-spline, only 2 and 3 is supported
|
||||
closed: ``True`` for closed curve
|
||||
|
||||
"""
|
||||
self.control_points = Vec3.list(control_points)
|
||||
self.degree = degree
|
||||
self.closed = closed
|
||||
|
||||
def approximate(
|
||||
self, segments: int = 40, ucs: Optional[UCS] = None
|
||||
) -> list[UVec]:
|
||||
"""Approximate the B-spline by a polyline with `segments` line segments.
|
||||
If `ucs` is not ``None``, ucs defines an :class:`~ezdxf.math.UCS`, to
|
||||
transform the curve into :ref:`OCS`. The control points are placed
|
||||
xy-plane of the UCS, don't use z-axis coordinates, if so make sure all
|
||||
control points are in a plane parallel to the OCS base plane
|
||||
(UCS xy-plane), else the result is unpredictable and depends on the CAD
|
||||
application used to open the DXF file - it may crash.
|
||||
|
||||
Args:
|
||||
segments: count of line segments for approximation, vertex count is
|
||||
`segments` + 1
|
||||
ucs: :class:`~ezdxf.math.UCS` definition, control points in ucs
|
||||
coordinates
|
||||
|
||||
Returns:
|
||||
list of vertices in :class:`~ezdxf.math.OCS` as
|
||||
:class:`~ezdxf.math.Vec3` objects
|
||||
|
||||
"""
|
||||
if self.closed:
|
||||
spline = closed_uniform_bspline(
|
||||
self.control_points, order=self.degree + 1
|
||||
)
|
||||
else:
|
||||
spline = BSpline(self.control_points, order=self.degree + 1)
|
||||
vertices = spline.approximate(segments)
|
||||
if ucs is not None:
|
||||
vertices = (ucs.to_ocs(vertex) for vertex in vertices)
|
||||
return list(vertices)
|
||||
|
||||
def render(
|
||||
self,
|
||||
layout: BaseLayout,
|
||||
segments: int = 40,
|
||||
ucs: Optional[UCS] = None,
|
||||
dxfattribs=None,
|
||||
) -> Polyline:
|
||||
"""Renders the B-spline into `layout` as 2D :class:`~ezdxf.entities.Polyline`
|
||||
entity. Use an :class:`~ezdxf.math.UCS` to place the 2D spline in the
|
||||
3D space, see :meth:`approximate` for more information.
|
||||
|
||||
Args:
|
||||
layout: :class:`~ezdxf.layouts.BaseLayout` object
|
||||
segments: count of line segments for approximation, vertex count is
|
||||
`segments` + 1
|
||||
ucs: :class:`~ezdxf.math.UCS` definition, control points in ucs
|
||||
coordinates.
|
||||
dxfattribs: DXF attributes for :class:`~ezdxf.entities.Polyline`
|
||||
|
||||
"""
|
||||
polyline = layout.add_polyline2d(points=[], dxfattribs=dxfattribs)
|
||||
flags = polyline.SPLINE_FIT_VERTICES_ADDED
|
||||
if self.closed:
|
||||
flags |= polyline.CLOSED
|
||||
polyline.dxf.flags = flags
|
||||
|
||||
if self.degree == 2:
|
||||
smooth_type = polyline.QUADRATIC_BSPLINE
|
||||
elif self.degree == 3:
|
||||
smooth_type = polyline.CUBIC_BSPLINE
|
||||
else:
|
||||
raise ValueError("invalid degree of spline")
|
||||
polyline.dxf.smooth_type = smooth_type
|
||||
|
||||
# set OCS extrusion vector
|
||||
if ucs is not None:
|
||||
polyline.dxf.extrusion = ucs.uz
|
||||
|
||||
# add fit points in OCS
|
||||
polyline.append_vertices(
|
||||
self.approximate(segments, ucs),
|
||||
dxfattribs={
|
||||
"layer": polyline.dxf.layer,
|
||||
"flags": const.VTX_SPLINE_VERTEX_CREATED,
|
||||
},
|
||||
)
|
||||
|
||||
# add control frame points in OCS
|
||||
control_points = self.control_points
|
||||
if ucs is not None:
|
||||
control_points = list(ucs.points_to_ocs(control_points))
|
||||
polyline.dxf.elevation = (0, 0, control_points[0].z)
|
||||
polyline.append_vertices(
|
||||
control_points,
|
||||
dxfattribs={
|
||||
"layer": polyline.dxf.layer,
|
||||
"flags": const.VTX_SPLINE_FRAME_CONTROL_POINT,
|
||||
},
|
||||
)
|
||||
return polyline
|
||||
608
.venv/lib/python3.12/site-packages/ezdxf/render/trace.py
Normal file
608
.venv/lib/python3.12/site-packages/ezdxf/render/trace.py
Normal file
@@ -0,0 +1,608 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
Sequence,
|
||||
Optional,
|
||||
)
|
||||
from typing_extensions import TypeAlias
|
||||
from abc import abstractmethod
|
||||
from collections import namedtuple
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from ezdxf.math import (
|
||||
Vec2,
|
||||
Vec3,
|
||||
UVec,
|
||||
BSpline,
|
||||
ConstructionRay,
|
||||
OCS,
|
||||
ParallelRaysError,
|
||||
bulge_to_arc,
|
||||
ConstructionArc,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFGraphic, Solid, Trace, Face3d, LWPolyline, Polyline
|
||||
|
||||
__all__ = ["TraceBuilder", "LinearTrace", "CurvedTrace"]
|
||||
|
||||
LinearStation = namedtuple("LinearStation", ("vertex", "start_width", "end_width"))
|
||||
# start_width of the next (following) segment
|
||||
# end_width of the next (following) segment
|
||||
|
||||
CurveStation = namedtuple("CurveStation", ("vertex0", "vertex1"))
|
||||
|
||||
Face: TypeAlias = Tuple[Vec2, Vec2, Vec2, Vec2]
|
||||
Polygon: TypeAlias = Sequence[Vec2]
|
||||
Quadrilateral: TypeAlias = Union["Solid", "Trace", "Face3d"]
|
||||
|
||||
|
||||
class AbstractTrace:
|
||||
@abstractmethod
|
||||
def faces(self) -> Iterable[Face]:
|
||||
# vertex order: up1, down1, down2, up2
|
||||
# faces connections:
|
||||
# up2 -> next up1
|
||||
# down2 -> next down1
|
||||
pass
|
||||
|
||||
def polygon(self) -> Polygon:
|
||||
def merge(vertices: Polygon) -> Iterable[UVec]:
|
||||
if not len(vertices):
|
||||
return
|
||||
|
||||
_vertices = iter(vertices)
|
||||
prev = next(_vertices)
|
||||
yield prev
|
||||
for vertex in _vertices:
|
||||
if not prev.isclose(vertex):
|
||||
yield vertex
|
||||
prev = vertex
|
||||
|
||||
forward_contour: list[Vec2] = []
|
||||
backward_contour: list[Vec2] = []
|
||||
for up1, down1, down2, up2 in self.faces():
|
||||
forward_contour.extend((down1, down2))
|
||||
backward_contour.extend((up1, up2))
|
||||
|
||||
contour = list(merge(forward_contour))
|
||||
contour.extend(reversed(list(merge(backward_contour))))
|
||||
return contour
|
||||
|
||||
def virtual_entities(
|
||||
self, dxftype="TRACE", dxfattribs=None, doc: Optional[Drawing] = None
|
||||
) -> Iterable[Quadrilateral]:
|
||||
"""
|
||||
Yields faces as SOLID, TRACE or 3DFACE entities with DXF attributes
|
||||
given in `dxfattribs`.
|
||||
|
||||
If a document is given, the doc attribute of the new entities will be
|
||||
set and the new entities will be automatically added to the entity
|
||||
database of that document.
|
||||
|
||||
Args:
|
||||
dxftype: DXF type as string, "SOLID", "TRACE" or "3DFACE"
|
||||
dxfattribs: DXF attributes for SOLID, TRACE or 3DFACE entities
|
||||
doc: associated document
|
||||
|
||||
"""
|
||||
from ezdxf.entities.factory import new
|
||||
|
||||
if dxftype not in {"SOLID", "TRACE", "3DFACE"}:
|
||||
raise TypeError(f"Invalid dxftype {dxftype}.")
|
||||
dxfattribs = dict(dxfattribs or {})
|
||||
for face in self.faces():
|
||||
for i in range(4):
|
||||
dxfattribs[f"vtx{i}"] = face[i]
|
||||
|
||||
if dxftype != "3DFACE":
|
||||
# weird vertex order for SOLID and TRACE
|
||||
dxfattribs["vtx2"] = face[3]
|
||||
dxfattribs["vtx3"] = face[2]
|
||||
entity = new(dxftype, dxfattribs, doc)
|
||||
if doc:
|
||||
doc.entitydb.add(entity)
|
||||
yield entity # type: ignore
|
||||
|
||||
|
||||
class LinearTrace(AbstractTrace):
|
||||
"""Linear 2D banded lines like polylines with start- and end width.
|
||||
|
||||
Accepts 3D input, but z-axis is ignored.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._stations: list[LinearStation] = []
|
||||
self.abs_tol = 1e-12
|
||||
|
||||
def __len__(self):
|
||||
return len(self._stations)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._stations[item]
|
||||
|
||||
@property
|
||||
def is_started(self) -> bool:
|
||||
"""`True` if at least one station exist."""
|
||||
return bool(self._stations)
|
||||
|
||||
def add_station(
|
||||
self, point: UVec, start_width: float, end_width: Optional[float] = None
|
||||
) -> None:
|
||||
"""Add a trace station (like a vertex) at location `point`,
|
||||
`start_width` is the width of the next segment starting at this station,
|
||||
`end_width` is the end width of the next segment.
|
||||
|
||||
Adding the last location again, replaces the actual last location e.g.
|
||||
adding lines (a, b), (b, c), creates only 3 stations (a, b, c), this is
|
||||
very important to connect to/from splines.
|
||||
|
||||
Args:
|
||||
point: 2D location (vertex), z-axis of 3D vertices is ignored.
|
||||
start_width: start width of next segment
|
||||
end_width: end width of next segment
|
||||
|
||||
"""
|
||||
if end_width is None:
|
||||
end_width = start_width
|
||||
point = Vec2(point)
|
||||
stations = self._stations
|
||||
|
||||
if bool(stations) and stations[-1].vertex.isclose(point, abs_tol=self.abs_tol):
|
||||
# replace last station
|
||||
stations.pop()
|
||||
stations.append(LinearStation(point, float(start_width), float(end_width)))
|
||||
|
||||
def faces(self) -> Iterable[Face]:
|
||||
"""Yields all faces as 4-tuples of :class:`~ezdxf.math.Vec2` objects.
|
||||
|
||||
First and last miter is 90 degrees if the path is not closed, otherwise
|
||||
the intersection of first and last segment is taken into account,
|
||||
a closed path has to have explicit the same last and first vertex.
|
||||
|
||||
"""
|
||||
stations = self._stations
|
||||
count = len(stations)
|
||||
if count < 2: # Two or more stations required to create faces
|
||||
return
|
||||
|
||||
def offset_rays(
|
||||
segment: int,
|
||||
) -> tuple[ConstructionRay, ConstructionRay]:
|
||||
"""Create offset rays from segment offset vertices."""
|
||||
|
||||
def ray(v1, v2):
|
||||
if v1.isclose(v2):
|
||||
# vertices too close to define a ray, offset ray is parallel to segment:
|
||||
angle = (
|
||||
stations[segment].vertex - stations[segment + 1].vertex
|
||||
).angle
|
||||
return ConstructionRay(v1, angle)
|
||||
else:
|
||||
return ConstructionRay(v1, v2)
|
||||
|
||||
left1, left2, right1, right2 = segments[segment]
|
||||
return ray(left1, left2), ray(right1, right2)
|
||||
|
||||
def intersect(
|
||||
ray1: ConstructionRay, ray2: ConstructionRay, default: Vec2
|
||||
) -> Vec2:
|
||||
"""Intersect two rays but take parallel rays into account."""
|
||||
# check for nearly parallel rays pi/100 ~1.8 degrees
|
||||
angle = abs(ray1.direction.angle_between(ray2.direction))
|
||||
if angle < 0.031415 or abs(math.pi - angle) < 0.031415:
|
||||
return default
|
||||
try:
|
||||
return ray1.intersect(ray2)
|
||||
except ParallelRaysError:
|
||||
return default
|
||||
|
||||
# Path has to be explicit closed by vertices:
|
||||
is_closed = stations[0].vertex.isclose(stations[-1].vertex)
|
||||
|
||||
segments = []
|
||||
# Each segment has 4 offset vertices normal to the line from start- to
|
||||
# end vertex
|
||||
# 1st vertex left of line at the start, distance = start_width/2
|
||||
# 2nd vertex left of line at the end, distance = end_width/2
|
||||
# 3rd vertex right of line at the start, distance = start_width/2
|
||||
# 4th vertex right of line at the end, distance = end_width/2
|
||||
for station in range(count - 1):
|
||||
start_vertex, start_width, end_width = stations[station]
|
||||
end_vertex = stations[station + 1].vertex
|
||||
# Start- and end vertex are never to close together, close stations
|
||||
# will be merged in method LinearTrace.add_station().
|
||||
segments.append(
|
||||
_normal_offset_points(start_vertex, end_vertex, start_width, end_width)
|
||||
)
|
||||
|
||||
# offset rays:
|
||||
# 1 is the upper or left of line
|
||||
# 2 is the lower or right of line
|
||||
offset_ray1, offset_ray2 = offset_rays(0)
|
||||
prev_offset_ray1 = None
|
||||
prev_offset_ray2 = None
|
||||
|
||||
# Store last vertices explicit, they get modified for closed paths.
|
||||
last_up1, last_up2, last_down1, last_down2 = segments[-1]
|
||||
|
||||
for i in range(len(segments)):
|
||||
up1, up2, down1, down2 = segments[i]
|
||||
if i == 0:
|
||||
# Set first vertices of the first face.
|
||||
if is_closed:
|
||||
# Compute first two vertices as intersection of first and
|
||||
# last segment
|
||||
last_offset_ray1, last_offset_ray2 = offset_rays(len(segments) - 1)
|
||||
vtx0 = intersect(last_offset_ray1, offset_ray1, up1)
|
||||
vtx1 = intersect(last_offset_ray2, offset_ray2, down1)
|
||||
|
||||
# Store last vertices for the closing face.
|
||||
last_up2 = vtx0
|
||||
last_down2 = vtx1
|
||||
else:
|
||||
# Set first two vertices of the first face for an open path.
|
||||
vtx0 = up1
|
||||
vtx1 = down1
|
||||
prev_offset_ray1 = offset_ray1
|
||||
prev_offset_ray2 = offset_ray2
|
||||
else:
|
||||
# Compute first two vertices for the actual face.
|
||||
vtx0 = intersect(prev_offset_ray1, offset_ray1, up1) # type: ignore
|
||||
vtx1 = intersect(prev_offset_ray2, offset_ray2, down1) # type: ignore
|
||||
|
||||
if i < len(segments) - 1:
|
||||
# Compute last two vertices for the actual face.
|
||||
next_offset_ray1, next_offset_ray2 = offset_rays(i + 1)
|
||||
vtx2 = intersect(next_offset_ray2, offset_ray2, down2)
|
||||
vtx3 = intersect(next_offset_ray1, offset_ray1, up2)
|
||||
prev_offset_ray1 = offset_ray1
|
||||
prev_offset_ray2 = offset_ray2
|
||||
offset_ray1 = next_offset_ray1
|
||||
offset_ray2 = next_offset_ray2
|
||||
else:
|
||||
# Pickup last two vertices for the last face.
|
||||
vtx2 = last_down2
|
||||
vtx3 = last_up2
|
||||
yield vtx0, vtx1, vtx2, vtx3
|
||||
|
||||
|
||||
def _normal_offset_points(
|
||||
start: Vec2, end: Vec2, start_width: float, end_width: float
|
||||
) -> Face:
|
||||
dir_vector = (end - start).normalize()
|
||||
ortho = dir_vector.orthogonal(True)
|
||||
offset_start = ortho.normalize(start_width / 2)
|
||||
offset_end = ortho.normalize(end_width / 2)
|
||||
return (
|
||||
start + offset_start,
|
||||
end + offset_end,
|
||||
start - offset_start,
|
||||
end - offset_end,
|
||||
)
|
||||
|
||||
|
||||
_NULLVEC2 = Vec2((0, 0))
|
||||
|
||||
|
||||
class CurvedTrace(AbstractTrace):
|
||||
"""2D banded curves like arcs or splines with start- and end width.
|
||||
|
||||
Represents always only one curved entity and all miter of curve segments
|
||||
are perpendicular to curve tangents.
|
||||
|
||||
Accepts 3D input, but z-axis is ignored.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._stations: list[CurveStation] = []
|
||||
|
||||
def __len__(self):
|
||||
return len(self._stations)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._stations[item]
|
||||
|
||||
@classmethod
|
||||
def from_spline(
|
||||
cls,
|
||||
spline: BSpline,
|
||||
start_width: float,
|
||||
end_width: float,
|
||||
segments: int,
|
||||
) -> CurvedTrace:
|
||||
"""
|
||||
Create curved trace from a B-spline.
|
||||
|
||||
Args:
|
||||
spline: :class:`~ezdxf.math.BSpline` object
|
||||
start_width: start width
|
||||
end_width: end width
|
||||
segments: count of segments for approximation
|
||||
|
||||
"""
|
||||
curve_trace = cls()
|
||||
count = segments + 1
|
||||
t = np.linspace(0, spline.max_t, count)
|
||||
for (point, derivative), width in zip(
|
||||
spline.derivatives(t, n=1), np.linspace(start_width, end_width, count)
|
||||
):
|
||||
normal = Vec2(derivative).orthogonal(True)
|
||||
curve_trace._append(Vec2(point), normal, width)
|
||||
return curve_trace
|
||||
|
||||
@classmethod
|
||||
def from_arc(
|
||||
cls,
|
||||
arc: ConstructionArc,
|
||||
start_width: float,
|
||||
end_width: float,
|
||||
segments: int = 64,
|
||||
) -> CurvedTrace:
|
||||
"""
|
||||
Create curved trace from an arc.
|
||||
|
||||
Args:
|
||||
arc: :class:`~ezdxf.math.ConstructionArc` object
|
||||
start_width: start width
|
||||
end_width: end width
|
||||
segments: count of segments for full circle (360 degree)
|
||||
approximation, partial arcs have proportional less segments,
|
||||
but at least 3
|
||||
|
||||
Raises:
|
||||
ValueError: if arc.radius <= 0
|
||||
|
||||
"""
|
||||
if arc.radius <= 0:
|
||||
raise ValueError(f"Invalid radius: {arc.radius}.")
|
||||
curve_trace = cls()
|
||||
count = max(math.ceil(arc.angle_span / 360.0 * segments), 3) + 1
|
||||
center = Vec2(arc.center)
|
||||
for point, width in zip(
|
||||
arc.vertices(arc.angles(count)),
|
||||
np.linspace(start_width, end_width, count),
|
||||
):
|
||||
curve_trace._append(point, point - center, width)
|
||||
return curve_trace
|
||||
|
||||
def _append(self, point: Vec2, normal: Vec2, width: float) -> None:
|
||||
"""
|
||||
Add a curve trace station (like a vertex) at location `point`.
|
||||
|
||||
Args:
|
||||
point: 2D curve location (vertex), z-axis of 3D vertices is ignored.
|
||||
normal: curve normal
|
||||
width: width of station
|
||||
|
||||
"""
|
||||
if _NULLVEC2.isclose(normal):
|
||||
normal = _NULLVEC2
|
||||
else:
|
||||
normal = normal.normalize(width / 2)
|
||||
self._stations.append(CurveStation(point + normal, point - normal))
|
||||
|
||||
def faces(self) -> Iterable[Face]:
|
||||
"""Yields all faces as 4-tuples of :class:`~ezdxf.math.Vec2` objects."""
|
||||
count = len(self._stations)
|
||||
if count < 2: # Two or more stations required to create faces
|
||||
return
|
||||
|
||||
vtx0 = None
|
||||
vtx1 = None
|
||||
for vtx2, vtx3 in self._stations:
|
||||
if vtx0 is None:
|
||||
vtx0 = vtx3
|
||||
vtx1 = vtx2
|
||||
continue
|
||||
yield vtx0, vtx1, vtx2, vtx3
|
||||
vtx0 = vtx3
|
||||
vtx1 = vtx2
|
||||
|
||||
|
||||
class TraceBuilder(Sequence):
|
||||
"""Sequence of 2D banded lines like polylines with start- and end width or
|
||||
curves with start- and end width.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Accepts 3D input, but z-axis is ignored. The :class:`TraceBuilder` is a
|
||||
2D only object and uses only the :ref:`OCS` coordinates!
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._traces: list[AbstractTrace] = []
|
||||
self.abs_tol = 1e-12
|
||||
|
||||
def __len__(self):
|
||||
return len(self._traces)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._traces[item]
|
||||
|
||||
def append(self, trace: AbstractTrace) -> None:
|
||||
"""Append a new trace."""
|
||||
self._traces.append(trace)
|
||||
|
||||
def faces(self) -> Iterable[Face]:
|
||||
"""Yields all faces as 4-tuples of :class:`~ezdxf.math.Vec2` objects
|
||||
in :ref:`OCS`.
|
||||
"""
|
||||
for trace in self._traces:
|
||||
yield from trace.faces()
|
||||
|
||||
def faces_wcs(self, ocs: OCS, elevation: float) -> Iterable[Sequence[Vec3]]:
|
||||
"""Yields all faces as 4-tuples of :class:`~ezdxf.math.Vec3` objects
|
||||
in :ref:`WCS`.
|
||||
"""
|
||||
for face in self.faces():
|
||||
yield tuple(ocs.points_to_wcs(Vec3(v.x, v.y, elevation) for v in face))
|
||||
|
||||
def polygons(self) -> Iterable[Polygon]:
|
||||
"""Yields for each sub-trace a single polygon as sequence of
|
||||
:class:`~ezdxf.math.Vec2` objects in :ref:`OCS`.
|
||||
"""
|
||||
for trace in self._traces:
|
||||
yield trace.polygon()
|
||||
|
||||
def polygons_wcs(self, ocs: OCS, elevation: float) -> Iterable[Sequence[Vec3]]:
|
||||
"""Yields for each sub-trace a single polygon as sequence of
|
||||
:class:`~ezdxf.math.Vec3` objects in :ref:`WCS`.
|
||||
"""
|
||||
for trace in self._traces:
|
||||
yield tuple(
|
||||
ocs.points_to_wcs(Vec3(v.x, v.y, elevation) for v in trace.polygon())
|
||||
)
|
||||
|
||||
def virtual_entities(
|
||||
self, dxftype="TRACE", dxfattribs=None, doc: Optional[Drawing] = None
|
||||
) -> Iterable[Quadrilateral]:
|
||||
"""Yields faces as SOLID, TRACE or 3DFACE entities with DXF attributes
|
||||
given in `dxfattribs`.
|
||||
|
||||
If a document is given, the doc attribute of the new entities will be
|
||||
set and the new entities will be automatically added to the entity
|
||||
database of that document.
|
||||
|
||||
.. note::
|
||||
|
||||
The :class:`TraceBuilder` is a 2D only object and uses only the
|
||||
:ref:`OCS` coordinates!
|
||||
|
||||
Args:
|
||||
dxftype: DXF type as string, "SOLID", "TRACE" or "3DFACE"
|
||||
dxfattribs: DXF attributes for SOLID, TRACE or 3DFACE entities
|
||||
doc: associated document
|
||||
|
||||
"""
|
||||
for trace in self._traces:
|
||||
yield from trace.virtual_entities(dxftype, dxfattribs, doc)
|
||||
|
||||
def close(self):
|
||||
"""Close multi traces by merging first and last trace, if linear traces."""
|
||||
traces = self._traces
|
||||
if len(traces) < 2:
|
||||
return
|
||||
if isinstance(traces[0], LinearTrace) and isinstance(traces[-1], LinearTrace):
|
||||
first = cast(LinearTrace, traces.pop(0))
|
||||
last = cast(LinearTrace, traces[-1])
|
||||
for point, start_width, end_width in first:
|
||||
last.add_station(point, start_width, end_width)
|
||||
|
||||
@classmethod
|
||||
def from_polyline(cls, polyline: DXFGraphic, segments: int = 64) -> TraceBuilder:
|
||||
"""
|
||||
Create a complete trace from a LWPOLYLINE or a 2D POLYLINE entity, the
|
||||
trace consist of multiple sub-traces if :term:`bulge` values are
|
||||
present. Uses only the :ref:`OCS` coordinates!
|
||||
|
||||
Args:
|
||||
polyline: :class:`~ezdxf.entities.LWPolyline` or 2D
|
||||
:class:`~ezdxf.entities.Polyline`
|
||||
segments: count of segments for bulge approximation, given count is
|
||||
for a full circle, partial arcs have proportional less segments,
|
||||
but at least 3
|
||||
|
||||
"""
|
||||
dxftype = polyline.dxftype()
|
||||
if dxftype == "LWPOLYLINE":
|
||||
polyline = cast("LWPolyline", polyline)
|
||||
const_width = polyline.dxf.const_width
|
||||
points = []
|
||||
for x, y, start_width, end_width, bulge in polyline.lwpoints:
|
||||
location = Vec2(x, y)
|
||||
if const_width:
|
||||
# This is AutoCAD behavior, BricsCAD uses const width
|
||||
# only for missing width values.
|
||||
start_width = const_width
|
||||
end_width = const_width
|
||||
points.append((location, start_width, end_width, bulge))
|
||||
closed = polyline.closed
|
||||
elif dxftype == "POLYLINE":
|
||||
polyline = cast("Polyline", polyline)
|
||||
if not polyline.is_2d_polyline:
|
||||
raise TypeError("2D POLYLINE required")
|
||||
closed = polyline.is_closed
|
||||
default_start_width = polyline.dxf.default_start_width
|
||||
default_end_width = polyline.dxf.default_end_width
|
||||
points = []
|
||||
for vertex in polyline.vertices:
|
||||
location = Vec2(vertex.dxf.location)
|
||||
if vertex.dxf.hasattr("start_width"):
|
||||
start_width = vertex.dxf.start_width
|
||||
else:
|
||||
start_width = default_start_width
|
||||
if vertex.dxf.hasattr("end_width"):
|
||||
end_width = vertex.dxf.end_width
|
||||
else:
|
||||
end_width = default_end_width
|
||||
bulge = vertex.dxf.bulge
|
||||
points.append((location, start_width, end_width, bulge))
|
||||
else:
|
||||
raise TypeError(f"Invalid DXF type {dxftype}")
|
||||
|
||||
if closed and not points[0][0].isclose(points[-1][0]):
|
||||
# close polyline explicit
|
||||
points.append(points[0])
|
||||
|
||||
trace = cls()
|
||||
store_bulge = None
|
||||
store_start_width = None
|
||||
store_end_width = None
|
||||
store_point = None
|
||||
|
||||
linear_trace = LinearTrace()
|
||||
for point, start_width, end_width, bulge in points:
|
||||
if store_bulge:
|
||||
center, start_angle, end_angle, radius = bulge_to_arc(
|
||||
store_point, point, store_bulge
|
||||
)
|
||||
if radius > 0:
|
||||
arc = ConstructionArc(
|
||||
center,
|
||||
radius,
|
||||
math.degrees(start_angle),
|
||||
math.degrees(end_angle),
|
||||
is_counter_clockwise=True,
|
||||
)
|
||||
if arc.start_point.isclose(point):
|
||||
sw = store_end_width
|
||||
ew = store_start_width
|
||||
else:
|
||||
ew = store_end_width
|
||||
sw = store_start_width
|
||||
trace.append(CurvedTrace.from_arc(arc, sw, ew, segments))
|
||||
store_bulge = None
|
||||
|
||||
if bulge != 0: # arc from prev_point to point
|
||||
if linear_trace.is_started:
|
||||
linear_trace.add_station(point, start_width, end_width)
|
||||
trace.append(linear_trace)
|
||||
linear_trace = LinearTrace()
|
||||
store_bulge = bulge
|
||||
store_start_width = start_width
|
||||
store_end_width = end_width
|
||||
store_point = point
|
||||
continue
|
||||
|
||||
linear_trace.add_station(point, start_width, end_width)
|
||||
if linear_trace.is_started:
|
||||
trace.append(linear_trace)
|
||||
|
||||
if closed and len(trace) > 1:
|
||||
# This is required for traces with multiple paths to create the correct
|
||||
# miter at the closing point. (only linear to linear trace).
|
||||
trace.close()
|
||||
return trace
|
||||
Reference in New Issue
Block a user