meerk40t 0.9.7030__py2.py3-none-any.whl → 0.9.7040__py2.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 (79) hide show
  1. meerk40t/balormk/clone_loader.py +3 -2
  2. meerk40t/balormk/controller.py +28 -11
  3. meerk40t/balormk/cylindermod.py +1 -0
  4. meerk40t/balormk/device.py +13 -9
  5. meerk40t/balormk/driver.py +9 -2
  6. meerk40t/balormk/galvo_commands.py +3 -1
  7. meerk40t/balormk/gui/gui.py +6 -0
  8. meerk40t/balormk/livelightjob.py +338 -321
  9. meerk40t/balormk/mock_connection.py +4 -3
  10. meerk40t/balormk/usb_connection.py +11 -2
  11. meerk40t/camera/camera.py +19 -14
  12. meerk40t/camera/gui/camerapanel.py +6 -0
  13. meerk40t/core/cutplan.py +109 -51
  14. meerk40t/core/elements/element_treeops.py +435 -140
  15. meerk40t/core/elements/elements.py +100 -9
  16. meerk40t/core/elements/shapes.py +259 -39
  17. meerk40t/core/elements/tree_commands.py +10 -5
  18. meerk40t/core/node/elem_ellipse.py +18 -8
  19. meerk40t/core/node/elem_image.py +51 -19
  20. meerk40t/core/node/elem_line.py +18 -8
  21. meerk40t/core/node/elem_path.py +18 -8
  22. meerk40t/core/node/elem_point.py +10 -4
  23. meerk40t/core/node/elem_polyline.py +19 -11
  24. meerk40t/core/node/elem_rect.py +18 -8
  25. meerk40t/core/node/elem_text.py +11 -5
  26. meerk40t/core/node/filenode.py +2 -8
  27. meerk40t/core/node/groupnode.py +11 -11
  28. meerk40t/core/node/image_processed.py +11 -5
  29. meerk40t/core/node/image_raster.py +11 -5
  30. meerk40t/core/node/node.py +64 -16
  31. meerk40t/core/node/refnode.py +2 -1
  32. meerk40t/core/svg_io.py +91 -34
  33. meerk40t/device/dummydevice.py +7 -1
  34. meerk40t/extra/vtracer.py +222 -0
  35. meerk40t/grbl/device.py +81 -8
  36. meerk40t/gui/about.py +20 -0
  37. meerk40t/gui/devicepanel.py +20 -16
  38. meerk40t/gui/gui_mixins.py +4 -0
  39. meerk40t/gui/icons.py +330 -253
  40. meerk40t/gui/laserpanel.py +8 -3
  41. meerk40t/gui/laserrender.py +41 -21
  42. meerk40t/gui/magnetoptions.py +158 -65
  43. meerk40t/gui/materialtest.py +229 -39
  44. meerk40t/gui/navigationpanels.py +229 -24
  45. meerk40t/gui/propertypanels/hatchproperty.py +2 -0
  46. meerk40t/gui/propertypanels/imageproperty.py +160 -106
  47. meerk40t/gui/ribbon.py +6 -1
  48. meerk40t/gui/scenewidgets/gridwidget.py +29 -32
  49. meerk40t/gui/scenewidgets/rectselectwidget.py +190 -192
  50. meerk40t/gui/simulation.py +75 -77
  51. meerk40t/gui/statusbarwidgets/defaultoperations.py +84 -48
  52. meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
  53. meerk40t/gui/tips.py +15 -1
  54. meerk40t/gui/toolwidgets/toolpointmove.py +3 -1
  55. meerk40t/gui/wxmmain.py +242 -114
  56. meerk40t/gui/wxmscene.py +107 -24
  57. meerk40t/gui/wxmtree.py +4 -2
  58. meerk40t/gui/wxutils.py +60 -15
  59. meerk40t/image/imagetools.py +129 -65
  60. meerk40t/internal_plugins.py +4 -0
  61. meerk40t/kernel/kernel.py +39 -18
  62. meerk40t/kernel/settings.py +28 -9
  63. meerk40t/lihuiyu/device.py +24 -12
  64. meerk40t/main.py +1 -1
  65. meerk40t/moshi/device.py +20 -6
  66. meerk40t/network/console_server.py +22 -6
  67. meerk40t/newly/device.py +10 -3
  68. meerk40t/newly/gui/gui.py +10 -0
  69. meerk40t/ruida/device.py +22 -2
  70. meerk40t/ruida/loader.py +6 -3
  71. meerk40t/tools/geomstr.py +193 -125
  72. meerk40t/tools/rasterplotter.py +179 -93
  73. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/METADATA +1 -1
  74. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/RECORD +79 -78
  75. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/LICENSE +0 -0
  76. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/WHEEL +0 -0
  77. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/entry_points.txt +0 -0
  78. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/top_level.txt +0 -0
  79. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/zip-safe +0 -0
