359 lines
12 KiB
Python
359 lines
12 KiB
Python
# Copyright (c) 2021-2023, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import Union
|
|
import enum
|
|
|
|
from ezdxf.entities import Text, Attrib, Hatch, DXFGraphic
|
|
from ezdxf.lldxf import const
|
|
from ezdxf.enums import TextEntityAlignment, MAP_TEXT_ENUM_TO_ALIGN_FLAGS
|
|
from ezdxf.math import Matrix44, BoundingBox
|
|
from ezdxf import path
|
|
from ezdxf.path import Path
|
|
from ezdxf.fonts import fonts
|
|
from ezdxf.query import EntityQuery
|
|
|
|
__all__ = [
|
|
"make_path_from_str",
|
|
"make_paths_from_str",
|
|
"make_hatches_from_str",
|
|
"make_path_from_entity",
|
|
"make_paths_from_entity",
|
|
"make_hatches_from_entity",
|
|
"virtual_entities",
|
|
"explode",
|
|
"Kind",
|
|
]
|
|
|
|
AnyText = Union[Text, Attrib]
|
|
VALID_TYPES = ("TEXT", "ATTRIB")
|
|
|
|
|
|
def make_path_from_str(
|
|
s: str,
|
|
font: fonts.FontFace,
|
|
size: float = 1.0,
|
|
align=TextEntityAlignment.LEFT,
|
|
length: float = 0,
|
|
m: Matrix44 = None,
|
|
) -> Path:
|
|
"""Convert a single line string `s` into a :term:`Multi-Path` object.
|
|
The text `size` is the height of the uppercase letter "X" (cap height).
|
|
The paths are aligned about the insertion point at (0, 0).
|
|
BASELINE means the bottom of the letter "X".
|
|
|
|
Args:
|
|
s: text to convert
|
|
font: font face definition as :class:`~ezdxf.tools.fonts.FontFace` object
|
|
size: text size (cap height) in drawing units
|
|
align: alignment as :class:`ezdxf.enums.TextEntityAlignment`,
|
|
default is :attr:`LEFT`
|
|
length: target length for the :attr:`ALIGNED` and :attr:`FIT` alignments
|
|
m: transformation :class:`~ezdxf.math.Matrix44`
|
|
|
|
"""
|
|
if len(s) == 0:
|
|
return Path()
|
|
abstract_font = get_font(font)
|
|
# scale font rendering units to drawing units:
|
|
p = _str_to_path(s, abstract_font, size)
|
|
bbox = path.bbox([p], fast=True)
|
|
|
|
# Text is rendered in drawing units,
|
|
# therefore do alignment in drawing units:
|
|
draw_units_fm = abstract_font.measurements.scale_from_baseline(size)
|
|
matrix = alignment_transformation(draw_units_fm, bbox, align, length)
|
|
if m is not None:
|
|
matrix *= m
|
|
return p.transform(matrix)
|
|
|
|
|
|
def make_paths_from_str(
|
|
s: str,
|
|
font: fonts.FontFace,
|
|
size: float = 1.0,
|
|
align=TextEntityAlignment.LEFT,
|
|
length: float = 0,
|
|
m: Matrix44 = None,
|
|
) -> list[Path]:
|
|
"""Convert a single line string `s` into a list of
|
|
:class:`~ezdxf.path.Path` objects. All paths are returned as a list of
|
|
:term:`Single-Path` objects.
|
|
The text `size` is the height of the uppercase letter "X" (cap height).
|
|
The paths are aligned about the insertion point at (0, 0).
|
|
BASELINE means the bottom of the letter "X".
|
|
|
|
Args:
|
|
s: text to convert
|
|
font: font face definition as :class:`~ezdxf.tools.fonts.FontFace` object
|
|
size: text size (cap height) in drawing units
|
|
align: alignment as :class:`ezdxf.enums.TextEntityAlignment`,
|
|
default is :attr:`LEFT`
|
|
length: target length for the :attr:`ALIGNED` and :attr:`FIT` alignments
|
|
m: transformation :class:`~ezdxf.math.Matrix44`
|
|
|
|
"""
|
|
if len(s) == 0:
|
|
return []
|
|
p = make_path_from_str(s, font, size, align, length, m)
|
|
return list(p.sub_paths())
|
|
|
|
|
|
def get_font(font: fonts.FontFace) -> fonts.AbstractFont:
|
|
font_name = fonts.font_manager.find_font_name(font)
|
|
return fonts.make_font(font_name, cap_height=1.0)
|
|
|
|
|
|
def _str_to_path(s: str, render_engine: fonts.AbstractFont, size: float = 1.0) -> Path:
|
|
return render_engine.text_path_ex(s, cap_height=size).to_path()
|
|
|
|
|
|
def alignment_transformation(
|
|
fm: fonts.FontMeasurements,
|
|
bbox: BoundingBox,
|
|
align: TextEntityAlignment,
|
|
length: float,
|
|
) -> Matrix44:
|
|
"""Returns the alignment transformation matrix to transform a basic
|
|
text path at location (0, 0) and alignment :attr:`LEFT` into the final text
|
|
path of the given alignment.
|
|
For the alignments :attr:`FIT` and :attr:`ALIGNED` defines the argument
|
|
`length` the total length of the final text path. The given bounding box
|
|
defines the rendering borders of the basic text path.
|
|
|
|
"""
|
|
halign, valign = MAP_TEXT_ENUM_TO_ALIGN_FLAGS[align]
|
|
matrix = basic_alignment_transformation(fm, bbox, halign, valign)
|
|
|
|
stretch_x = 1.0
|
|
stretch_y = 1.0
|
|
if align == TextEntityAlignment.ALIGNED:
|
|
stretch_x = length / bbox.size.x
|
|
stretch_y = stretch_x
|
|
elif align == TextEntityAlignment.FIT:
|
|
stretch_x = length / bbox.size.x
|
|
if stretch_x != 1.0:
|
|
matrix *= Matrix44.scale(stretch_x, stretch_y, 1.0)
|
|
return matrix
|
|
|
|
|
|
def basic_alignment_transformation(
|
|
fm: fonts.FontMeasurements, bbox: BoundingBox, halign: int, valign: int
|
|
) -> Matrix44:
|
|
if halign == const.LEFT:
|
|
shift_x = 0.0
|
|
elif halign == const.RIGHT:
|
|
assert bbox.extmax is not None, "invalid empty bounding box"
|
|
shift_x = -bbox.extmax.x
|
|
elif halign == const.CENTER or halign > 2: # ALIGNED, MIDDLE, FIT
|
|
assert bbox.center is not None, "invalid empty bounding box"
|
|
shift_x = -bbox.center.x
|
|
else:
|
|
raise ValueError(f"invalid halign argument: {halign}")
|
|
cap_height = fm.cap_height
|
|
descender_height = fm.descender_height
|
|
if valign == const.BASELINE:
|
|
shift_y = 0.0
|
|
elif valign == const.TOP:
|
|
shift_y = -cap_height
|
|
elif valign == const.MIDDLE:
|
|
shift_y = -cap_height / 2
|
|
elif valign == const.BOTTOM:
|
|
shift_y = descender_height
|
|
else:
|
|
raise ValueError(f"invalid valign argument: {valign}")
|
|
if halign == 4: # MIDDLE
|
|
shift_y = -cap_height + fm.total_height / 2.0
|
|
return Matrix44.translate(shift_x, shift_y, 0)
|
|
|
|
|
|
def make_hatches_from_str(
|
|
s: str,
|
|
font: fonts.FontFace,
|
|
size: float = 1.0,
|
|
align=TextEntityAlignment.LEFT,
|
|
length: float = 0,
|
|
dxfattribs=None,
|
|
m: Matrix44 = None,
|
|
) -> list[Hatch]:
|
|
"""Convert a single line string `s` into a list of virtual
|
|
:class:`~ezdxf.entities.Hatch` entities.
|
|
The text `size` is the height of the uppercase letter "X" (cap height).
|
|
The paths are aligned about the insertion point at (0, 0).
|
|
The HATCH entities are aligned to this insertion point. BASELINE means the
|
|
bottom of the letter "X".
|
|
|
|
.. important::
|
|
|
|
Returns an empty list for .shx, .shp and .lff fonts a.k.a. stroke fonts.
|
|
|
|
Args:
|
|
s: text to convert
|
|
font: font face definition as :class:`~ezdxf.tools.fonts.FontFace` object
|
|
size: text size (cap height) in drawing units
|
|
align: alignment as :class:`ezdxf.enums.TextEntityAlignment`,
|
|
default is :attr:`LEFT`
|
|
length: target length for the :attr:`ALIGNED` and :attr:`FIT` alignments
|
|
dxfattribs: additional DXF attributes
|
|
m: transformation :class:`~ezdxf.math.Matrix44`
|
|
|
|
"""
|
|
font_ = get_font(font)
|
|
if font_.font_render_type is fonts.FontRenderType.STROKE:
|
|
return []
|
|
|
|
# HATCH is an OCS entity, transforming just the polyline paths
|
|
# is not correct! The Hatch has to be created in the xy-plane!
|
|
paths = make_paths_from_str(s, font, size, align, length)
|
|
dxfattribs = dict(dxfattribs or {})
|
|
dxfattribs.setdefault("solid_fill", 1)
|
|
dxfattribs.setdefault("pattern_name", "SOLID")
|
|
dxfattribs.setdefault("color", const.BYLAYER)
|
|
hatches = path.to_hatches(paths, edge_path=True, dxfattribs=dxfattribs)
|
|
if m is not None:
|
|
# Transform HATCH entities as a unit:
|
|
return [hatch.transform(m) for hatch in hatches] # type: ignore
|
|
else:
|
|
return list(hatches)
|
|
|
|
|
|
def check_entity_type(entity):
|
|
if entity is None:
|
|
raise TypeError("entity is None")
|
|
elif not entity.dxftype() in VALID_TYPES:
|
|
raise TypeError(f"unsupported entity type: {entity.dxftype()}")
|
|
|
|
|
|
def make_path_from_entity(entity: AnyText) -> Path:
|
|
"""Convert text content from DXF entities TEXT and ATTRIB into a
|
|
:term:`Multi-Path` object.
|
|
The paths are located at the location of the source entity.
|
|
"""
|
|
|
|
check_entity_type(entity)
|
|
text = entity.plain_text()
|
|
p = make_path_from_str(
|
|
text,
|
|
fonts.get_font_face(entity.font_name()),
|
|
size=entity.dxf.height, # cap height in drawing units
|
|
align=entity.get_align_enum(),
|
|
length=entity.fit_length(),
|
|
)
|
|
m = entity.wcs_transformation_matrix()
|
|
return p.transform(m)
|
|
|
|
|
|
def make_paths_from_entity(entity: AnyText) -> list[Path]:
|
|
"""Convert text content from DXF entities TEXT and ATTRIB into a
|
|
list of :class:`~ezdxf.path.Path` objects. All paths are returned as a
|
|
list of :term:`Single-Path` objects.
|
|
The paths are located at the location of the source entity.
|
|
|
|
"""
|
|
return list(make_path_from_entity(entity).sub_paths())
|
|
|
|
|
|
def make_hatches_from_entity(entity: AnyText) -> list[Hatch]:
|
|
"""Convert text content from DXF entities TEXT and ATTRIB into a
|
|
list of virtual :class:`~ezdxf.entities.Hatch` entities.
|
|
The hatches are placed at the same location as the source entity and have
|
|
the same DXF attributes as the source entity.
|
|
|
|
"""
|
|
check_entity_type(entity)
|
|
extrusion = entity.dxf.extrusion
|
|
attribs = entity.graphic_properties()
|
|
paths = make_paths_from_entity(entity)
|
|
return list(
|
|
path.to_hatches(
|
|
paths,
|
|
edge_path=True,
|
|
extrusion=extrusion,
|
|
dxfattribs=attribs,
|
|
)
|
|
)
|
|
|
|
|
|
@enum.unique
|
|
class Kind(enum.IntEnum):
|
|
"""The :class:`Kind` enum defines the DXF types to create as bit flags,
|
|
e.g. 1+2 to get HATCHES as filling and SPLINES and POLYLINES as outline:
|
|
|
|
=== =========== ==============================
|
|
Int Enum Description
|
|
=== =========== ==============================
|
|
1 HATCHES :class:`~ezdxf.entities.Hatch` entities as filling
|
|
2 SPLINES :class:`~ezdxf.entities.Spline` and 3D :class:`~ezdxf.entities.Polyline`
|
|
entities as outline
|
|
4 LWPOLYLINES :class:`~ezdxf.entities.LWPolyline` entities as approximated
|
|
(flattened) outline
|
|
=== =========== ==============================
|
|
|
|
"""
|
|
|
|
HATCHES = 1
|
|
SPLINES = 2
|
|
LWPOLYLINES = 4
|
|
|
|
|
|
def virtual_entities(entity: AnyText, kind: int = Kind.HATCHES) -> EntityQuery:
|
|
"""Convert the text content of DXF entities TEXT and ATTRIB into virtual
|
|
SPLINE and 3D POLYLINE entities or approximated LWPOLYLINE entities
|
|
as outlines, or as HATCH entities as fillings.
|
|
|
|
Returns the virtual DXF entities as an :class:`~ezdxf.query.EntityQuery`
|
|
object.
|
|
|
|
Args:
|
|
entity: TEXT or ATTRIB entity
|
|
kind: kind of entities to create as bit flags, see enum :class:`Kind`
|
|
|
|
"""
|
|
check_entity_type(entity)
|
|
extrusion = entity.dxf.extrusion
|
|
attribs = entity.graphic_properties()
|
|
entities: list[DXFGraphic] = []
|
|
|
|
if kind & Kind.HATCHES:
|
|
entities.extend(make_hatches_from_entity(entity))
|
|
if kind & (Kind.SPLINES + Kind.LWPOLYLINES):
|
|
paths = make_paths_from_entity(entity)
|
|
if kind & Kind.SPLINES:
|
|
entities.extend(path.to_splines_and_polylines(paths, dxfattribs=attribs))
|
|
if kind & Kind.LWPOLYLINES:
|
|
entities.extend(
|
|
path.to_lwpolylines(paths, extrusion=extrusion, dxfattribs=attribs)
|
|
)
|
|
|
|
return EntityQuery(entities)
|
|
|
|
|
|
def explode(entity: AnyText, kind: int = Kind.HATCHES, target=None) -> EntityQuery:
|
|
"""Explode the text `entity` into virtual entities,
|
|
see :func:`virtual_entities`. The source entity will be destroyed.
|
|
|
|
The target layout is given by the `target` argument, if `target` is ``None``,
|
|
the target layout is the source layout of the text entity.
|
|
|
|
Returns the created DXF entities as an :class:`~ezdxf.query.EntityQuery`
|
|
object.
|
|
|
|
Args:
|
|
entity: TEXT or ATTRIB entity to explode
|
|
kind: kind of entities to create as bit flags, see enum :class:`Kind`
|
|
target: target layout for new created DXF entities, ``None`` for the
|
|
same layout as the source entity.
|
|
|
|
"""
|
|
entities = virtual_entities(entity, kind)
|
|
|
|
# Explicit check for None is required, because empty layouts are also False
|
|
if target is None:
|
|
target = entity.get_layout()
|
|
entity.destroy()
|
|
|
|
if target is not None:
|
|
for e in entities:
|
|
target.add_entity(e)
|
|
return EntityQuery(entities)
|