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.
Files changed (38) hide show
  1. gerberdiff/__init__.py +86 -0
  2. gerberdiff/cli.py +527 -0
  3. gerberdiff/diff/__init__.py +0 -0
  4. gerberdiff/diff/diff_engine.py +409 -0
  5. gerberdiff/diff/layer_matcher.py +202 -0
  6. gerberdiff/export/__init__.py +0 -0
  7. gerberdiff/export/json_report.py +184 -0
  8. gerberdiff/export/png_export.py +92 -0
  9. gerberdiff/export/svg_export.py +187 -0
  10. gerberdiff/geometry/__init__.py +37 -0
  11. gerberdiff/geometry/attribute.py +203 -0
  12. gerberdiff/geometry/driver.py +227 -0
  13. gerberdiff/geometry/expand.py +232 -0
  14. gerberdiff/geometry/geom_diff.py +153 -0
  15. gerberdiff/geometry/layer_geometry.py +665 -0
  16. gerberdiff/geometry/macro_geom.py +215 -0
  17. gerberdiff/geometry/primitives.py +108 -0
  18. gerberdiff/geometry/types.py +85 -0
  19. gerberdiff/parse/__init__.py +0 -0
  20. gerberdiff/parse/arc_math.py +162 -0
  21. gerberdiff/parse/excellon_parser.py +338 -0
  22. gerberdiff/parse/gerber_parser.py +244 -0
  23. gerberdiff/parse/gerber_state.py +780 -0
  24. gerberdiff/parse/macro_parser.py +604 -0
  25. gerberdiff/parse/tokenizer.py +153 -0
  26. gerberdiff/py.typed +0 -0
  27. gerberdiff/render/__init__.py +0 -0
  28. gerberdiff/render/compiled_render.py +240 -0
  29. gerberdiff/render/draw_ops.py +205 -0
  30. gerberdiff/render/macro_renderer.py +343 -0
  31. gerberdiff/render/renderer.py +283 -0
  32. gerberdiff/render/viewport.py +85 -0
  33. gerberdiff/types.py +360 -0
  34. gerberdiff-0.29.0.dist-info/METADATA +105 -0
  35. gerberdiff-0.29.0.dist-info/RECORD +38 -0
  36. gerberdiff-0.29.0.dist-info/WHEEL +4 -0
  37. gerberdiff-0.29.0.dist-info/entry_points.txt +2 -0
  38. 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