@@ -6,6 +6,8 @@ when the elements change. It will show the updated job.
6
6
 
7
7
  This job works as a spoolerjob. Implementing all the regular calls for being a spooled job.
8
8
  """
9
+
10
+ import threading
9
11
  import time
10
12
  from math import isinf
11
13
 
@@ -13,6 +15,7 @@ import numpy as np
13
15
 
14
16
  from meerk40t.core.node.node import Node
15
17
  from meerk40t.core.units import UNITS_PER_PIXEL, Length
18
+ from meerk40t.kernel.jobs import Job
16
19
  from meerk40t.svgelements import Matrix
17
20
  from meerk40t.tools.geomstr import Geomstr
18
21
 
@@ -25,50 +28,60 @@ class LiveLightJob:
25
28
  geometry=None,
26
29
  travel_speed=None,
27
30
  jump_delay=None,
28
- quantization=50,
31
+ quantization=100,
29
32
  listen=True,
30
33
  raw=False,
31
34
  ):
32
35
  self.service = service
36
+ self.quantization = quantization
37
+ self.mode = mode
38
+ self.listen = listen
39
+ self.raw = raw
40
+ self._geometry = geometry
41
+ self._travel_speed = travel_speed
42
+ self._jump_delay = jump_delay
43
+
44
+ # Kernel Job definition
45
+ self.redlight_lock = threading.RLock()
46
+ self.redlight_job = Job(
47
+ self.jobevent,
48
+ interval=0.1,
49
+ job_name=f"redlight_{self.mode}_{time.perf_counter():3f}",
50
+ run_main=False,
51
+ )
52
+
53
+ # Spooler-Job mirroring
33
54
  self.stopped = False
34
55
  self.started = False
35
- self.changed = False
36
- self._last_bounds = None
37
56
  self.priority = -1
38
57
  self.time_submitted = time.time()
39
58
  self.time_started = time.time()
40
59
  self.runtime = 0
41
60
 
42
- self.quantization = quantization
43
- self.mode = mode
61
+ # Update logic
62
+ self._connection = None
63
+ self.update_method = None
64
+ self.changed = False
44
65
  self.points = None
45
- self.source = "elements"
46
- if self.mode == "full":
47
- self.label = "Live Full Light Job"
48
- self._mode_light = self._full
49
- elif self.mode == "bounds":
50
- self.label = "Live Selection Light Job"
51
- self._mode_light = self._bounds
52
- elif self.mode == "crosshair":
53
- self.label = "Simple Crosshairs"
54
- self._mode_light = self._crosshairs
55
- # elif self.mode == "regmarks":
56
- # self.label = "Live Regmark Light Job"
57
- # self._mode_light = self._regmarks
58
- elif self.mode == "hull":
59
- self.label = "Live Hull Light Job"
60
- self._mode_light = self._hull
61
- elif self.mode == "geometry":
62
- self.label = "Element Light Job"
63
- self._mode_light = self._static
64
- else:
66
+ self.bounded = False
67
+
68
+ methods = {
69
+ "full": ("Full Light Job", self.update_full),
70
+ "hull": ("Hull Light Job", self.update_hull),
71
+ "bounds": ("Selection Light job", self.update_bounds),
72
+ "crosshair": ("Simple Crosshairs", self.update_crosshair),
73
+ "geometry": ("Element Light Job", self.update_geometry),
74
+ }
75
+ if self.mode not in methods:
65
76
  raise ValueError("Invalid mode.")
77
+ self.label, self.update_method = methods[self.mode]
78
+ if self.listen:
79
+ self.label = f"Live {self.label}"
66
80
 
67
- self.listen = listen
68
- self.raw = raw
69
- self._geometry = geometry
70
- self._travel_speed = travel_speed
71
- self._jump_delay = jump_delay
81
+ # Caching of geometry to be drawn
82
+ self.changed = True
83
+ self._last_bounds = None
84
+ self.source = "elements"
72
85
 
73
86
  @property
74
87
  def status(self):
@@ -82,182 +95,224 @@ class LiveLightJob:
82
95
  def is_running(self):
83
96
  return not self.stopped
84
97
 
85
- def execute(self, driver):
86
- """
87
- Spooler job execute.
98
+ def stop(self):
99
+ """Stop the spooler job.
88
100
 
89
- @param driver: driver-like object
90
- @return:
101
+ This function sets the 'stopped' flag to True, effectively
102
+ terminating the job's execution.
91
103
  """
