harnice 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harnice/__init__.py +0 -0
- harnice/__main__.py +4 -0
- harnice/cli.py +234 -0
- harnice/fileio.py +295 -0
- harnice/gui/launcher.py +426 -0
- harnice/lists/channel_map.py +182 -0
- harnice/lists/circuits_list.py +302 -0
- harnice/lists/disconnect_map.py +237 -0
- harnice/lists/formboard_graph.py +63 -0
- harnice/lists/instances_list.py +280 -0
- harnice/lists/library_history.py +40 -0
- harnice/lists/manifest.py +93 -0
- harnice/lists/post_harness_instances_list.py +66 -0
- harnice/lists/rev_history.py +325 -0
- harnice/lists/signals_list.py +135 -0
- harnice/products/__init__.py +1 -0
- harnice/products/cable.py +152 -0
- harnice/products/chtype.py +80 -0
- harnice/products/device.py +844 -0
- harnice/products/disconnect.py +225 -0
- harnice/products/flagnote.py +139 -0
- harnice/products/harness.py +522 -0
- harnice/products/macro.py +10 -0
- harnice/products/part.py +640 -0
- harnice/products/system.py +125 -0
- harnice/products/tblock.py +270 -0
- harnice/state.py +57 -0
- harnice/utils/appearance.py +51 -0
- harnice/utils/circuit_utils.py +326 -0
- harnice/utils/feature_tree_utils.py +183 -0
- harnice/utils/formboard_utils.py +973 -0
- harnice/utils/library_utils.py +333 -0
- harnice/utils/note_utils.py +417 -0
- harnice/utils/svg_utils.py +819 -0
- harnice/utils/system_utils.py +563 -0
- harnice-0.3.0.dist-info/METADATA +32 -0
- harnice-0.3.0.dist-info/RECORD +41 -0
- harnice-0.3.0.dist-info/WHEEL +5 -0
- harnice-0.3.0.dist-info/entry_points.txt +3 -0
- harnice-0.3.0.dist-info/licenses/LICENSE +19 -0
- harnice-0.3.0.dist-info/top_level.txt +1 -0
harnice/products/part.py
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import random
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
7
|
+
from harnice import fileio, state
|
|
8
|
+
from harnice.utils import svg_utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
default_desc = "COTS COMPONENT, SIZE, COLOR, etc."
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def file_structure():
|
|
15
|
+
return {
|
|
16
|
+
f"{state.partnumber('pn-rev')}-drawing.svg": "drawing",
|
|
17
|
+
f"{state.partnumber('pn-rev')}-drawing.png": "drawing png",
|
|
18
|
+
f"{state.partnumber('pn-rev')}-attributes.json": "attributes",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_structure():
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render():
|
|
27
|
+
# === ATTRIBUTES JSON DEFAULTS ===
|
|
28
|
+
default_attributes = {
|
|
29
|
+
"tools": [],
|
|
30
|
+
"build_notes": [],
|
|
31
|
+
"csys_children": {
|
|
32
|
+
"accessory-1": {"x": 3, "y": 2, "angle": 0, "rotation": 0},
|
|
33
|
+
"accessory-2": {"x": 2, "y": 3, "angle": 0, "rotation": 0},
|
|
34
|
+
"flagnote-1": {"angle": 0, "distance": 2, "rotation": 0},
|
|
35
|
+
"flagnote-leader-1": {"angle": 0, "distance": 1, "rotation": 0},
|
|
36
|
+
"flagnote-2": {"angle": 15, "distance": 2, "rotation": 0},
|
|
37
|
+
"flagnote-leader-2": {"angle": 15, "distance": 1, "rotation": 0},
|
|
38
|
+
"flagnote-3": {"angle": -15, "distance": 2, "rotation": 0},
|
|
39
|
+
"flagnote-leader-3": {"angle": -15, "distance": 1, "rotation": 0},
|
|
40
|
+
"flagnote-4": {"angle": 30, "distance": 2, "rotation": 0},
|
|
41
|
+
"flagnote-leader-4": {"angle": 30, "distance": 1, "rotation": 0},
|
|
42
|
+
"flagnote-5": {"angle": -30, "distance": 2, "rotation": 0},
|
|
43
|
+
"flagnote-leader-5": {"angle": -30, "distance": 1, "rotation": 0},
|
|
44
|
+
"flagnote-6": {"angle": 45, "distance": 2, "rotation": 0},
|
|
45
|
+
"flagnote-leader-6": {"angle": 45, "distance": 1, "rotation": 0},
|
|
46
|
+
"flagnote-7": {"angle": -45, "distance": 2, "rotation": 0},
|
|
47
|
+
"flagnote-leader-7": {"angle": -45, "distance": 1, "rotation": 0},
|
|
48
|
+
"flagnote-8": {"angle": 60, "distance": 2, "rotation": 0},
|
|
49
|
+
"flagnote-leader-8": {"angle": 60, "distance": 1, "rotation": 0},
|
|
50
|
+
"flagnote-9": {"angle": -60, "distance": 2, "rotation": 0},
|
|
51
|
+
"flagnote-leader-9": {"angle": -60, "distance": 1, "rotation": 0},
|
|
52
|
+
"flagnote-10": {"angle": -75, "distance": 2, "rotation": 0},
|
|
53
|
+
"flagnote-leader-10": {"angle": -75, "distance": 1, "rotation": 0},
|
|
54
|
+
"flagnote-11": {"angle": 75, "distance": 2, "rotation": 0},
|
|
55
|
+
"flagnote-leader-11": {"angle": 75, "distance": 1, "rotation": 0},
|
|
56
|
+
"flagnote-12": {"angle": -90, "distance": 2, "rotation": 0},
|
|
57
|
+
"flagnote-leader-12": {"angle": -90, "distance": 1, "rotation": 0},
|
|
58
|
+
"flagnote-13": {"angle": 90, "distance": 2, "rotation": 0},
|
|
59
|
+
"flagnote-leader-13": {"angle": 90, "distance": 1, "rotation": 0},
|
|
60
|
+
"flagnote-14": {"angle": -105, "distance": 2, "rotation": 0},
|
|
61
|
+
"flagnote-leader-14": {"angle": -105, "distance": 1, "rotation": 0},
|
|
62
|
+
"flagnote-15": {"angle": 105, "distance": 2, "rotation": 0},
|
|
63
|
+
"flagnote-leader-15": {"angle": 105, "distance": 1, "rotation": 0},
|
|
64
|
+
"flagnote-16": {"angle": -120, "distance": 2, "rotation": 0},
|
|
65
|
+
"flagnote-leader-16": {"angle": -120, "distance": 1, "rotation": 0},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
attributes_path = fileio.path("attributes")
|
|
70
|
+
|
|
71
|
+
# Load or create attributes.json
|
|
72
|
+
if os.path.exists(attributes_path):
|
|
73
|
+
try:
|
|
74
|
+
with open(attributes_path, "r", encoding="utf-8") as f:
|
|
75
|
+
attrs = json.load(f)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"[WARNING] Could not load existing attributes.json: {e}")
|
|
78
|
+
attrs = default_attributes.copy()
|
|
79
|
+
else:
|
|
80
|
+
attrs = default_attributes.copy()
|
|
81
|
+
with open(attributes_path, "w", encoding="utf-8") as f:
|
|
82
|
+
json.dump(attrs, f, indent=4)
|
|
83
|
+
|
|
84
|
+
# === SVG SETUP ===
|
|
85
|
+
svg_path = fileio.path("drawing")
|
|
86
|
+
temp_svg_path = svg_path + ".tmp"
|
|
87
|
+
|
|
88
|
+
svg_width = 400
|
|
89
|
+
svg_height = 400
|
|
90
|
+
group_name = f"{state.partnumber('pn')}-drawing"
|
|
91
|
+
|
|
92
|
+
random_fill = "#{:06X}".format(random.randint(0, 0xFFFFFF))
|
|
93
|
+
fallback_rect = f' <rect x="0" y="-48" width="96" height="96" fill="{random_fill}" stroke="black" stroke-width="1"/>'
|
|
94
|
+
|
|
95
|
+
csys_children = attrs.get("csys_children", {})
|
|
96
|
+
|
|
97
|
+
lines = [
|
|
98
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
99
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{svg_width}" height="{svg_height}">',
|
|
100
|
+
f' <g id="{group_name}-contents-start">',
|
|
101
|
+
fallback_rect,
|
|
102
|
+
" </g>",
|
|
103
|
+
f' <g id="{group_name}-contents-end">',
|
|
104
|
+
" </g>",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
# === Render Output Csys Locations ===
|
|
108
|
+
lines.append(' <g id="output csys locations">')
|
|
109
|
+
|
|
110
|
+
arrow_len = 24
|
|
111
|
+
dot_radius = 4
|
|
112
|
+
arrow_size = 6
|
|
113
|
+
|
|
114
|
+
for csys_name, csys in csys_children.items():
|
|
115
|
+
try:
|
|
116
|
+
x = float(csys.get("x", 0)) * 96
|
|
117
|
+
y = float(csys.get("y", 0)) * 96
|
|
118
|
+
|
|
119
|
+
angle_deg = float(csys.get("angle", 0))
|
|
120
|
+
distance_in = float(csys.get("distance", 0))
|
|
121
|
+
angle_rad = math.radians(angle_deg)
|
|
122
|
+
dist_px = distance_in * 96
|
|
123
|
+
x += dist_px * math.cos(angle_rad)
|
|
124
|
+
y += dist_px * math.sin(angle_rad)
|
|
125
|
+
|
|
126
|
+
rotation_deg = float(csys.get("rotation", 0))
|
|
127
|
+
rotation_rad = math.radians(rotation_deg)
|
|
128
|
+
cos_r, sin_r = math.cos(rotation_rad), math.sin(rotation_rad)
|
|
129
|
+
|
|
130
|
+
dx_x, dy_x = arrow_len * cos_r, arrow_len * sin_r
|
|
131
|
+
dx_y, dy_y = -arrow_len * sin_r, arrow_len * cos_r
|
|
132
|
+
|
|
133
|
+
lines.append(f' <g id="{csys_name}">')
|
|
134
|
+
lines.append(
|
|
135
|
+
f' <circle cx="{x:.2f}" cy="{-y:.2f}" r="{dot_radius}" fill="black"/>'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def draw_arrow(x1, y1, dx, dy, color):
|
|
139
|
+
x2, y2 = x1 + dx, y1 + dy
|
|
140
|
+
lines.append(
|
|
141
|
+
f' <line x1="{x1:.2f}" y1="{-y1:.2f}" '
|
|
142
|
+
f'x2="{x2:.2f}" y2="{-y2:.2f}" stroke="{color}" stroke-width="2"/>'
|
|
143
|
+
)
|
|
144
|
+
length = math.hypot(dx, dy)
|
|
145
|
+
if length == 0:
|
|
146
|
+
return
|
|
147
|
+
ux, uy = dx / length, dy / length
|
|
148
|
+
px, py = -uy, ux
|
|
149
|
+
base_x = x2 - ux * arrow_size
|
|
150
|
+
base_y = y2 - uy * arrow_size
|
|
151
|
+
tip = (x2, y2)
|
|
152
|
+
left = (base_x + px * (arrow_size / 2), base_y + py * (arrow_size / 2))
|
|
153
|
+
right = (base_x - px * (arrow_size / 2), base_y - py * (arrow_size / 2))
|
|
154
|
+
lines.append(
|
|
155
|
+
f' <polygon points="{tip[0]:.2f},{-tip[1]:.2f} '
|
|
156
|
+
f'{left[0]:.2f},{-left[1]:.2f} {right[0]:.2f},{-right[1]:.2f}" fill="{color}"/>'
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
draw_arrow(x, y, dx_x, dy_x, "red")
|
|
160
|
+
draw_arrow(x, y, dx_y, dy_y, "green")
|
|
161
|
+
lines.append(" </g>")
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"[WARNING] Failed to render csys {csys_name}: {e}")
|
|
165
|
+
|
|
166
|
+
lines.append(" </g>")
|
|
167
|
+
lines.append("</svg>")
|
|
168
|
+
|
|
169
|
+
with open(temp_svg_path, "w", encoding="utf-8") as f:
|
|
170
|
+
f.write("\n".join(lines) + "\n")
|
|
171
|
+
|
|
172
|
+
if os.path.exists(svg_path):
|
|
173
|
+
try:
|
|
174
|
+
with open(svg_path, "r", encoding="utf-8") as f:
|
|
175
|
+
svg_text = f.read()
|
|
176
|
+
except Exception:
|
|
177
|
+
svg_text = ""
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
f"{group_name}-contents-start" not in svg_text
|
|
181
|
+
or f"{group_name}-contents-end" not in svg_text
|
|
182
|
+
):
|
|
183
|
+
svg_utils.add_entire_svg_file_contents_to_group(svg_path, group_name)
|
|
184
|
+
|
|
185
|
+
svg_utils.find_and_replace_svg_group(
|
|
186
|
+
source_svg_filepath=svg_path,
|
|
187
|
+
source_group_name=group_name,
|
|
188
|
+
destination_svg_filepath=temp_svg_path,
|
|
189
|
+
destination_group_name=group_name,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if os.path.exists(svg_path):
|
|
193
|
+
os.remove(svg_path)
|
|
194
|
+
os.rename(temp_svg_path, svg_path)
|
|
195
|
+
|
|
196
|
+
# ==================================================
|
|
197
|
+
# PNG generation (SVG = truth for graphics; JSON = truth for CSYS)
|
|
198
|
+
# ==================================================
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
# 1. Extract raw contents group from final SVG
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
with open(svg_path, "r", encoding="utf-8") as f:
|
|
204
|
+
svg_text = f.read()
|
|
205
|
+
|
|
206
|
+
start_tag = f'<g id="{group_name}-contents-start">'
|
|
207
|
+
end_tag = f'<g id="{group_name}-contents-end">'
|
|
208
|
+
|
|
209
|
+
start_idx = svg_text.find(start_tag)
|
|
210
|
+
end_idx = svg_text.find(end_tag)
|
|
211
|
+
|
|
212
|
+
if start_idx == -1 or end_idx == -1:
|
|
213
|
+
print(
|
|
214
|
+
"[WARNING] Could not find contents group in SVG — PNG will only draw csys."
|
|
215
|
+
)
|
|
216
|
+
inner_svg = ""
|
|
217
|
+
else:
|
|
218
|
+
inner_svg = svg_text[start_idx + len(start_tag) : end_idx]
|
|
219
|
+
|
|
220
|
+
# ======================================================
|
|
221
|
+
# 2. Utility functions + transform matrix code
|
|
222
|
+
# ======================================================
|
|
223
|
+
|
|
224
|
+
def get_attr(tag, name, default=None):
|
|
225
|
+
m = re.search(rf'{name}="([^"]+)"', tag)
|
|
226
|
+
return m.group(1) if m else default
|
|
227
|
+
|
|
228
|
+
def _parse_floats(s):
|
|
229
|
+
return [float(x) for x in re.split(r"[ ,]+", s.strip()) if x]
|
|
230
|
+
|
|
231
|
+
def _mat_identity():
|
|
232
|
+
return [
|
|
233
|
+
[1.0, 0.0, 0.0],
|
|
234
|
+
[0.0, 1.0, 0.0],
|
|
235
|
+
[0.0, 0.0, 1.0],
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
def _mat_mul(a, b):
|
|
239
|
+
res = [[0.0, 0.0, 0.0] for _ in range(3)]
|
|
240
|
+
for i in range(3):
|
|
241
|
+
for j in range(3):
|
|
242
|
+
res[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j]
|
|
243
|
+
return res
|
|
244
|
+
|
|
245
|
+
def _mat_apply(m, x, y):
|
|
246
|
+
nx = m[0][0] * x + m[0][1] * y + m[0][2]
|
|
247
|
+
ny = m[1][0] * x + m[1][1] * y + m[1][2]
|
|
248
|
+
return nx, ny
|
|
249
|
+
|
|
250
|
+
def parse_transform(transform_str):
|
|
251
|
+
"""Parse SVG transform into 3x3 matrix."""
|
|
252
|
+
if not transform_str:
|
|
253
|
+
return _mat_identity()
|
|
254
|
+
|
|
255
|
+
mat = _mat_identity()
|
|
256
|
+
items = re.findall(
|
|
257
|
+
r"(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)", transform_str
|
|
258
|
+
)
|
|
259
|
+
for op, args_str in items:
|
|
260
|
+
nums = _parse_floats(args_str)
|
|
261
|
+
op = op.lower()
|
|
262
|
+
|
|
263
|
+
if op == "matrix" and len(nums) == 6:
|
|
264
|
+
a, b, c, d, e, f = nums
|
|
265
|
+
m2 = [
|
|
266
|
+
[a, c, e],
|
|
267
|
+
[b, d, f],
|
|
268
|
+
[0.0, 0.0, 1.0],
|
|
269
|
+
]
|
|
270
|
+
elif op == "translate":
|
|
271
|
+
tx = nums[0] if len(nums) else 0.0
|
|
272
|
+
ty = nums[1] if len(nums) > 1 else 0.0
|
|
273
|
+
m2 = [
|
|
274
|
+
[1.0, 0.0, tx],
|
|
275
|
+
[0.0, 1.0, ty],
|
|
276
|
+
[0.0, 0.0, 1.0],
|
|
277
|
+
]
|
|
278
|
+
elif op == "scale":
|
|
279
|
+
sx = nums[0] if len(nums) else 1.0
|
|
280
|
+
sy = nums[1] if len(nums) > 1 else sx
|
|
281
|
+
m2 = [
|
|
282
|
+
[sx, 0.0, 0.0],
|
|
283
|
+
[0.0, sy, 0.0],
|
|
284
|
+
[0.0, 0.0, 1.0],
|
|
285
|
+
]
|
|
286
|
+
elif op == "rotate":
|
|
287
|
+
angle = nums[0] if nums else 0.0
|
|
288
|
+
rad = math.radians(angle)
|
|
289
|
+
cos_a, sin_a = math.cos(rad), math.sin(rad)
|
|
290
|
+
|
|
291
|
+
if len(nums) == 3:
|
|
292
|
+
cx, cy = nums[1], nums[2]
|
|
293
|
+
t1 = [[1, 0, cx], [0, 1, cy], [0, 0, 1]]
|
|
294
|
+
r = [[cos_a, -sin_a, 0], [sin_a, cos_a, 0], [0, 0, 1]]
|
|
295
|
+
t2 = [[1, 0, -cx], [0, 1, -cy], [0, 0, 1]]
|
|
296
|
+
m2 = _mat_mul(_mat_mul(t1, r), t2)
|
|
297
|
+
else:
|
|
298
|
+
m2 = [
|
|
299
|
+
[cos_a, -sin_a, 0.0],
|
|
300
|
+
[sin_a, cos_a, 0.0],
|
|
301
|
+
[0.0, 0.0, 1.0],
|
|
302
|
+
]
|
|
303
|
+
elif op == "skewx":
|
|
304
|
+
angle = nums[0] if nums else 0.0
|
|
305
|
+
t = math.tan(math.radians(angle))
|
|
306
|
+
m2 = [[1, t, 0], [0, 1, 0], [0, 0, 1]]
|
|
307
|
+
elif op == "skewy":
|
|
308
|
+
angle = nums[0] if nums else 0.0
|
|
309
|
+
t = math.tan(math.radians(angle))
|
|
310
|
+
m2 = [[1, 0, 0], [t, 1, 0], [0, 0, 1]]
|
|
311
|
+
else:
|
|
312
|
+
m2 = _mat_identity()
|
|
313
|
+
|
|
314
|
+
mat = _mat_mul(mat, m2)
|
|
315
|
+
|
|
316
|
+
return mat
|
|
317
|
+
|
|
318
|
+
# ======================================================
|
|
319
|
+
# 3. Parse actual shapes into SVG pixel space
|
|
320
|
+
# ======================================================
|
|
321
|
+
|
|
322
|
+
parsed_shapes = []
|
|
323
|
+
|
|
324
|
+
# -------- RECTANGLE --------
|
|
325
|
+
for tag in re.findall(r"<rect[^>]*/?>", inner_svg):
|
|
326
|
+
x = float(get_attr(tag, "x", 0))
|
|
327
|
+
y = float(get_attr(tag, "y", 0))
|
|
328
|
+
w = float(get_attr(tag, "width", 0))
|
|
329
|
+
h = float(get_attr(tag, "height", 0))
|
|
330
|
+
fill = get_attr(tag, "fill", "none")
|
|
331
|
+
stroke = get_attr(tag, "stroke", None)
|
|
332
|
+
stroke_w = float(get_attr(tag, "stroke-width", 1) or 1)
|
|
333
|
+
transform_str = get_attr(tag, "transform", "")
|
|
334
|
+
|
|
335
|
+
if transform_str:
|
|
336
|
+
mat = parse_transform(transform_str)
|
|
337
|
+
pts = [
|
|
338
|
+
(x, y),
|
|
339
|
+
(x + w, y),
|
|
340
|
+
(x + w, y + h),
|
|
341
|
+
(x, y + h),
|
|
342
|
+
]
|
|
343
|
+
pts_tr = [_mat_apply(mat, px, py) for px, py in pts]
|
|
344
|
+
parsed_shapes.append(
|
|
345
|
+
(
|
|
346
|
+
"polygon",
|
|
347
|
+
{
|
|
348
|
+
"points": pts_tr,
|
|
349
|
+
"fill": fill,
|
|
350
|
+
"stroke": stroke,
|
|
351
|
+
"sw": stroke_w,
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
parsed_shapes.append(
|
|
357
|
+
(
|
|
358
|
+
"rect",
|
|
359
|
+
{
|
|
360
|
+
"x": x,
|
|
361
|
+
"y": y,
|
|
362
|
+
"w": w,
|
|
363
|
+
"h": h,
|
|
364
|
+
"fill": fill,
|
|
365
|
+
"stroke": stroke,
|
|
366
|
+
"sw": stroke_w,
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# -------- CIRCLE --------
|
|
372
|
+
for tag in re.findall(r"<circle[^>]*/?>", inner_svg):
|
|
373
|
+
cx = float(get_attr(tag, "cx", 0))
|
|
374
|
+
cy = float(get_attr(tag, "cy", 0))
|
|
375
|
+
r = float(get_attr(tag, "r", 0))
|
|
376
|
+
fill = get_attr(tag, "fill", "none")
|
|
377
|
+
stroke = get_attr(tag, "stroke", None)
|
|
378
|
+
stroke_w = float(get_attr(tag, "stroke-width", 1) or 1)
|
|
379
|
+
transform_str = get_attr(tag, "transform", "")
|
|
380
|
+
|
|
381
|
+
if transform_str:
|
|
382
|
+
mat = parse_transform(transform_str)
|
|
383
|
+
cx, cy = _mat_apply(mat, cx, cy)
|
|
384
|
+
|
|
385
|
+
parsed_shapes.append(
|
|
386
|
+
(
|
|
387
|
+
"circle",
|
|
388
|
+
{
|
|
389
|
+
"cx": cx,
|
|
390
|
+
"cy": cy,
|
|
391
|
+
"r": r,
|
|
392
|
+
"fill": fill,
|
|
393
|
+
"stroke": stroke,
|
|
394
|
+
"sw": stroke_w,
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# -------- LINE --------
|
|
400
|
+
for tag in re.findall(r"<line[^>]*/?>", inner_svg):
|
|
401
|
+
x1 = float(get_attr(tag, "x1", 0))
|
|
402
|
+
y1 = float(get_attr(tag, "y1", 0))
|
|
403
|
+
x2 = float(get_attr(tag, "x2", 0))
|
|
404
|
+
y2 = float(get_attr(tag, "y2", 0))
|
|
405
|
+
stroke = get_attr(tag, "stroke", "black")
|
|
406
|
+
stroke_w = float(get_attr(tag, "stroke-width", 1) or 1)
|
|
407
|
+
transform_str = get_attr(tag, "transform", "")
|
|
408
|
+
|
|
409
|
+
if transform_str:
|
|
410
|
+
mat = parse_transform(transform_str)
|
|
411
|
+
x1, y1 = _mat_apply(mat, x1, y1)
|
|
412
|
+
x2, y2 = _mat_apply(mat, x2, y2)
|
|
413
|
+
|
|
414
|
+
parsed_shapes.append(
|
|
415
|
+
(
|
|
416
|
+
"line",
|
|
417
|
+
{
|
|
418
|
+
"x1": x1,
|
|
419
|
+
"y1": y1,
|
|
420
|
+
"x2": x2,
|
|
421
|
+
"y2": y2,
|
|
422
|
+
"stroke": stroke,
|
|
423
|
+
"sw": stroke_w,
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# -------- POLYGON --------
|
|
429
|
+
for tag in re.findall(r"<polygon[^>]*/?>", inner_svg):
|
|
430
|
+
pts_raw = get_attr(tag, "points", "")
|
|
431
|
+
pts = []
|
|
432
|
+
for p in pts_raw.split():
|
|
433
|
+
if "," in p:
|
|
434
|
+
xx, yy = p.split(",")
|
|
435
|
+
pts.append((float(xx), float(yy)))
|
|
436
|
+
fill = get_attr(tag, "fill", "none")
|
|
437
|
+
stroke = get_attr(tag, "stroke", None)
|
|
438
|
+
stroke_w = float(get_attr(tag, "stroke-width", 1) or 1)
|
|
439
|
+
transform_str = get_attr(tag, "transform", "")
|
|
440
|
+
|
|
441
|
+
if transform_str:
|
|
442
|
+
mat = parse_transform(transform_str)
|
|
443
|
+
pts = [_mat_apply(mat, px, py) for px, py in pts]
|
|
444
|
+
|
|
445
|
+
parsed_shapes.append(
|
|
446
|
+
("polygon", {"points": pts, "fill": fill, "stroke": stroke, "sw": stroke_w})
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# -------- TEXT (simple) --------
|
|
450
|
+
for full_tag in re.findall(r"<text[^>]*>.*?</text>", inner_svg, flags=re.DOTALL):
|
|
451
|
+
txt = re.sub(r"<.*?>", "", full_tag)
|
|
452
|
+
x = float(get_attr(full_tag, "x", 0))
|
|
453
|
+
y = float(get_attr(full_tag, "y", 0))
|
|
454
|
+
fill = get_attr(full_tag, "fill", "black")
|
|
455
|
+
transform_str = get_attr(full_tag, "transform", "")
|
|
456
|
+
|
|
457
|
+
if transform_str:
|
|
458
|
+
mat = parse_transform(transform_str)
|
|
459
|
+
x, y = _mat_apply(mat, x, y)
|
|
460
|
+
|
|
461
|
+
parsed_shapes.append(
|
|
462
|
+
("text", {"x": x, "y": y, "text": txt.strip(), "fill": fill})
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# ======================================================
|
|
466
|
+
# 4. CSYS → SVG pixel coordinates
|
|
467
|
+
# ======================================================
|
|
468
|
+
|
|
469
|
+
padding = 50
|
|
470
|
+
scale = 96 # px per inch
|
|
471
|
+
arrow_len_svg = 24
|
|
472
|
+
|
|
473
|
+
def csys_svg_xy(csys):
|
|
474
|
+
raw_x = csys.get("x")
|
|
475
|
+
raw_y = csys.get("y")
|
|
476
|
+
raw_d = csys.get("distance")
|
|
477
|
+
raw_a = csys.get("angle")
|
|
478
|
+
|
|
479
|
+
if raw_x not in ("", None) and raw_y not in ("", None):
|
|
480
|
+
x_in = float(raw_x)
|
|
481
|
+
y_in = float(raw_y)
|
|
482
|
+
elif raw_d not in ("", None) and raw_a not in ("", None):
|
|
483
|
+
dist = float(raw_d)
|
|
484
|
+
ang = math.radians(float(raw_a))
|
|
485
|
+
x_in = dist * math.cos(ang)
|
|
486
|
+
y_in = dist * math.sin(ang)
|
|
487
|
+
else:
|
|
488
|
+
x_in, y_in = 0.0, 0.0
|
|
489
|
+
|
|
490
|
+
# inches → SVG px ; y-up → y-down
|
|
491
|
+
return x_in * scale, -y_in * scale
|
|
492
|
+
|
|
493
|
+
def csys_svg_axes_endpoints(csys, base_x, base_y, arrow_len):
|
|
494
|
+
rot = math.radians(float(csys.get("rotation", 0) or 0))
|
|
495
|
+
|
|
496
|
+
# CSYS-space directions (Y-up)
|
|
497
|
+
dx_x = math.cos(rot)
|
|
498
|
+
dy_x = math.sin(rot)
|
|
499
|
+
dx_y = -math.sin(rot)
|
|
500
|
+
dy_y = math.cos(rot)
|
|
501
|
+
|
|
502
|
+
# Convert CSYS → SVG (flip y)
|
|
503
|
+
dx_x_s = dx_x
|
|
504
|
+
dy_x_s = -dy_x
|
|
505
|
+
dx_y_s = dx_y
|
|
506
|
+
dy_y_s = -dy_y
|
|
507
|
+
|
|
508
|
+
# Endpoints in SVG space
|
|
509
|
+
x_x = base_x + dx_x_s * arrow_len
|
|
510
|
+
y_x = base_y + dy_x_s * arrow_len
|
|
511
|
+
x_y = base_x + dx_y_s * arrow_len
|
|
512
|
+
y_y = base_y + dy_y_s * arrow_len
|
|
513
|
+
|
|
514
|
+
return (x_x, y_x), (x_y, y_y)
|
|
515
|
+
|
|
516
|
+
# ======================================================
|
|
517
|
+
# 5. Compute bounding box from (SVG shapes + CSYS)
|
|
518
|
+
# ======================================================
|
|
519
|
+
|
|
520
|
+
pts = []
|
|
521
|
+
|
|
522
|
+
# SVG shapes
|
|
523
|
+
for typ, p in parsed_shapes:
|
|
524
|
+
if typ == "rect":
|
|
525
|
+
pts += [(p["x"], p["y"]), (p["x"] + p["w"], p["y"] + p["h"])]
|
|
526
|
+
elif typ == "circle":
|
|
527
|
+
pts += [
|
|
528
|
+
(p["cx"] - p["r"], p["cy"] - p["r"]),
|
|
529
|
+
(p["cx"] + p["r"], p["cy"] + p["r"]),
|
|
530
|
+
]
|
|
531
|
+
elif typ == "line":
|
|
532
|
+
pts += [(p["x1"], p["y1"]), (p["x2"], p["y2"])]
|
|
533
|
+
elif typ == "polygon":
|
|
534
|
+
pts += p["points"]
|
|
535
|
+
elif typ == "text":
|
|
536
|
+
pts.append((p["x"], p["y"]))
|
|
537
|
+
|
|
538
|
+
# CSYS
|
|
539
|
+
for csys_name, csys in csys_children.items():
|
|
540
|
+
bx, by = csys_svg_xy(csys)
|
|
541
|
+
pts.append((bx, by))
|
|
542
|
+
(x_x, y_x), (x_y, y_y) = csys_svg_axes_endpoints(csys, bx, by, arrow_len_svg)
|
|
543
|
+
pts += [(x_x, y_x), (x_y, y_y)]
|
|
544
|
+
|
|
545
|
+
if not pts:
|
|
546
|
+
pts = [(0, 0)]
|
|
547
|
+
|
|
548
|
+
xs = [p[0] for p in pts]
|
|
549
|
+
ys = [p[1] for p in pts]
|
|
550
|
+
|
|
551
|
+
min_x, max_x = min(xs), max(xs)
|
|
552
|
+
min_y, max_y = min(ys), max(ys)
|
|
553
|
+
|
|
554
|
+
width = int((max_x - min_x) + 2 * padding)
|
|
555
|
+
height = int((max_y - min_y) + 2 * padding)
|
|
556
|
+
|
|
557
|
+
def map_xy(x, y):
|
|
558
|
+
return (
|
|
559
|
+
int((x - min_x) + padding),
|
|
560
|
+
int((y - min_y) + padding),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# ======================================================
|
|
564
|
+
# 6. Create PNG canvas and draw everything
|
|
565
|
+
# ======================================================
|
|
566
|
+
|
|
567
|
+
img = Image.new("RGB", (width, height), "white")
|
|
568
|
+
draw = ImageDraw.Draw(img)
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
font = ImageFont.truetype("Arial.ttf", 8)
|
|
572
|
+
except Exception:
|
|
573
|
+
font = ImageFont.load_default()
|
|
574
|
+
|
|
575
|
+
# --- SHAPES ---
|
|
576
|
+
for typ, p in parsed_shapes:
|
|
577
|
+
if typ == "rect":
|
|
578
|
+
x1, y1 = map_xy(p["x"], p["y"])
|
|
579
|
+
x2, y2 = map_xy(p["x"] + p["w"], p["y"] + p["h"])
|
|
580
|
+
draw.rectangle(
|
|
581
|
+
(x1, y1, x2, y2),
|
|
582
|
+
fill=p["fill"],
|
|
583
|
+
outline=p["stroke"],
|
|
584
|
+
width=int(p["sw"]),
|
|
585
|
+
)
|
|
586
|
+
elif typ == "circle":
|
|
587
|
+
cx, cy = map_xy(p["cx"], p["cy"])
|
|
588
|
+
r = p["r"]
|
|
589
|
+
draw.ellipse(
|
|
590
|
+
(cx - r, cy - r, cx + r, cy + r),
|
|
591
|
+
fill=p["fill"],
|
|
592
|
+
outline=p["stroke"],
|
|
593
|
+
width=int(p["sw"]),
|
|
594
|
+
)
|
|
595
|
+
elif typ == "line":
|
|
596
|
+
x1, y1 = map_xy(p["x1"], p["y1"])
|
|
597
|
+
x2, y2 = map_xy(p["x2"], p["y2"])
|
|
598
|
+
draw.line((x1, y1, x2, y2), fill=p["stroke"], width=int(p["sw"]))
|
|
599
|
+
elif typ == "polygon":
|
|
600
|
+
pts2 = [map_xy(x, y) for x, y in p["points"]]
|
|
601
|
+
draw.polygon(pts2, fill=p["fill"], outline=p["stroke"])
|
|
602
|
+
elif typ == "text":
|
|
603
|
+
tx, ty = map_xy(p["x"], p["y"])
|
|
604
|
+
draw.text((tx, ty), p["text"], fill=p["fill"], font=font)
|
|
605
|
+
|
|
606
|
+
# --- CSYS ---
|
|
607
|
+
dot_radius = 4
|
|
608
|
+
|
|
609
|
+
for csys_name, csys in csys_children.items():
|
|
610
|
+
bx, by = csys_svg_xy(csys)
|
|
611
|
+
cx, cy = map_xy(bx, by)
|
|
612
|
+
|
|
613
|
+
# Dot
|
|
614
|
+
draw.ellipse(
|
|
615
|
+
(cx - dot_radius, cy - dot_radius, cx + dot_radius, cy + dot_radius),
|
|
616
|
+
fill="black",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Endpoints in SVG → map to PNG
|
|
620
|
+
(x_x, y_x), (x_y, y_y) = csys_svg_axes_endpoints(csys, bx, by, arrow_len_svg)
|
|
621
|
+
px1, py1 = map_xy(x_x, y_x)
|
|
622
|
+
px2, py2 = map_xy(x_y, y_y)
|
|
623
|
+
|
|
624
|
+
# Axes
|
|
625
|
+
draw.line((cx, cy, px1, py1), fill="red", width=2) # X
|
|
626
|
+
draw.line((cx, cy, px2, py2), fill="green", width=2) # Y
|
|
627
|
+
|
|
628
|
+
# Label
|
|
629
|
+
draw.text((cx + 6, cy - 6), csys_name, fill="blue", font=font)
|
|
630
|
+
|
|
631
|
+
# ======================================================
|
|
632
|
+
# 7. Save PNG
|
|
633
|
+
# ======================================================
|
|
634
|
+
|
|
635
|
+
png_path = fileio.path("drawing png")
|
|
636
|
+
img.save(png_path, dpi=(1000, 1000))
|
|
637
|
+
|
|
638
|
+
print()
|
|
639
|
+
print(f"Part file '{state.partnumber('pn')}' updated")
|
|
640
|
+
print()
|