gerberdiff 0.29.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.
- gerberdiff/__init__.py +86 -0
- gerberdiff/cli.py +527 -0
- gerberdiff/diff/__init__.py +0 -0
- gerberdiff/diff/diff_engine.py +409 -0
- gerberdiff/diff/layer_matcher.py +202 -0
- gerberdiff/export/__init__.py +0 -0
- gerberdiff/export/json_report.py +184 -0
- gerberdiff/export/png_export.py +92 -0
- gerberdiff/export/svg_export.py +187 -0
- gerberdiff/geometry/__init__.py +37 -0
- gerberdiff/geometry/attribute.py +203 -0
- gerberdiff/geometry/driver.py +227 -0
- gerberdiff/geometry/expand.py +232 -0
- gerberdiff/geometry/geom_diff.py +153 -0
- gerberdiff/geometry/layer_geometry.py +665 -0
- gerberdiff/geometry/macro_geom.py +215 -0
- gerberdiff/geometry/primitives.py +108 -0
- gerberdiff/geometry/types.py +85 -0
- gerberdiff/parse/__init__.py +0 -0
- gerberdiff/parse/arc_math.py +162 -0
- gerberdiff/parse/excellon_parser.py +338 -0
- gerberdiff/parse/gerber_parser.py +244 -0
- gerberdiff/parse/gerber_state.py +780 -0
- gerberdiff/parse/macro_parser.py +604 -0
- gerberdiff/parse/tokenizer.py +153 -0
- gerberdiff/py.typed +0 -0
- gerberdiff/render/__init__.py +0 -0
- gerberdiff/render/compiled_render.py +240 -0
- gerberdiff/render/draw_ops.py +205 -0
- gerberdiff/render/macro_renderer.py +343 -0
- gerberdiff/render/renderer.py +283 -0
- gerberdiff/render/viewport.py +85 -0
- gerberdiff/types.py +360 -0
- gerberdiff-0.29.0.dist-info/METADATA +105 -0
- gerberdiff-0.29.0.dist-info/RECORD +38 -0
- gerberdiff-0.29.0.dist-info/WHEEL +4 -0
- gerberdiff-0.29.0.dist-info/entry_points.txt +2 -0
- gerberdiff-0.29.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import ClassVar, TypeAlias
|
|
5
|
+
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# Expression node types
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# These form a recursive union (ExprNode). With `from __future__ import
|
|
10
|
+
# annotations` all field type annotations are strings, so forward references
|
|
11
|
+
# to ExprNode resolve correctly even though the alias is defined after the
|
|
12
|
+
# dataclasses.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LiteralNode:
|
|
17
|
+
value: float = 0.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class VariableNode:
|
|
22
|
+
index: int = 1 # $1, $2, ... -- 1-based
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class UnaryNegNode:
|
|
27
|
+
operand: ExprNode = field(default_factory=LiteralNode)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class BinaryNode:
|
|
32
|
+
op: str = "+" # "+", "-", "x", "/"
|
|
33
|
+
left: ExprNode = field(default_factory=LiteralNode)
|
|
34
|
+
right: ExprNode = field(default_factory=LiteralNode)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
ExprNode: TypeAlias = LiteralNode | VariableNode | UnaryNegNode | BinaryNode
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lit(v: float = 0.0) -> LiteralNode:
|
|
41
|
+
return LiteralNode(value=v)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Macro primitive templates (ExprNode fields -- evaluated later)
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CirclePrimitive:
|
|
51
|
+
code: ClassVar[int] = 1
|
|
52
|
+
exposure: ExprNode = field(default_factory=lambda: _lit(1.0))
|
|
53
|
+
diameter: ExprNode = field(default_factory=_lit)
|
|
54
|
+
center_x: ExprNode = field(default_factory=_lit)
|
|
55
|
+
center_y: ExprNode = field(default_factory=_lit)
|
|
56
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class LineVectorPrimitive:
|
|
61
|
+
code: ClassVar[int] = 20
|
|
62
|
+
exposure: ExprNode = field(default_factory=lambda: _lit(1.0))
|
|
63
|
+
width: ExprNode = field(default_factory=_lit)
|
|
64
|
+
start_x: ExprNode = field(default_factory=_lit)
|
|
65
|
+
start_y: ExprNode = field(default_factory=_lit)
|
|
66
|
+
end_x: ExprNode = field(default_factory=_lit)
|
|
67
|
+
end_y: ExprNode = field(default_factory=_lit)
|
|
68
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class LineCenterPrimitive:
|
|
73
|
+
code: ClassVar[int] = 21
|
|
74
|
+
exposure: ExprNode = field(default_factory=lambda: _lit(1.0))
|
|
75
|
+
width: ExprNode = field(default_factory=_lit)
|
|
76
|
+
height: ExprNode = field(default_factory=_lit)
|
|
77
|
+
center_x: ExprNode = field(default_factory=_lit)
|
|
78
|
+
center_y: ExprNode = field(default_factory=_lit)
|
|
79
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class OutlinePrimitive:
|
|
84
|
+
code: ClassVar[int] = 4
|
|
85
|
+
exposure: ExprNode = field(default_factory=lambda: _lit(1.0))
|
|
86
|
+
vertices: list[ExprNode] = field(default_factory=list) # flat [x0,y0,x1,y1,...]
|
|
87
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class PolygonPrimitive:
|
|
92
|
+
code: ClassVar[int] = 5
|
|
93
|
+
exposure: ExprNode = field(default_factory=lambda: _lit(1.0))
|
|
94
|
+
num_vertices: ExprNode = field(default_factory=lambda: _lit(4.0))
|
|
95
|
+
center_x: ExprNode = field(default_factory=_lit)
|
|
96
|
+
center_y: ExprNode = field(default_factory=_lit)
|
|
97
|
+
diameter: ExprNode = field(default_factory=_lit)
|
|
98
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class MoirePrimitive:
|
|
103
|
+
code: ClassVar[int] = 6
|
|
104
|
+
center_x: ExprNode = field(default_factory=_lit)
|
|
105
|
+
center_y: ExprNode = field(default_factory=_lit)
|
|
106
|
+
outer_diameter: ExprNode = field(default_factory=_lit)
|
|
107
|
+
ring_thickness: ExprNode = field(default_factory=_lit)
|
|
108
|
+
ring_gap: ExprNode = field(default_factory=_lit)
|
|
109
|
+
max_rings: ExprNode = field(default_factory=_lit)
|
|
110
|
+
crosshair_thickness: ExprNode = field(default_factory=_lit)
|
|
111
|
+
crosshair_length: ExprNode = field(default_factory=_lit)
|
|
112
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class ThermalPrimitive:
|
|
117
|
+
code: ClassVar[int] = 7
|
|
118
|
+
center_x: ExprNode = field(default_factory=_lit)
|
|
119
|
+
center_y: ExprNode = field(default_factory=_lit)
|
|
120
|
+
outer_diameter: ExprNode = field(default_factory=_lit)
|
|
121
|
+
inner_diameter: ExprNode = field(default_factory=_lit)
|
|
122
|
+
gap: ExprNode = field(default_factory=_lit)
|
|
123
|
+
rotation: ExprNode = field(default_factory=_lit)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class AssignmentStatement:
|
|
128
|
+
variable_index: int
|
|
129
|
+
expression: ExprNode = field(default_factory=_lit)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
MacroBodyItem: TypeAlias = (
|
|
133
|
+
CirclePrimitive
|
|
134
|
+
| LineVectorPrimitive
|
|
135
|
+
| LineCenterPrimitive
|
|
136
|
+
| OutlinePrimitive
|
|
137
|
+
| PolygonPrimitive
|
|
138
|
+
| MoirePrimitive
|
|
139
|
+
| ThermalPrimitive
|
|
140
|
+
| AssignmentStatement
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class MacroDef:
|
|
146
|
+
name: str
|
|
147
|
+
body: list[MacroBodyItem] = field(default_factory=list)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Evaluated primitive types (float fields -- produced after evaluation)
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class EvaluatedCircle:
|
|
157
|
+
code: ClassVar[int] = 1
|
|
158
|
+
exposure: float = 0.0
|
|
159
|
+
diameter: float = 0.0
|
|
160
|
+
center_x: float = 0.0
|
|
161
|
+
center_y: float = 0.0
|
|
162
|
+
rotation: float = 0.0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class EvaluatedLineVector:
|
|
167
|
+
code: ClassVar[int] = 20
|
|
168
|
+
exposure: float = 0.0
|
|
169
|
+
width: float = 0.0
|
|
170
|
+
start_x: float = 0.0
|
|
171
|
+
start_y: float = 0.0
|
|
172
|
+
end_x: float = 0.0
|
|
173
|
+
end_y: float = 0.0
|
|
174
|
+
rotation: float = 0.0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class EvaluatedLineCenter:
|
|
179
|
+
code: ClassVar[int] = 21
|
|
180
|
+
exposure: float = 0.0
|
|
181
|
+
width: float = 0.0
|
|
182
|
+
height: float = 0.0
|
|
183
|
+
center_x: float = 0.0
|
|
184
|
+
center_y: float = 0.0
|
|
185
|
+
rotation: float = 0.0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class EvaluatedOutline:
|
|
190
|
+
code: ClassVar[int] = 4
|
|
191
|
+
exposure: float = 0.0
|
|
192
|
+
vertices: list[float] = field(default_factory=list)
|
|
193
|
+
rotation: float = 0.0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class EvaluatedPolygon:
|
|
198
|
+
code: ClassVar[int] = 5
|
|
199
|
+
exposure: float = 0.0
|
|
200
|
+
num_vertices: int = 4
|
|
201
|
+
center_x: float = 0.0
|
|
202
|
+
center_y: float = 0.0
|
|
203
|
+
diameter: float = 0.0
|
|
204
|
+
rotation: float = 0.0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class EvaluatedMoire:
|
|
209
|
+
code: ClassVar[int] = 6
|
|
210
|
+
center_x: float = 0.0
|
|
211
|
+
center_y: float = 0.0
|
|
212
|
+
outer_diameter: float = 0.0
|
|
213
|
+
ring_thickness: float = 0.0
|
|
214
|
+
ring_gap: float = 0.0
|
|
215
|
+
max_rings: int = 0
|
|
216
|
+
crosshair_thickness: float = 0.0
|
|
217
|
+
crosshair_length: float = 0.0
|
|
218
|
+
rotation: float = 0.0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class EvaluatedThermal:
|
|
223
|
+
code: ClassVar[int] = 7
|
|
224
|
+
center_x: float = 0.0
|
|
225
|
+
center_y: float = 0.0
|
|
226
|
+
outer_diameter: float = 0.0
|
|
227
|
+
inner_diameter: float = 0.0
|
|
228
|
+
gap: float = 0.0
|
|
229
|
+
rotation: float = 0.0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
EvaluatedPrimitive: TypeAlias = (
|
|
233
|
+
EvaluatedCircle
|
|
234
|
+
| EvaluatedLineVector
|
|
235
|
+
| EvaluatedLineCenter
|
|
236
|
+
| EvaluatedOutline
|
|
237
|
+
| EvaluatedPolygon
|
|
238
|
+
| EvaluatedMoire
|
|
239
|
+
| EvaluatedThermal
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Expression parser (internal)
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Tokens: ("number", "3.14") | ("variable", "1") | ("op", "+") | ("lparen","(") | ("rparen",")")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _tokenize_expr(s: str) -> list[tuple[str, str]]:
|
|
249
|
+
tokens: list[tuple[str, str]] = []
|
|
250
|
+
i = 0
|
|
251
|
+
while i < len(s):
|
|
252
|
+
c = s[i]
|
|
253
|
+
if c in " \t\n\r":
|
|
254
|
+
i += 1
|
|
255
|
+
elif c.isdigit() or (c == "." and i + 1 < len(s) and s[i + 1].isdigit()):
|
|
256
|
+
j = i
|
|
257
|
+
while j < len(s) and (s[j].isdigit() or s[j] == "."):
|
|
258
|
+
j += 1
|
|
259
|
+
tokens.append(("number", s[i:j]))
|
|
260
|
+
i = j
|
|
261
|
+
elif c == "$":
|
|
262
|
+
i += 1
|
|
263
|
+
j = i
|
|
264
|
+
while j < len(s) and s[j].isdigit():
|
|
265
|
+
j += 1
|
|
266
|
+
tokens.append(("variable", s[i:j]))
|
|
267
|
+
i = j
|
|
268
|
+
elif c in "+-/":
|
|
269
|
+
tokens.append(("op", c))
|
|
270
|
+
i += 1
|
|
271
|
+
elif c in "xX":
|
|
272
|
+
tokens.append(("op", "x"))
|
|
273
|
+
i += 1
|
|
274
|
+
elif c == "(":
|
|
275
|
+
tokens.append(("lparen", "("))
|
|
276
|
+
i += 1
|
|
277
|
+
elif c == ")":
|
|
278
|
+
tokens.append(("rparen", ")"))
|
|
279
|
+
i += 1
|
|
280
|
+
else:
|
|
281
|
+
i += 1
|
|
282
|
+
return tokens
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class _ExprParser:
|
|
286
|
+
"""Recursive-descent parser for Gerber macro expressions.
|
|
287
|
+
|
|
288
|
+
Precedence (low -> high): additive (+/-) -> multiplicative (x/) -> unary (-) -> primary
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
def __init__(self, tokens: list[tuple[str, str]]) -> None:
|
|
292
|
+
self._tokens = tokens
|
|
293
|
+
self._pos = 0
|
|
294
|
+
|
|
295
|
+
def _peek(self) -> tuple[str, str] | None:
|
|
296
|
+
return self._tokens[self._pos] if self._pos < len(self._tokens) else None
|
|
297
|
+
|
|
298
|
+
def _consume(self) -> tuple[str, str]:
|
|
299
|
+
tok = self._tokens[self._pos]
|
|
300
|
+
self._pos += 1
|
|
301
|
+
return tok
|
|
302
|
+
|
|
303
|
+
def parse(self) -> ExprNode:
|
|
304
|
+
return self._additive()
|
|
305
|
+
|
|
306
|
+
def _additive(self) -> ExprNode:
|
|
307
|
+
left: ExprNode = self._multiplicative()
|
|
308
|
+
tok = self._peek()
|
|
309
|
+
while tok is not None and tok[0] == "op" and tok[1] in ("+", "-"):
|
|
310
|
+
op = self._consume()[1]
|
|
311
|
+
right = self._multiplicative()
|
|
312
|
+
left = BinaryNode(op=op, left=left, right=right)
|
|
313
|
+
tok = self._peek()
|
|
314
|
+
return left
|
|
315
|
+
|
|
316
|
+
def _multiplicative(self) -> ExprNode:
|
|
317
|
+
left: ExprNode = self._unary()
|
|
318
|
+
tok = self._peek()
|
|
319
|
+
while tok is not None and tok[0] == "op" and tok[1] in ("x", "/"):
|
|
320
|
+
op = self._consume()[1]
|
|
321
|
+
right = self._unary()
|
|
322
|
+
left = BinaryNode(op=op, left=left, right=right)
|
|
323
|
+
tok = self._peek()
|
|
324
|
+
return left
|
|
325
|
+
|
|
326
|
+
def _unary(self) -> ExprNode:
|
|
327
|
+
tok = self._peek()
|
|
328
|
+
if tok is not None and tok[0] == "op" and tok[1] == "-":
|
|
329
|
+
self._consume()
|
|
330
|
+
return UnaryNegNode(operand=self._unary())
|
|
331
|
+
return self._primary()
|
|
332
|
+
|
|
333
|
+
def _primary(self) -> ExprNode:
|
|
334
|
+
tok = self._peek()
|
|
335
|
+
if tok is None:
|
|
336
|
+
return LiteralNode(value=0.0)
|
|
337
|
+
if tok[0] == "number":
|
|
338
|
+
self._consume()
|
|
339
|
+
return LiteralNode(value=float(tok[1]))
|
|
340
|
+
if tok[0] == "variable":
|
|
341
|
+
self._consume()
|
|
342
|
+
return VariableNode(index=int(tok[1]))
|
|
343
|
+
if tok[0] == "lparen":
|
|
344
|
+
self._consume()
|
|
345
|
+
expr = self._additive()
|
|
346
|
+
if (t := self._peek()) is not None and t[0] == "rparen":
|
|
347
|
+
self._consume()
|
|
348
|
+
return expr
|
|
349
|
+
return LiteralNode(value=0.0)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _parse_expr(s: str) -> ExprNode:
|
|
353
|
+
return _ExprParser(_tokenize_expr(s)).parse()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _parse_args(s: str) -> list[ExprNode]:
|
|
357
|
+
"""Split a comma-separated argument string (respecting parentheses) and parse each arg."""
|
|
358
|
+
args: list[ExprNode] = []
|
|
359
|
+
depth = 0
|
|
360
|
+
buf = ""
|
|
361
|
+
for c in s:
|
|
362
|
+
if c == "(":
|
|
363
|
+
depth += 1
|
|
364
|
+
buf += c
|
|
365
|
+
elif c == ")":
|
|
366
|
+
depth -= 1
|
|
367
|
+
buf += c
|
|
368
|
+
elif c == "," and depth == 0:
|
|
369
|
+
args.append(_parse_expr(buf.strip()))
|
|
370
|
+
buf = ""
|
|
371
|
+
else:
|
|
372
|
+
buf += c
|
|
373
|
+
if buf.strip():
|
|
374
|
+
args.append(_parse_expr(buf.strip()))
|
|
375
|
+
return args
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _get_arg(args: list[ExprNode], idx: int, default: float = 0.0) -> ExprNode:
|
|
379
|
+
return args[idx] if idx < len(args) else _lit(default)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _build_primitive(code: int, args: list[ExprNode]) -> MacroBodyItem | None:
|
|
383
|
+
"""Construct the appropriate primitive template from a code and argument list."""
|
|
384
|
+
if code == 1:
|
|
385
|
+
return CirclePrimitive(
|
|
386
|
+
exposure=_get_arg(args, 0, 1.0),
|
|
387
|
+
diameter=_get_arg(args, 1),
|
|
388
|
+
center_x=_get_arg(args, 2),
|
|
389
|
+
center_y=_get_arg(args, 3),
|
|
390
|
+
rotation=_get_arg(args, 4),
|
|
391
|
+
)
|
|
392
|
+
if code in (2, 20):
|
|
393
|
+
return LineVectorPrimitive(
|
|
394
|
+
exposure=_get_arg(args, 0, 1.0),
|
|
395
|
+
width=_get_arg(args, 1),
|
|
396
|
+
start_x=_get_arg(args, 2),
|
|
397
|
+
start_y=_get_arg(args, 3),
|
|
398
|
+
end_x=_get_arg(args, 4),
|
|
399
|
+
end_y=_get_arg(args, 5),
|
|
400
|
+
rotation=_get_arg(args, 6),
|
|
401
|
+
)
|
|
402
|
+
if code in (21, 22):
|
|
403
|
+
return LineCenterPrimitive(
|
|
404
|
+
exposure=_get_arg(args, 0, 1.0),
|
|
405
|
+
width=_get_arg(args, 1),
|
|
406
|
+
height=_get_arg(args, 2),
|
|
407
|
+
center_x=_get_arg(args, 3),
|
|
408
|
+
center_y=_get_arg(args, 4),
|
|
409
|
+
rotation=_get_arg(args, 5),
|
|
410
|
+
)
|
|
411
|
+
if code == 4:
|
|
412
|
+
if len(args) < 2:
|
|
413
|
+
return None
|
|
414
|
+
# args[0]=exposure, args[1]=vertex_count (evaluated at parse time)
|
|
415
|
+
# args[2..n-1]=vertex pairs, args[-1]=rotation
|
|
416
|
+
exposure = args[0]
|
|
417
|
+
vertices = args[2 : len(args) - 1]
|
|
418
|
+
rotation = args[-1]
|
|
419
|
+
return OutlinePrimitive(exposure=exposure, vertices=vertices, rotation=rotation)
|
|
420
|
+
if code == 5:
|
|
421
|
+
return PolygonPrimitive(
|
|
422
|
+
exposure=_get_arg(args, 0, 1.0),
|
|
423
|
+
num_vertices=_get_arg(args, 1, 4.0),
|
|
424
|
+
center_x=_get_arg(args, 2),
|
|
425
|
+
center_y=_get_arg(args, 3),
|
|
426
|
+
diameter=_get_arg(args, 4),
|
|
427
|
+
rotation=_get_arg(args, 5),
|
|
428
|
+
)
|
|
429
|
+
if code == 6:
|
|
430
|
+
return MoirePrimitive(
|
|
431
|
+
center_x=_get_arg(args, 0),
|
|
432
|
+
center_y=_get_arg(args, 1),
|
|
433
|
+
outer_diameter=_get_arg(args, 2),
|
|
434
|
+
ring_thickness=_get_arg(args, 3),
|
|
435
|
+
ring_gap=_get_arg(args, 4),
|
|
436
|
+
max_rings=_get_arg(args, 5),
|
|
437
|
+
crosshair_thickness=_get_arg(args, 6),
|
|
438
|
+
crosshair_length=_get_arg(args, 7),
|
|
439
|
+
rotation=_get_arg(args, 8),
|
|
440
|
+
)
|
|
441
|
+
if code == 7:
|
|
442
|
+
return ThermalPrimitive(
|
|
443
|
+
center_x=_get_arg(args, 0),
|
|
444
|
+
center_y=_get_arg(args, 1),
|
|
445
|
+
outer_diameter=_get_arg(args, 2),
|
|
446
|
+
inner_diameter=_get_arg(args, 3),
|
|
447
|
+
gap=_get_arg(args, 4),
|
|
448
|
+
rotation=_get_arg(args, 5),
|
|
449
|
+
)
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Public functions
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def parse_macro_body(name: str, body: str) -> MacroDef:
|
|
459
|
+
"""Parse the body of an AM extended command block into a MacroDef.
|
|
460
|
+
|
|
461
|
+
``body`` is the raw content between the AMNAME* token and the closing %,
|
|
462
|
+
with individual statements delimited by *. Each statement is either:
|
|
463
|
+
|
|
464
|
+
- An assignment: ``$n=<expr>``
|
|
465
|
+
- A primitive: ``<code>,<param0>,<param1>,...``
|
|
466
|
+
- A comment: starts with ``0 `` or is the single character ``0``
|
|
467
|
+
"""
|
|
468
|
+
items: list[MacroBodyItem] = []
|
|
469
|
+
for stmt in (s.strip() for s in body.split("*")):
|
|
470
|
+
if not stmt or stmt == "0" or stmt.startswith("0 "):
|
|
471
|
+
continue # skip comments
|
|
472
|
+
if stmt.startswith("$"):
|
|
473
|
+
eq = stmt.find("=")
|
|
474
|
+
if eq > 0:
|
|
475
|
+
var_idx = int(stmt[1:eq])
|
|
476
|
+
expr = _parse_expr(stmt[eq + 1 :])
|
|
477
|
+
items.append(AssignmentStatement(variable_index=var_idx, expression=expr))
|
|
478
|
+
continue
|
|
479
|
+
# Primitive statement: first argument is the primitive code (a literal integer)
|
|
480
|
+
args = _parse_args(stmt)
|
|
481
|
+
if not args:
|
|
482
|
+
continue
|
|
483
|
+
# Evaluate code with an empty var_map -- it must be a literal in practice
|
|
484
|
+
code = round(evaluate_macro_expression(args[0], {}))
|
|
485
|
+
prim = _build_primitive(code, args[1:])
|
|
486
|
+
if prim is not None:
|
|
487
|
+
items.append(prim)
|
|
488
|
+
|
|
489
|
+
return MacroDef(name=name, body=items)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def evaluate_macro_expression(node: ExprNode, var_map: dict[int, float]) -> float:
|
|
493
|
+
"""Recursively evaluate a macro expression tree given a variable substitution map."""
|
|
494
|
+
if isinstance(node, LiteralNode):
|
|
495
|
+
return node.value
|
|
496
|
+
if isinstance(node, VariableNode):
|
|
497
|
+
return var_map.get(node.index, 0.0)
|
|
498
|
+
if isinstance(node, UnaryNegNode):
|
|
499
|
+
return -evaluate_macro_expression(node.operand, var_map)
|
|
500
|
+
# BinaryNode
|
|
501
|
+
lv = evaluate_macro_expression(node.left, var_map)
|
|
502
|
+
rv = evaluate_macro_expression(node.right, var_map)
|
|
503
|
+
if node.op == "+":
|
|
504
|
+
return lv + rv
|
|
505
|
+
if node.op == "-":
|
|
506
|
+
return lv - rv
|
|
507
|
+
if node.op == "x":
|
|
508
|
+
return lv * rv
|
|
509
|
+
if node.op == "/" and rv != 0.0:
|
|
510
|
+
return lv / rv
|
|
511
|
+
return 0.0
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _eval(node: ExprNode, vm: dict[int, float]) -> float:
|
|
515
|
+
return evaluate_macro_expression(node, vm)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def evaluate_macro_primitives(
|
|
519
|
+
macro_def: MacroDef,
|
|
520
|
+
params: list[float],
|
|
521
|
+
) -> list[EvaluatedPrimitive]:
|
|
522
|
+
"""Evaluate all primitives in a macro definition with the given parameter list.
|
|
523
|
+
|
|
524
|
+
``params[0]`` maps to ``$1``, ``params[1]`` to ``$2``, etc.
|
|
525
|
+
Assignment statements update the variable map in evaluation order.
|
|
526
|
+
"""
|
|
527
|
+
var_map: dict[int, float] = {i + 1: v for i, v in enumerate(params)}
|
|
528
|
+
result: list[EvaluatedPrimitive] = []
|
|
529
|
+
|
|
530
|
+
for item in macro_def.body:
|
|
531
|
+
if isinstance(item, AssignmentStatement):
|
|
532
|
+
var_map[item.variable_index] = _eval(item.expression, var_map)
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
ev: EvaluatedPrimitive | None = None
|
|
536
|
+
|
|
537
|
+
if isinstance(item, CirclePrimitive):
|
|
538
|
+
ev = EvaluatedCircle(
|
|
539
|
+
exposure=_eval(item.exposure, var_map),
|
|
540
|
+
diameter=_eval(item.diameter, var_map),
|
|
541
|
+
center_x=_eval(item.center_x, var_map),
|
|
542
|
+
center_y=_eval(item.center_y, var_map),
|
|
543
|
+
rotation=_eval(item.rotation, var_map),
|
|
544
|
+
)
|
|
545
|
+
elif isinstance(item, LineVectorPrimitive):
|
|
546
|
+
ev = EvaluatedLineVector(
|
|
547
|
+
exposure=_eval(item.exposure, var_map),
|
|
548
|
+
width=_eval(item.width, var_map),
|
|
549
|
+
start_x=_eval(item.start_x, var_map),
|
|
550
|
+
start_y=_eval(item.start_y, var_map),
|
|
551
|
+
end_x=_eval(item.end_x, var_map),
|
|
552
|
+
end_y=_eval(item.end_y, var_map),
|
|
553
|
+
rotation=_eval(item.rotation, var_map),
|
|
554
|
+
)
|
|
555
|
+
elif isinstance(item, LineCenterPrimitive):
|
|
556
|
+
ev = EvaluatedLineCenter(
|
|
557
|
+
exposure=_eval(item.exposure, var_map),
|
|
558
|
+
width=_eval(item.width, var_map),
|
|
559
|
+
height=_eval(item.height, var_map),
|
|
560
|
+
center_x=_eval(item.center_x, var_map),
|
|
561
|
+
center_y=_eval(item.center_y, var_map),
|
|
562
|
+
rotation=_eval(item.rotation, var_map),
|
|
563
|
+
)
|
|
564
|
+
elif isinstance(item, OutlinePrimitive):
|
|
565
|
+
ev = EvaluatedOutline(
|
|
566
|
+
exposure=_eval(item.exposure, var_map),
|
|
567
|
+
vertices=[_eval(v, var_map) for v in item.vertices],
|
|
568
|
+
rotation=_eval(item.rotation, var_map),
|
|
569
|
+
)
|
|
570
|
+
elif isinstance(item, PolygonPrimitive):
|
|
571
|
+
ev = EvaluatedPolygon(
|
|
572
|
+
exposure=_eval(item.exposure, var_map),
|
|
573
|
+
num_vertices=round(_eval(item.num_vertices, var_map)),
|
|
574
|
+
center_x=_eval(item.center_x, var_map),
|
|
575
|
+
center_y=_eval(item.center_y, var_map),
|
|
576
|
+
diameter=_eval(item.diameter, var_map),
|
|
577
|
+
rotation=_eval(item.rotation, var_map),
|
|
578
|
+
)
|
|
579
|
+
elif isinstance(item, MoirePrimitive):
|
|
580
|
+
ev = EvaluatedMoire(
|
|
581
|
+
center_x=_eval(item.center_x, var_map),
|
|
582
|
+
center_y=_eval(item.center_y, var_map),
|
|
583
|
+
outer_diameter=_eval(item.outer_diameter, var_map),
|
|
584
|
+
ring_thickness=_eval(item.ring_thickness, var_map),
|
|
585
|
+
ring_gap=_eval(item.ring_gap, var_map),
|
|
586
|
+
max_rings=round(_eval(item.max_rings, var_map)),
|
|
587
|
+
crosshair_thickness=_eval(item.crosshair_thickness, var_map),
|
|
588
|
+
crosshair_length=_eval(item.crosshair_length, var_map),
|
|
589
|
+
rotation=_eval(item.rotation, var_map),
|
|
590
|
+
)
|
|
591
|
+
elif isinstance(item, ThermalPrimitive):
|
|
592
|
+
ev = EvaluatedThermal(
|
|
593
|
+
center_x=_eval(item.center_x, var_map),
|
|
594
|
+
center_y=_eval(item.center_y, var_map),
|
|
595
|
+
outer_diameter=_eval(item.outer_diameter, var_map),
|
|
596
|
+
inner_diameter=_eval(item.inner_diameter, var_map),
|
|
597
|
+
gap=_eval(item.gap, var_map),
|
|
598
|
+
rotation=_eval(item.rotation, var_map),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if ev is not None:
|
|
602
|
+
result.append(ev)
|
|
603
|
+
|
|
604
|
+
return result
|