92
- if self.stopped:
93
- return True
94
- if self.listen:
95
- self.service.listen("emphasized", self.on_emphasis_changed)
96
- self.service.listen("modified_by_tool", self.on_emphasis_changed)
97
- self.service.listen("updating", self.on_emphasis_changed)
98
- self.service.listen("view;realized", self.on_emphasis_changed)
99
- self.time_started = time.time()
100
- self.started = True
101
- connection = driver.connection
102
- connection.rapid_mode()
103
- connection.light_mode()
104
- while self.process(connection):
105
- # Calls process while execute() is running.
106
- if self.stopped:
107
- break
108
- connection.abort()
109
104
  self.stopped = True
110
- self.runtime += time.time() - self.time_started
111
- if self.listen:
112
- self.service.unlisten("emphasized", self.on_emphasis_changed)
113
- self.service.unlisten("modified_by_tool", self.on_emphasis_changed)
114
- self.service.unlisten("updating", self.on_emphasis_changed)
115
- self.service.unlisten("view;realized", self.on_emphasis_changed)
116
- self.service.signal("light_simulate", False)
117
- if self.service.redlight_preferred:
118
- connection.light_on()
119
- connection.write_port()
120
- else:
121
- connection.light_off()
122
- connection.write_port()
123
- return True
124
105
 
125
- def set_travel_speed(self, update_speed):
126
- self._travel_speed = update_speed
106
+ def elapsed_time(self):
107
+ """Calculate the elapsed time of the job.
127
108
 
128
- def stop(self):
129
- """
130
- Called in order to kill the spooler-job.
131
- @return:
132
- """
133
- self.stopped = True
109
+ Returns the runtime if available, otherwise calculates the
110
+ elapsed time since the job started.
134
111
 
135
- def elapsed_time(self):
136
- """
137
- How long is this job already running...
112
+ Returns:
113
+ float: The elapsed time in seconds.
138
114
  """
139
115
  result = 0
140
116
  if self.runtime != 0:
141
117
  result = self.runtime
142
- else:
143
- if self.is_running():
144
- result = time.time() - self.time_started
118
+ elif self.is_running():
119
+ result = time.time() - self.time_started
145
120
  return result
146
121
 
147
122
  def estimate_time(self):
148
- """
149
- Estimate how long this spooler job will require to complete.
150
- @return:
123
+ """Estimate the remaining time for the job to complete.
124
+
125
+ For this specific job, the estimation is always 0 as it's
126
+ a continuous live process.
127
+
128
+ Returns:
129
+ int: 0, representing no estimated time.
151
130
  """
152
131
  return 0
153
132
 
154
- def update(self):
155
- self.changed = True
156
- self.points = None
133
+ def set_travel_speed(self, update_speed):
134
+ self._travel_speed = update_speed
157
135
 
