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

191 lines
5.8 KiB
Python

# Copyright (c) 2015-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Sequence, Tuple, Optional
from typing_extensions import TypeAlias
from ezdxf.math import Vec2
from ._iso_pattern import ISO_PATTERN
# Predefined hatch pattern prior to ezdxf v0.11 were scaled for imperial units,
# and were too small for ISO units by a factor of 1/25.4, to replicate this
# pattern scaling use load(measurement=0).
__all__ = [
"load",
"scale_pattern",
"scale_all",
"parse",
"ISO_PATTERN",
"IMPERIAL_PATTERN",
"HatchPatternLineType",
"HatchPatternType",
"PatternAnalyser",
]
IMPERIAL_SCALE_FACTOR = 1.0 / 25.4
HatchPatternLineType: TypeAlias = Tuple[
float, Sequence[float], Sequence[float], Sequence[float]
]
HatchPatternType: TypeAlias = Sequence[HatchPatternLineType]
def load(measurement: int = 1, factor: Optional[float] = None):
"""Load hatch pattern definition, default scaling is like the iso.pat of
BricsCAD, set `measurement` to 0 to use the imperial (US) scaled pattern,
which has a scaling factor of 1/25.4 = ~0.03937.
Args:
measurement: like the $MEASUREMENT header variable, 0 to user imperial
scaled pattern, 1 to use ISO scaled pattern.
factor: hatch pattern scaling factor, overrides `measurement`
Returns: hatch pattern dict of scaled pattern
"""
if factor is None:
factor = 1.0 if measurement == 1 else IMPERIAL_SCALE_FACTOR
pattern = ISO_PATTERN
if factor != 1.0:
pattern = scale_all(pattern, factor=factor)
return pattern
def scale_pattern(
pattern: HatchPatternType, factor: float = 1, angle: float = 0
) -> HatchPatternType:
ndigits = 10
def _scale(iterable) -> Sequence[float]:
return [round(i * factor, ndigits) for i in iterable]
def _scale_line(line) -> HatchPatternLineType:
angle0, base_point, offset, dash_length_items = line
if angle:
base_point = Vec2(base_point).rotate_deg(angle)
offset = Vec2(offset).rotate_deg(angle)
angle0 = (angle0 + angle) % 360.0
# noinspection PyTypeChecker
return [ # type: ignore
round(angle0, ndigits),
tuple(_scale(base_point)),
tuple(_scale(offset)),
_scale(dash_length_items),
]
return [_scale_line(line) for line in pattern]
def scale_all(pattern: dict, factor: float = 1, angle: float = 0):
return {name: scale_pattern(p, factor, angle) for name, p in pattern.items()}
def parse(pattern: str) -> dict:
try:
comp = PatternFileCompiler(pattern)
return comp.compile_pattern()
except Exception:
raise ValueError("Incompatible pattern definition.")
def _tokenize_pattern_line(line: str) -> list:
return line.split(",", maxsplit=1 if line.startswith("*") else -1)
class PatternFileCompiler:
def __init__(self, content: str):
self._lines = [
_tokenize_pattern_line(line)
for line in (line.strip() for line in content.split("\n"))
if line and line[0] != ";"
]
def _parse_pattern(self):
pattern = []
for line in self._lines:
if line[0].startswith("*"):
if pattern:
yield pattern
pattern = [[line[0][1:], line[1]]] # name, description
else:
pattern.append([float(e) for e in line]) # list[floats]
if pattern:
yield pattern
def compile_pattern(self, ndigits: int = 10) -> dict:
pattern = dict()
for p in self._parse_pattern():
pat = []
for line in p[1:]:
# offset before rounding:
offset = Vec2(line[3], line[4])
# round all values:
line = [round(e, ndigits) for e in line]
pat_line = []
angle = line[0]
pat_line.append(angle)
# base point:
pat_line.append((line[1], line[2]))
# rotate offset:
offset = offset.rotate_deg(angle)
pat_line.append((round(offset.x, ndigits), round(offset.y, ndigits)))
# line dash pattern
pat_line.append(line[5:])
pat.append(pat_line)
pattern[p[0][0]] = pat
return pattern
IMPERIAL_PATTERN = load(measurement=0)
def is_solid(pattern: Sequence[float]) -> bool:
return not bool(len(pattern))
def round_angle_15_deg(angle: float) -> int:
return round((angle % 180) / 15) * 15
class PatternAnalyser:
def __init__(self, pattern: HatchPatternType):
# List of 2-tuples: (angle, is solid line pattern)
# angle is rounded to a multiple of 15° in the range [0, 180)
self._lines: list[tuple[int, bool]] = [
(round_angle_15_deg(angle), is_solid(line_pattern))
for angle, _, _, line_pattern in pattern
]
def has_angle(self, angle: int) -> bool:
return any(angle_ == angle for angle_, _ in self._lines)
def all_angles(self, angle: int) -> bool:
return all(angle_ == angle for angle_, _ in self._lines)
def has_line(self, angle: int, solid: bool) -> bool:
return any(
angle_ == angle and solid_ == solid for angle_, solid_ in self._lines
)
def all_lines(self, angle: int, solid: bool) -> bool:
return all(
angle_ == angle and solid_ == solid for angle_, solid_ in self._lines
)
def has_solid_line(self) -> bool:
return any(solid for _, solid in self._lines)
def has_dashed_line(self) -> bool:
return any(not solid for _, solid in self._lines)
def all_solid_lines(self) -> bool:
return all(solid for _, solid in self._lines)
def all_dashed_lines(self) -> bool:
return all(not solid for _, solid in self._lines)