-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.py
More file actions
124 lines (100 loc) · 4.27 KB
/
Copy pathutils.py
File metadata and controls
124 lines (100 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Geometry and text helpers shared by the plan generators."""
import re
from bs4 import BeautifulSoup
from ezdxf.tools.text import MTextEditor
def polygon_orientation(coords) -> str:
"""Return 'CW' or 'CCW' for a polygon given as [(x1, y1), (x2, y2), ...]."""
area = 0
for i in range(len(coords)):
x1, y1 = coords[i]
x2, y2 = coords[(i + 1) % len(coords)]
area += (x2 - x1) * (y2 + y1)
return "CW" if area > 0 else "CCW"
def line_normals(p1, p2, orientation: str = "CCW"):
"""Return the (inside, outside) normal vectors of a polygon edge."""
dx, dy = p2[0] - p1[0], p2[1] - p1[1]
if orientation == "CCW": # inside = left normal
inside = (-dy, dx)
outside = (dy, -dx)
else: # CW polygon
inside = (dy, -dx)
outside = (-dy, dx)
return inside, outside
def line_direction(angle: float) -> str:
"""Reading direction of a line drawn at ``angle`` degrees (-180 to 180)."""
if -90 <= angle <= 90:
return "left → right"
return "right → left"
# Label rotations at or past this angle flip by 180 degrees so they stay
# readable. 110 (not 90) is the drafting convention (e.g. Civil3D's
# readability bias): near-vertical lines on either side of 90 degrees all
# read bottom-to-top instead of flipping direction exactly at vertical.
READABILITY_BIAS = 110.0
def readable_angle(angle: float, bias: float = READABILITY_BIAS) -> float:
"""Return ``angle`` or its reciprocal so label text reads 'uphill'.
The result lies in [bias - 180, bias): left-to-right for horizontal-ish
lines and bottom-to-top for vertical-ish ones, with the flip boundary
``bias - 90`` degrees past vertical so lines that straddle 90 degrees
do not read in opposite directions.
"""
angle %= 360.0
if bias <= angle < bias + 180.0:
angle -= 180.0
elif angle >= bias + 180.0:
angle -= 360.0
return angle
def format_number(num, mode: str = "tenth") -> str:
"""Zero-pad a number to 2 ('tenth') or 3 ('hundredth') digits."""
if mode == "tenth":
return f"{num:02d}" if isinstance(num, int) else str(num)
if mode == "hundredth":
return f"{num:03d}" if isinstance(num, int) else str(num)
raise ValueError("mode must be either 'tenth' or 'hundredth'")
def html_to_mtext(html_text: str, font: str = "Times New Roman") -> str:
"""Convert a small subset of HTML (b/i/u/br/p) to DXF MText markup.
MText has no standalone bold/italic toggles; both are expressed through
font-change codes (``\\fFamily|b1|i0;``), so the target font family must
be known here.
"""
if not html_text:
return ""
soup = BeautifulSoup(html_text.replace("\n", " "), "html.parser")
editor = MTextEditor()
# Track nested bold/italic state so closing a tag restores the outer style.
style = {"bold": 0, "italic": 0}
def apply_font():
editor.append(rf"\f{font}|b{min(style['bold'], 1)}|i{min(style['italic'], 1)};")
def with_style(key: str, child):
style[key] += 1
apply_font()
parse_tag(child)
style[key] -= 1
apply_font()
def parse_tag(tag):
for child in tag.children:
if isinstance(child, str): # plain text
text = re.sub(r"\s+", " ", child)
if text.strip():
editor.append(text)
elif child.name in ("b", "strong"):
with_style("bold", child)
elif child.name in ("i", "em"):
with_style("italic", child)
elif child.name == "u":
editor.append(MTextEditor.UNDERLINE_START)
parse_tag(child)
editor.append(MTextEditor.UNDERLINE_STOP)
elif child.name == "br":
editor.append(MTextEditor.NEW_LINE)
elif child.name == "p":
# paragraphs start on a new line, except the very first element
prev = child.previous_sibling
while prev is not None and str(prev).strip() == "":
prev = prev.previous_sibling
if prev is not None:
editor.append(MTextEditor.NEW_LINE)
parse_tag(child)
else:
parse_tag(child)
parse_tag(soup)
return str(editor).replace("\n", "")