158
- def on_emphasis_changed(self, *args):
159
- """
160
- During execute the emphasis signal will call this function.
136
+ def execute(self, driver):
137
+ """Execute the spooler job.
161
138
 
162
- @param args:
163
- @return:
164
- """
165
- self.update()
139
+ This function runs the main loop of the job, continuously
140
+ updating and tracing the redlight until stopped.
166
141
 
167
- def process(self, con):
168
- """
169
- Called repeatedly by `execute()`
170
- @param con:
171
- @return:
142
+ Args:
143
+ driver: The driver-like object providing the connection.
144
+
145
+ Returns:
146
+ bool: True when the job completes (after being stopped).
172
147
  """
173
148
  if self.stopped:
174
- return False
175
- if self.listen:
176
- # Watch for changes.
177
- bounds = self.service.elements.selected_area()
178
- if bounds is None or isinf(bounds[0]):
179
- bounds = Node.union_bounds(
180
- list(self.service.elements.regmarks(emphasized=True))
181
- )
182
- if bounds is None or isinf(bounds[0]):
183
- bounds = Node.union_bounds(list(self.service.elements.elems()))
184
- if self._last_bounds is not None and bounds != self._last_bounds:
185
- # Emphasis did not change but the bounds did. We dragged something.
186
- self.changed = True
187
- self.points = None
188
- self._last_bounds = bounds
149
+ return True
150
+ self.pre_job(driver)
151
+ while not self.stopped:
152
+ time.sleep(0.05)
153
+ self.post_job(driver)
154
+ return True
189
155
 
190
- if self.changed:
191
- # The emphasis selection has changed.
192
- self.changed = False
156
+ def jobevent(self):
157
+ """Handle redlight job events.
158
+
159
+ This function is called periodically by the redlight job.
160
+ It updates the redlight path if necessary and then traces
161
+ the path with the laser.
162
+ """
163
+
164
+ def init_red(con):
193
165
  con.abort()
194
166
  first_x, first_y = con.get_last_xy()
195
167
  con.light_off()
196
168
  con.write_port()
197
169
  con.goto_xy(first_x, first_y, distance=0xFFFF)
170
+ if self._travel_speed is not None:
171
+ con._light_speed = self._travel_speed
172
+ con._dark_speed = self._travel_speed
173
+ con._goto_speed = self._travel_speed
174
+ else:
175
+ con._light_speed = self.service.redlight_speed
176
+ con._dark_speed = self.service.redlight_speed
177
+ con._goto_speed = self.service.redlight_speed
198
178
  con.light_mode()
199
179
 
200
- if self._travel_speed is not None:
201
- con._light_speed = self._travel_speed
202
- con._dark_speed = self._travel_speed
203
- con._goto_speed = self._travel_speed
204
- else:
205
- con._light_speed = self.service.redlight_speed
206
- con._dark_speed = self.service.redlight_speed
207
- con._goto_speed = self.service.redlight_speed
180
+ if self.stopped or self._connection is None:
181
+ return
182
+ con = self._connection
183
+ if self.changed:
184
+ # print ("Something changed")
185
+ with self.redlight_lock:
186
+ if self.update_method is not None:
187
+ self.update_method()
188
+ # print (f"We are having now {len(self.points)} points")
189
+ self.changed = False
190
+ init_red(con)
191
+
192
+ # Now draw the stuff
193
+ self.trace_redlight(con)
194
+
195
+ def trace_redlight(self, con):
196
+ """Trace the redlight path.
197
+
198
+ This function iterates through the calculated redlight points
199
+ and sends commands to the controller to move the laser
200
+ accordingly, turning the laser on and off to trace the path.
201
+
202
+ Args:
203
+ con: The connection to the laser controller.
204
+ """
208
205
  con.light_mode()
209
- # Calls light based on the set mode.
210
- return self._mode_light(con)
206
+ delay_dark = self.service.delay_jump_long
207
+ delay_between = self.service.delay_jump_short
208
+ move = True
209
+ for i, e in enumerate(self.points):
210
+ if self.stopped or self.changed:
211
+ # Abort due to stoppage or change, no sense to continue
212
+ return
213
+ if e is None:
214
+ move = True
215
+ continue
216
+ x, y = e.real, e.imag
217
+ if np.isnan(x) or np.isnan(y):
218
+ move = True
219
+ continue
220
+ x = int(x)
221
+ y = int(y)
222
+ if x < 0 or x > 0xFFFF or y < 0 or y > 0xFFFF:
223
+ # Our bounds are not in frame.
224
+ if self.bounded:
225
+ # We required them in frame.
226
+ continue
227
+ # Fix them.
228
+ x &= 0xFFFF
229
+ y &= 0xFFFF
230
+ if move:
231
+ con.dark(x, y, long=delay_dark, short=delay_dark)
232
+ move = False
233
+ continue
234
+ con.light(x, y, long=delay_between, short=delay_between)
235
+ con.light_off()
236
+ con.write_port()
237
+
238
+ def setup_listen(self, start):
239
+ """Set up or tear down listeners for element changes.
211
240
 
212
- # def _regmarks(self, con):
213
- # """
214
- # Mode light regmarks gets the elements for regmarks. Sends to light elements.
241
+ Args:
242
+ start (bool): True to start listening, False to stop.
243
+ """
244
+ if not self.listen:
245
+ return
246
+ for method in ("emphasized", "modified_by_tool", "updating", "view;realized"):
247
+ if start:
248
+ self.service.listen(method, self.on_emphasis_changed)
249
+ else:
250
+ self.service.unlisten(method, self.on_emphasis_changed)
215
251
 
216
- # @param con: connection
217
- # @return:
218
- # """
219
- # elements = list(self.service.elements.regmarks(emphasized=True))
220
- # if len(elements) == 0:
221
- # elements = list(self.service.elements.regmarks())
222
- # return self._light_elements(con, elements)
252
+ def pre_job(self, driver):
253
+ """Perform pre-job setup.
223
254
 
224
- def _gather_source(self):
225
- self.source = "elements"
226
- elements = list(self.service.elements.elems(emphasized=True))
227
- if len(elements) == 0:
228
- elements = list(self.service.elements.regmarks(emphasized=True))
229
- self.source = "regmarks"
230
- if len(elements) == 0:
231
- elements = list(self.service.elements.elems())
232
- return elements
255
+ This function is called before the job starts executing.
256
+ It sets up listeners, initializes the connection, and
257
+ schedules the redlight job.
233
258
 
234
- def _full(self, con):
235
- """
236
- Mode light full gets the elements from the emphasized primary elements. Sends to light elements.
237
- @param con: connection
238
- @return:
259
+ Args:
260
+ driver: The driver-like object.
239
261
  """
240
- # Full was requested.
241
- elements = self._gather_source()
242
- return self._light_elements(con, elements)
262
+ self.setup_listen(True)
263
+ self.time_started = time.time()
264
+ self.started = True
265
+ self._connection = driver.connection
266
+ self._connection.rapid_mode()
267
+ self._connection.light_mode()
268
+ self.update()
269
+ self.service.kernel.schedule(self.redlight_job)
243
270
 
244
- def _hull(self, con):
245
- """
246
- Mode light hull gets the convex hull. Sends to light hull.
271
+ def post_job(self, driver):
272
+ """Perform post-job cleanup.
273
+
274
+ This function is called after the job finishes executing.
275
+ It stops listening, cancels the redlight job, and resets
276
+ the connection.
247
277
 
248
- @param con: connection
249
- @return:
278
+ Args:
279
+ driver: The driver-like object.
250
280
  """
251
- elements = self._gather_source()
252
- return self._light_hull(con, elements)
281
+ self.stopped = True
282
+ self.runtime += time.time() - self.time_started
283
+ self.redlight_job.cancel()
284
+ self.service.kernel.unschedule(self.redlight_job)
285
+ self.setup_listen(False)
286
+ if self._connection is not None:
287
+ self._connection.abort()
288
+ if self.service.redlight_preferred:
289
+ self._connection.light_on()
290
+ else:
291
+ self._connection.light_off()
292
+ self._connection.write_port()
293
+ self._connection = None
294
+ self.service.signal("light_simulate", False)
253
295
 
254
- def _crosshairs(self, con, margin=5000):
296
+ def update(self):
297
+ """Mark the redlight path as changed.
298
+
299
+ This function sets the 'changed' flag to True, indicating
300
+ that the redlight path needs to be recalculated.
255
301
  """
256
- Mode light crosshairs draws crosshairs. Sends to light geometry.
302
+ with self.redlight_lock:
303
+ self.changed = True
304
+
305
+ def on_emphasis_changed(self, *args):
306
+ """Handle emphasis changes.
257
307
 
258
- @param con: connection
259
- @return:
308
+ This function is called when the emphasis of elements changes.
309
+ It triggers an update of the redlight path.
260
310
  """
311
+ self.update()
312
+
313
+ def update_crosshair(self):
314
+ """Update the redlight path to display crosshairs. Fallback case when nohing can be displayed"""
315
+ margin = 5000
261
316
  geometry = Geomstr.lines(
262
317
  (0x8000, 0x8000),
263
318
  (0x8000 - margin, 0x8000),
@@ -269,52 +324,97 @@ class LiveLightJob:
269
324
  (0x8000, 0x8000 + margin),
270
325
  (0x8000, 0x8000),
271
326
  )
272
- rotate = self._redlight_adjust_matrix()
273
- geometry.transform(rotate)
327
+ self.prepare_redlight_point(geometry, False, "crosshair")
274
328
 
275
- return self._light_geometry(con, geometry)
276
-
277
- def _static(self, con):
329
+ def update_geometry(self):
330
+ """Update the redlight path based on the provided geometry. Static, won't be changed."""
331
+ if self._geometry is None:
332
+ self.update_crosshair()
333
+ return
278
334
  geometry = Geomstr(self._geometry)
279
- rotate = self._redlight_adjust_matrix()
280
- if not self.raw:
281
- geometry.transform(self.service.view.matrix)
282
- geometry.transform(rotate)
283
- return self._light_geometry(con, geometry)
335
+ self.prepare_redlight_point(geometry, not self.raw, "geometry")
284
336
 
285
- def _bounds(self, con):
286
- """
287
- Light the bound's geometry. Sends to light geometry.
337
+ def _update_common(self, method, source):
338
+ elems = self._gather_source()
339
+ if len(elems) == 0:
340
+ self.update_crosshair()
341
+ return
342
+ geometry = method(elems)
343
+ if geometry is None:
344
+ self.update_crosshair()
345
+ return
288
346
 
289
- @param con:
290
- @return:
291
- """
292
- bounds = self._last_bounds
293
- if not bounds:
294
- # If no bounds give crosshairs.
295
- return self._crosshairs(con)
296
- xmin, ymin, xmax, ymax = bounds
297
- geometry = Geomstr.lines(
298
- (xmin, ymin),
299
- (xmax, ymin),
300
- (xmax, ymax),
301
- (xmin, ymax),
302
- (xmin, ymin),
303
- )
304
- rotate = self._redlight_adjust_matrix()
305
- if not self.raw:
306
- geometry.transform(self.service.view.matrix)
307
- geometry.transform(rotate)
308
- return self._light_geometry(con, geometry, bounded=True)
347
+ self.prepare_redlight_point(geometry, True, source)
348
+
349
+ def update_bounds(self):
350
+ """Update the redlight path to outline the bounds of selected elements."""
351
+
352
+ def create_bounds_geometry(elemlist):
353
+ bounds = Node.union_bounds(elemlist)
354
+ if bounds is None or isinf(bounds[0]):
355
+ return None
356
+ xmin, ymin, xmax, ymax = bounds
357
+ return Geomstr.lines(
358
+ (xmin, ymin),
359
+ (xmax, ymin),
360
+ (xmax, ymax),
361
+ (xmin, ymax),
362
+ (xmin, ymin),
363
+ )
364
+
365
+ self._update_common(create_bounds_geometry, "bounds")
366
+
367
+ def update_hull(self):
368
+ """Update the redlight path to trace the convex hull of selected elements."""
369
+
370
+ def create_hull_geometry(elemlist):
371
+ geometry = Geomstr()
372
+ for node in elemlist:
373
+ try:
374
+ e = None
375
+ if hasattr(node, "convex_hull"):
376
+ e = node.convex_hull()
377
+ if e is None:
378
+ e = node.as_geometry()
379
+ except AttributeError:
380
+ continue
381
+ geometry.append(e)
382
+ # If not empty return hull
383
+ return None if geometry.index == 0 else Geomstr.hull(geometry, distance=500)
384
+
385
+ self._update_common(create_hull_geometry, "hull")
386
+
387
+ def update_full(self):
388
+ """Update the redlight path to trace the full geometry of selected elements."""
389
+
390
+ def create_full_geometry(elemlist):
391
+ geometry = Geomstr()
392
+ for node in elemlist:
393
+ try:
394
+ e = None
395
+ if e is None and hasattr(node, "convex_hull"):
396
+ e = node.convex_hull()
397
+ if e is None and hasattr(node, "as_geometry"):
398
+ e = node.as_geometry()
399
+ if e is None and hasattr(node, "bounds"):
400
+ nx, ny, mx, my = node.bounds
401
+ e = Geomstr.rect(nx, ny, mx - nx, my - ny)
402
+ except AttributeError:
403
+ continue
404
+ geometry.append(e)
405
+ return None if geometry.index == 0 else geometry
406
+
407
+ self._update_common(create_full_geometry, "full")
309
408
 
310
409
  def _redlight_adjust_matrix(self):
311
- """
312
- Calculate the redlight adjustment matrix which is the product of the redlight offset values and the
313
- redlight rotation value.
410
+ """Calculate the redlight adjustment matrix.
314
411
 
315
- @return:
316
- """
412
+ The matrix is calculated based on the redlight offset and
413
+ rotation values.
317
414
 
415
+ Returns:
416
+ Matrix: The redlight adjustment matrix.
417
+ """
318
418
  x_offset = float(
319
419
  Length(
320
420
  self.service.redlight_offset_x,
@@ -336,131 +436,48 @@ class LiveLightJob:
336
436
  redlight_adjust_matrix.post_translate(x_offset, y_offset)
337
437
  return redlight_adjust_matrix
338
438
 
339
- def _light_geometry(self, con, geometry, bounded=False):
340
- """
341
- Light the current geometry.
439
+ def prepare_redlight_point(self, draw_geometry, adjust, source):
440
+ """Prepare the redlight points for tracing.
342
441
 
343
- We abort quickly if self.stopped or self.changed is set.
442
+ This function takes the draw geometry, applies transformations
443
+ if necessary, and interpolates the geometry to generate a
444
+ list of points for the redlight to trace.
344
445
 
345
- @param con: connection
346
- @param geometry: geometry to light
347
- @param bounded: Require the geometry to be properly bounded.
348
- @return: True if we should continue, False if we should not.
446
+ Args:
447
+ draw_geometry (Geomstr): The geometry to trace.
448
+ adjust (bool): Whether to apply view transformations.
449
+ source (str): The source of the geometry.
349
450
  """
350
- delay_dark = self.service.delay_jump_long
351
- delay_between = self.service.delay_jump_short
352
-
353
- points = list(geometry.as_equal_interpolated_points(distance=self.quantization))
354
- move = True
355
- for i, e in enumerate(points):
356
- if self.stopped:
357
- # Abort due to stoppage.
358
- return False
359
- if self.changed:
360
- # Abort due to change.
361
- return True
362
- if e is None:
363
- move = True
364
- continue
365
- x, y = e.real, e.imag
366
- if np.isnan(x) or np.isnan(y):
367
- move = True
368
- continue
369
- x = int(x)
370
- y = int(y)
371
- if (0 > x or x > 0xFFFF) or (0 > y or y > 0xFFFF):
372
- # Our bounds are not in frame.
373
- if bounded:
374
- # We required them in frame.
375
- return self._crosshairs(con)
376
- else:
377
- # Fix them.
378
- x = x & 0xFFFF
379
- y = y & 0xFFFF
380
- if move:
381
- con.dark(x, y, long=delay_dark, short=delay_dark)
382
- move = False
383
- continue
384
- con.light(x, y, long=delay_between, short=delay_between)
385
- if con.light_off():
386
- con.list_write_port()
387
- return True
388
-
389
- def _light_elements(self, con, elements):
390
- """
391
- Light the given elements. The elements should be a node list with `as_geometry()` objects
392
- @param con:
393
- @param elements:
394
- @return:
395
- """
396
- geometry = Geomstr()
397
- for n in elements:
398
- e = None
399
- if hasattr(n, "convex_hull"):
400
- e = n.convex_hull()
401
- if e is None and hasattr(n, "as_geometry"):
402
- e = n.as_geometry()
403
-
404
- if e is not None:
405
- geometry.append(e)
406
- else:
407
- if hasattr(n, "as_image"):
408
- nx, ny, mx, my = n.bounds
409
- geometry.append(Geomstr.rect(nx, ny, mx - nx, my - ny))
410
- if not geometry:
411
- # There are no elements, return a default crosshair.
412
- return self._crosshairs(con)
413
-
414
- redlight_matrix = self._redlight_adjust_matrix()
415
- if self.stopped:
416
- return False
417
-
418
- if self.changed:
419
- return True
420
-
421
- # Move to device space.
422
- if not self.raw:
451
+ # draw_geometry.debug_me()
452
+ geometry = Geomstr(draw_geometry)
453
+ # print (f"Entered with {geometry.bbox()} ({geometry.index} segments [{source}])")
454
+ if adjust:
423
455
  geometry.transform(self.service.view.matrix)
456
+ # print (f"Adjusted to {geometry.bbox()}")
457
+ rotate = self._redlight_adjust_matrix()
458
+ geometry.transform(rotate)
459
+ self.points = list(
460
+ geometry.as_equal_interpolated_points(
461
+ distance=self.quantization, expand_lines=True
462
+ )
463
+ )
464
+ # print (f"Interpolation delivered: {len(self.points)} segments")
424
465
 
425
- # Add redlight adjustments within device space.
426
- geometry.transform(redlight_matrix)
427
-
428
- self._light_geometry(con, geometry)
429
- if con.light_off():
430
- con.list_write_port()
431
- return True
466
+ def _gather_source(self):
467
+ """Gather the source elements for redlight tracing.
432
468
 
433
- def _light_hull(self, con, elements):
434
- """
435
- Light the given elements convex hull.
469
+ This function determines the source elements based on emphasis
470
+ and returns them as a list.
436
471
 
437
- @param con:
438
- @param elements:
439
- @return:
472
+ Returns:
473
+ list: A list of source elements.
440
474
  """
475
+ self.source = "elements"
476
+ elements = list(self.service.elements.elems(emphasized=True))
441
477
  if not elements:
442
- # There are no elements, return a default crosshair.
443
- return self._crosshairs(con)
444
- if self.points is None:
445
- # Convert elements to geomstr
446
- geometry = Geomstr()
447
- for node in elements:
448
- try:
449
- e = None
450
- if hasattr(node, "convex_hull"):
451
- e = node.convex_hull()
452
- if e is None:
453
- e = node.as_geometry()
454
- except AttributeError:
455
- continue
456
- geometry.append(e)
457
-
458
- # Convert to hull.
459
- hull = Geomstr.hull(geometry, distance=500)
460
- if not self.raw:
461
- hull.transform(self.service.view.matrix)
462
- hull.transform(self._redlight_adjust_matrix())
463
- self.points = hull
464
-
465
- # Light geometry.
466
- return self._light_geometry(con, self.points)
478
+ elements = list(self.service.elements.regmarks(emphasized=True))
479
+ self.source = "regmarks"
480
+ # if not elements:
481
+ # elements = list(self.service.elements.elems())
482
+ # self.source = "elements"
483
+ return elements