meerk40t 0.9.7051__py2.py3-none-any.whl → 0.9.7900__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 (68) hide show
  1. meerk40t/balormk/controller.py +3 -3
  2. meerk40t/balormk/device.py +7 -0
  3. meerk40t/balormk/driver.py +23 -14
  4. meerk40t/balormk/galvo_commands.py +18 -3
  5. meerk40t/balormk/gui/balorconfig.py +6 -0
  6. meerk40t/balormk/livelightjob.py +36 -14
  7. meerk40t/camera/camera.py +1 -0
  8. meerk40t/camera/gui/camerapanel.py +154 -58
  9. meerk40t/camera/plugin.py +46 -5
  10. meerk40t/core/elements/branches.py +90 -20
  11. meerk40t/core/elements/elements.py +59 -37
  12. meerk40t/core/elements/trace.py +10 -6
  13. meerk40t/core/node/node.py +2 -0
  14. meerk40t/core/plotplanner.py +7 -4
  15. meerk40t/device/gui/defaultactions.py +78 -14
  16. meerk40t/dxf/dxf_io.py +42 -0
  17. meerk40t/grbl/controller.py +245 -35
  18. meerk40t/grbl/device.py +102 -26
  19. meerk40t/grbl/driver.py +8 -2
  20. meerk40t/grbl/gui/grblconfiguration.py +6 -0
  21. meerk40t/grbl/gui/grblcontroller.py +1 -1
  22. meerk40t/gui/about.py +7 -0
  23. meerk40t/gui/choicepropertypanel.py +20 -30
  24. meerk40t/gui/devicepanel.py +27 -16
  25. meerk40t/gui/icons.py +15 -0
  26. meerk40t/gui/laserpanel.py +102 -54
  27. meerk40t/gui/materialtest.py +10 -0
  28. meerk40t/gui/mkdebug.py +268 -9
  29. meerk40t/gui/navigationpanels.py +65 -7
  30. meerk40t/gui/propertypanels/operationpropertymain.py +185 -91
  31. meerk40t/gui/scenewidgets/elementswidget.py +7 -1
  32. meerk40t/gui/scenewidgets/selectionwidget.py +24 -9
  33. meerk40t/gui/simulation.py +1 -1
  34. meerk40t/gui/statusbarwidgets/shapepropwidget.py +50 -40
  35. meerk40t/gui/statusbarwidgets/statusbar.py +2 -2
  36. meerk40t/gui/toolwidgets/toolmeasure.py +1 -1
  37. meerk40t/gui/toolwidgets/toolnodeedit.py +4 -1
  38. meerk40t/gui/toolwidgets/tooltabedit.py +9 -7
  39. meerk40t/gui/wxmeerk40t.py +2 -0
  40. meerk40t/gui/wxmmain.py +23 -9
  41. meerk40t/gui/wxmribbon.py +36 -0
  42. meerk40t/gui/wxutils.py +66 -42
  43. meerk40t/kernel/inhibitor.py +120 -0
  44. meerk40t/kernel/kernel.py +38 -0
  45. meerk40t/lihuiyu/controller.py +33 -3
  46. meerk40t/lihuiyu/device.py +99 -4
  47. meerk40t/lihuiyu/driver.py +62 -5
  48. meerk40t/lihuiyu/gui/lhycontrollergui.py +69 -24
  49. meerk40t/lihuiyu/gui/lhydrivergui.py +6 -0
  50. meerk40t/lihuiyu/laserspeed.py +17 -10
  51. meerk40t/lihuiyu/parser.py +23 -0
  52. meerk40t/main.py +1 -1
  53. meerk40t/moshi/gui/moshidrivergui.py +7 -0
  54. meerk40t/newly/controller.py +3 -2
  55. meerk40t/newly/device.py +23 -2
  56. meerk40t/newly/driver.py +8 -3
  57. meerk40t/newly/gui/newlyconfig.py +7 -0
  58. meerk40t/ruida/gui/ruidaconfig.py +7 -0
  59. meerk40t/tools/geomstr.py +68 -48
  60. meerk40t/tools/rasterplotter.py +0 -5
  61. meerk40t/tools/ttfparser.py +155 -82
  62. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/METADATA +1 -1
  63. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/RECORD +68 -67
  64. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/LICENSE +0 -0
  65. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/WHEEL +0 -0
  66. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/entry_points.txt +0 -0
  67. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/top_level.txt +0 -0
  68. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/zip-safe +0 -0
meerk40t/camera/plugin.py CHANGED
@@ -15,13 +15,13 @@ def plugin(kernel, lifecycle=None):
15
15
  kernel.set_feature("camera")
16
16
  if lifecycle == "invalidate":
17
17
  try:
18
- import cv2
18
+ import cv2 # pylint: disable=unused-import
19
19
  except ImportError:
20
20
  print("OpenCV is not installed. Disabling Camera. Install with:")
21
21
  print("\tpip install opencv-python-headless")
22
22
  return True
23
23
  try:
24
- import numpy as np
24
+ import numpy as np # pylint: disable=unused-import
25
25
  except ImportError:
26
26
  print("Numpy is not installed. Disabling Camera.")
27
27
  return True
@@ -32,6 +32,12 @@ def plugin(kernel, lifecycle=None):
32
32
  kernel.register("camera-enabled", True)
33
33
  _ = kernel.translation
34
34
 
35
+ def get_camera_attribute(camera, attribute, default=None):
36
+ if isinstance(camera, int):
37
+ camera = f"camera/{camera}"
38
+ return kernel.read_persistent(str, camera, attribute, default) or default
39
+
40
+
35
41
  @kernel.console_option("width", "w", type=int, help="force the camera width")
36
42
  @kernel.console_option("height", "h", type=int, help="force the camera height")
37
43
  @kernel.console_option(
@@ -100,6 +106,32 @@ def plugin(kernel, lifecycle=None):
100
106
  data.set_uri(uri)
101
107
  return "camera", data
102
108
 
109
+ @kernel.console_argument("label", type=str)
110
+ @kernel.console_command(
111
+ "label", help="Set camera label", output_type="camera", input_type="camera"
112
+ )
113
+ def camera_label(
114
+ _,
115
+ channel,
116
+ data=None,
117
+ label=None,
118
+ **kwargs,
119
+ ):
120
+ if label is None:
121
+ channel(_("Current labels:"))
122
+ for d in kernel.section_startswith("camera/"):
123
+ # We read the persistent description instead of the context,
124
+ # as this might not be set.
125
+ desc = get_camera_attribute(d, "desc", "No description set")
126
+ channel(f"{d}: {desc}")
127
+ else:
128
+ data.desc = label
129
+ kernel.write_persistent(f"camera/{data.uri}", "desc", label)
130
+ # Alert UI to the change
131
+ kernel.signal("pane", "/", "")
132
+ kernel.signal("icon;label", "/", f"cam{data.uri}", label)
133
+ return "camera", data
134
+
103
135
  @kernel.console_command(
104
136
  "info", help="list camera info", output_type="camera", input_type="camera"
105
137
  )
@@ -110,9 +142,14 @@ def plugin(kernel, lifecycle=None):
110
142
  **kwargs,
111
143
  ):
112
144
  channel(_("Camera Information:"))
145
+ channel("Camera-Path\turi\tDescription")
146
+ channel("----\t---\t-----------")
113
147
  for d in kernel.section_startswith("camera/"):
114
- context = kernel.get_context(d)
115
- channel(f"{d}: {getattr(context, 'uri', '---')}")
148
+ # context = kernel.get_context(d)
149
+ # channel(f"{d}: {getattr(context, 'uri', '---')}")
150
+ camera_uri = get_camera_attribute(d, "uri", "No URI set")
151
+ camera_lbl = get_camera_attribute(d, "desc", "No description set")
152
+ channel(f"{d}:\t{camera_uri}\t{camera_lbl}")
116
153
  return "camera", data
117
154
 
118
155
  @kernel.console_command(
@@ -277,7 +314,11 @@ def plugin(kernel, lifecycle=None):
277
314
  input_type="camera",
278
315
  )
279
316
  def list_camera_resolutions(_, channel, data=None, **kwargs):
280
- channel(f"Available resolutions for camera #{data.uri}")
317
+ if data is None:
318
+ channel(_("No camera selected."))
319
+ return
320
+ lbl = f" ({data.desc})" if data.desc else ""
321
+ channel(f"Available resolutions for camera #{data.uri}{lbl}")
281
322
  info_array = data.guess_supported_resolutions()
282
323
  for width, height, description in info_array:
283
324
  channel (f"{width}x{height} - {description}")
@@ -144,6 +144,7 @@ Functions:
144
144
  import re
145
145
  from copy import copy
146
146
 
147
+ from meerk40t.core.elements.element_types import op_nodes
147
148
  from meerk40t.core.node.effect_hatch import HatchEffectNode
148
149
  from meerk40t.core.node.op_cut import CutOpNode
149
150
  from meerk40t.core.node.op_dots import DotsOpNode
@@ -153,8 +154,9 @@ from meerk40t.core.node.op_raster import RasterOpNode
153
154
  from meerk40t.core.units import Angle, Length
154
155
  from meerk40t.kernel import CommandSyntaxError
155
156
  from meerk40t.svgelements import Color, Matrix
156
- from meerk40t.core.elements.element_types import op_nodes
157
157
  from meerk40t.tools.geomstr import NON_GEOMETRY_TYPES
158
+
159
+
158
160
  def plugin(kernel, lifecycle=None):
159
161
  _ = kernel.translation
160
162
  if lifecycle == "postboot":
@@ -576,7 +578,6 @@ def init_commands(kernel):
576
578
  self.signal("refresh_scene", "Scene")
577
579
  return data_type, data
578
580
 
579
-
580
581
  @self.console_command(
581
582
  "empty",
582
583
  help=_("Remove all elements from provided operations"),
@@ -1336,10 +1337,28 @@ def init_commands(kernel):
1336
1337
  @self.console_command(
1337
1338
  "copy",
1338
1339
  help=_("Duplicate elements"),
1339
- input_type=("elements", "ops"),
1340
+ input_type=("elements", "ops", None),
1340
1341
  output_type=("elements", "ops"),
1341
1342
  )
1342
- def e_copy(data=None, data_type=None, post=None, dx=None, dy=None, copies=None, **kwargs):
1343
+ def e_copy(
1344
+ data=None, data_type=None, post=None, dx=None, dy=None, copies=None, **kwargs
1345
+ ):
1346
+ if data_type is None:
1347
+ if data is None:
1348
+ # Take tree selection for ops, scene selection for elements
1349
+ elemlist = list(self.elems(emphasized=True))
1350
+ if elemlist:
1351
+ data_type = "elements"
1352
+ data = list(self.elems(emphasized=True))
1353
+ else:
1354
+ data_type = "ops"
1355
+ data = list(self.ops(selected=True))
1356
+ else:
1357
+ # If data is given, we assume it is ops or elements
1358
+ if data[0].type.startswith("op ", "util "):
1359
+ data_type = "ops"
1360
+ else:
1361
+ data_type = "elements"
1343
1362
  if data is None:
1344
1363
  # Take tree selection for ops, scene selection for elements
1345
1364
  if data_type == "ops":
@@ -1646,19 +1665,44 @@ def init_commands(kernel):
1646
1665
  channel("----------")
1647
1666
  return "elements", data
1648
1667
 
1649
- @kernel.console_option("stitchtolerance", "s", type=Length, help=_("By default elements will be stitched together if they have common end/start points, this option allows to set a tolerance"))
1650
- @kernel.console_option("nostitch", "n", type=bool, action="store_true", help=_("By default elements will be stitched together if they have a common end/start point, this option prevents that and real subpaths will be created"))
1668
+ @kernel.console_option(
1669
+ "stitchtolerance",
1670
+ "s",
1671
+ type=Length,
1672
+ help=_(
1673
+ "By default elements will be stitched together if they have common end/start points, this option allows to set a tolerance"
1674
+ ),
1675
+ )
1676
+ @kernel.console_option(
1677
+ "nostitch",
1678
+ "n",
1679
+ type=bool,
1680
+ action="store_true",
1681
+ help=_(
1682
+ "By default elements will be stitched together if they have a common end/start point, this option prevents that and real subpaths will be created"
1683
+ ),
1684
+ )
1651
1685
  @self.console_command(
1652
1686
  "merge",
1653
1687
  help=_("merge elements"),
1654
1688
  input_type="elements",
1655
1689
  output_type="elements",
1656
1690
  )
1657
- def element_merge(command, channel, _, data=None, post=None, nostitch=None, stitchtolerance=None, **kwargs):
1691
+ def element_merge(
1692
+ command,
1693
+ channel,
1694
+ _,
1695
+ data=None,
1696
+ post=None,
1697
+ nostitch=None,
1698
+ stitchtolerance=None,
1699
+ **kwargs,
1700
+ ):
1658
1701
  """
1659
1702
  Merge combines the geometries of the inputs. This matters in some cases where fills are used. Such that two
1660
1703
  nested circles forms a toroid rather two independent circles.
1661
1704
  """
1705
+
1662
1706
  def set_nonset_attributes(node, e):
1663
1707
  try:
1664
1708
  if node.stroke is None:
@@ -1681,16 +1725,18 @@ def init_commands(kernel):
1681
1725
  def segtype(info):
1682
1726
  return int(info[2].real) & 0xFF
1683
1727
 
1684
- if segtype(aseg) not in NON_GEOMETRY_TYPES and segtype(bseg) not in NON_GEOMETRY_TYPES:
1728
+ if (
1729
+ segtype(aseg) not in NON_GEOMETRY_TYPES
1730
+ and segtype(bseg) not in NON_GEOMETRY_TYPES
1731
+ ):
1685
1732
  s1, _dummy2, _dummy3, _dummy4, e1 = aseg
1686
1733
  s2, _dummy2, _dummy3, _dummy4, e2 = bseg
1687
1734
  c1 = s1 if starta else e1
1688
1735
  c2 = s2 if startb else e2
1689
- if abs(c1 - c2) <= tolerance + 1E-6:
1736
+ if abs(c1 - c2) <= tolerance + 1e-6:
1690
1737
  return True
1691
1738
  return False
1692
1739
 
1693
-
1694
1740
  seg1_start = other.segments[0]
1695
1741
  seg1_end = other.segments[other.index - 1]
1696
1742
  seg2_start = path.segments[0]
@@ -1726,7 +1772,12 @@ def init_commands(kernel):
1726
1772
  orientation = False
1727
1773
  path_before = False
1728
1774
  separate = False
1729
- to_set_seg, to_set_idx, from_seg, from_idx = path.index - 1, 4, other.index - 1, 4
1775
+ to_set_seg, to_set_idx, from_seg, from_idx = (
1776
+ path.index - 1,
1777
+ 4,
1778
+ other.index - 1,
1779
+ 4,
1780
+ )
1730
1781
  else:
1731
1782
  # ignore, path after other, proper orientation, disjoint
1732
1783
  separate = True
@@ -1734,8 +1785,15 @@ def init_commands(kernel):
1734
1785
  path_before = False
1735
1786
  to_set_seg, to_set_idx, from_seg, from_idx = None, None, None, None
1736
1787
 
1737
- return separate, orientation, path_before, to_set_seg, to_set_idx, from_seg, from_idx
1738
-
1788
+ return (
1789
+ separate,
1790
+ orientation,
1791
+ path_before,
1792
+ to_set_seg,
1793
+ to_set_idx,
1794
+ from_seg,
1795
+ from_idx,
1796
+ )
1739
1797
 
1740
1798
  if nostitch is None:
1741
1799
  nostitch = False
@@ -1779,16 +1837,26 @@ def init_commands(kernel):
1779
1837
  else:
1780
1838
  other = node.geometry
1781
1839
 
1782
- separate, orientation, path_before, to_set_seg, to_set_idx, from_seg, from_idx = merge_paths(other, path, nostitch, tolerance)
1840
+ (
1841
+ separate,
1842
+ orientation,
1843
+ path_before,
1844
+ to_set_seg,
1845
+ to_set_idx,
1846
+ from_seg,
1847
+ from_idx,
1848
+ ) = merge_paths(other, path, nostitch, tolerance)
1783
1849
  if to_set_seg is not None:
1784
- path.segments[to_set_seg, to_set_idx] = other.segments[from_seg, from_idx]
1785
- actionstr = 'Insert' if path_before else 'Append'
1786
- typestr = 'regular' if orientation else 'reversed'
1850
+ path.segments[to_set_seg, to_set_idx] = other.segments[
1851
+ from_seg, from_idx
1852
+ ]
1853
+ actionstr = "Insert" if path_before else "Append"
1854
+ typestr = "regular" if orientation else "reversed"
1787
1855
  channel(f"{actionstr} a {typestr} path - separate: {separate}")
1788
1856
  if not orientation:
1789
1857
  path.reverse()
1790
1858
  if path_before:
1791
- node.geometry.insert(0, path.segments[:path.index])
1859
+ node.geometry.insert(0, path.segments[: path.index])
1792
1860
  else:
1793
1861
  node.geometry.append(path, end=separate)
1794
1862
 
@@ -1815,7 +1883,7 @@ def init_commands(kernel):
1815
1883
  return
1816
1884
  elements_nodes = []
1817
1885
  elems = []
1818
- groups= []
1886
+ groups = []
1819
1887
  # _("Break elements")
1820
1888
  with self.undoscope("Break elements"):
1821
1889
  for node in data:
@@ -1825,7 +1893,9 @@ def init_commands(kernel):
1825
1893
  if hasattr(node, attrib):
1826
1894
  oldval = getattr(node, attrib, None)
1827
1895
  node_attributes[attrib] = oldval
1828
- group_node = node.replace_node(type="group", label=node_label, expanded=True)
1896
+ group_node = node.replace_node(
1897
+ type="group", label=node_label, expanded=True
1898
+ )
1829
1899
  groups.append(group_node)
1830
1900
  try:
1831
1901
  if hasattr(node, "final_geometry"):
@@ -71,11 +71,11 @@ def plugin(kernel, lifecycle=None):
71
71
  placements,
72
72
  render,
73
73
  shapes,
74
+ testcases,
74
75
  trace,
75
76
  tree_commands,
76
77
  undo_redo,
77
78
  wordlist,
78
- testcases,
79
79
  )
80
80
 
81
81
  return [
@@ -218,9 +218,9 @@ def plugin(kernel, lifecycle=None):
218
218
  "default": True,
219
219
  "type": bool,
220
220
  "label": _("Track changes and allow undo"),
221
- "tip": _(
222
- "MK will save intermediate states to undo/redo changes") + "\n" +
223
- _("This may consume a significant amount of memory"),
221
+ "tip": _("MK will save intermediate states to undo/redo changes")
222
+ + "\n"
223
+ + _("This may consume a significant amount of memory"),
224
224
  "page": "Start",
225
225
  "section": "_60_Undo",
226
226
  "signals": "restart",
@@ -645,6 +645,7 @@ class Elemental(Service):
645
645
  self.remembered_keyhole_nodes = []
646
646
  self.setting(bool, "use_undo", True)
647
647
  self.setting(int, "undo_levels", 20)
648
+ self.setting(bool, "filenode_selection", False)
648
649
  undo_active = self.use_undo
649
650
  undo_levels = self.undo_levels
650
651
  self.undo = Undo(self, self._tree, active=undo_active, levels=undo_levels)
@@ -751,7 +752,7 @@ class Elemental(Service):
751
752
  self.signal(source)
752
753
 
753
754
  @contextlib.contextmanager
754
- def static(self, source:str):
755
+ def static(self, source: str):
755
756
  try:
756
757
  self.stop_updates(source, False)
757
758
  yield self
@@ -767,7 +768,7 @@ class Elemental(Service):
767
768
  self.do_undo = True
768
769
 
769
770
  @contextlib.contextmanager
770
- def undoscope(self, message:str, static:bool = True):
771
+ def undoscope(self, message: str, static: bool = True):
771
772
  busy = self.kernel.busyinfo
772
773
  busy.start(msg=self.kernel.translation(message))
773
774
  undo_active = self.do_undo
@@ -903,7 +904,10 @@ class Elemental(Service):
903
904
  child = child.node
904
905
  if getattr(child, "hidden", False):
905
906
  continue
906
- if hasattr(child, "affected_children") and len(child.affected_children()) == 0:
907
+ if (
908
+ hasattr(child, "affected_children")
909
+ and len(child.affected_children()) == 0
910
+ ):
907
911
  continue
908
912
  canburn = True
909
913
  break
@@ -1094,7 +1098,11 @@ class Elemental(Service):
1094
1098
  for ref in list(n._references):
1095
1099
  ref.remove_node()
1096
1100
  op_assign.drop(n, modify=True)
1097
- if impose == "to_elem" and target_color is not None and hasattr(n, attrib):
1101
+ if (
1102
+ impose == "to_elem"
1103
+ and target_color is not None
1104
+ and hasattr(n, attrib)
1105
+ ):
1098
1106
  setattr(n, attrib, target_color)
1099
1107
  if set_fill_to_none and hasattr(n, "fill"):
1100
1108
  n.fill = None
@@ -2316,10 +2324,11 @@ class Elemental(Service):
2316
2324
  with self.undoscope("Drag and drop"):
2317
2325
  for drag_node in data:
2318
2326
  to_be_refreshed.extend(drag_node.flat())
2319
- op_treatment = (
2320
- drop_node.type in op_parent_nodes and (
2321
- not drag_node.has_ancestor("branch reg") or
2322
- (drag_node.has_ancestor("branch reg") and self.allow_reg_to_op_dragging)
2327
+ op_treatment = drop_node.type in op_parent_nodes and (
2328
+ not drag_node.has_ancestor("branch reg")
2329
+ or (
2330
+ drag_node.has_ancestor("branch reg")
2331
+ and self.allow_reg_to_op_dragging
2323
2332
  )
2324
2333
  )
2325
2334
  if drop_node is drag_node:
@@ -2585,6 +2594,7 @@ class Elemental(Service):
2585
2594
  keep_old_selection=False,
2586
2595
  use_smallest=False,
2587
2596
  exit_over_selection=False,
2597
+ force_filenodes_too=False,
2588
2598
  ):
2589
2599
  def contains(box, x, y=None):
2590
2600
  if y is None:
@@ -2607,6 +2617,8 @@ class Elemental(Service):
2607
2617
  for node in self.elems(emphasized=True):
2608
2618
  e_list.append(node)
2609
2619
  for node in self.elems_nodes(emphasized=False):
2620
+ if not force_filenodes_too and node.type == "file":
2621
+ continue
2610
2622
  try:
2611
2623
  bounds = node.bounds
2612
2624
  except AttributeError:
@@ -2614,9 +2626,8 @@ class Elemental(Service):
2614
2626
  if hasattr(node, "hidden") and node.hidden:
2615
2627
  continue
2616
2628
  # Empty group / files may cause problems
2617
- if node.type in ("file", "group"):
2618
- if not node._children:
2619
- bounds = None
2629
+ if node.type in ("group", "file") and not node._children:
2630
+ bounds = None
2620
2631
  if bounds is None:
2621
2632
  continue
2622
2633
  if contains(bounds, position):
@@ -2715,7 +2726,11 @@ class Elemental(Service):
2715
2726
  def _get_next_auto_raster_count(operations):
2716
2727
  auto_raster_count = 0
2717
2728
  for op in operations:
2718
- if op.type == "op raster" and op.id is not None and op.id.startswith("AR#"):
2729
+ if (
2730
+ op.type == "op raster"
2731
+ and op.id is not None
2732
+ and op.id.startswith("AR#")
2733
+ ):
2719
2734
  try:
2720
2735
  used_id = int(op.id[3:])
2721
2736
  auto_raster_count = max(auto_raster_count, used_id)
@@ -2750,7 +2765,6 @@ class Elemental(Service):
2750
2765
  debug_set[opnode.type] = 0
2751
2766
  debug_set[opnode.type] = debug_set[opnode.type] + 1
2752
2767
 
2753
-
2754
2768
  if len(list(self.ops())) == 0 and not self.operation_default_empty:
2755
2769
  has_cut = False
2756
2770
  has_engrave = False
@@ -2796,7 +2810,7 @@ class Elemental(Service):
2796
2810
  continue
2797
2811
  classif_info = [False, False]
2798
2812
  # Even for fuzzy we check first a direct hit
2799
- fuzzy_param = (False, True) if fuzzy else (False, )
2813
+ fuzzy_param = (False, True) if fuzzy else (False,)
2800
2814
  do_stroke = True
2801
2815
  do_fill = True
2802
2816
  for tempfuzzy in fuzzy_param:
@@ -2854,24 +2868,27 @@ class Elemental(Service):
2854
2868
  classified = False
2855
2869
  classifying_op = None
2856
2870
  if (
2857
- self.classify_fill and
2858
- op.type=="op raster" and
2859
- hasattr(node, "fill") and node.fill is not None
2871
+ self.classify_fill
2872
+ and op.type == "op raster"
2873
+ and hasattr(node, "fill")
2874
+ and node.fill is not None
2860
2875
  ):
2861
2876
  # This is a special use case:
2862
2877
  # Usually we don't distinguish a fill color - all non-transparent objects
2863
2878
  # are assigned to a single raster operation.
2864
2879
  # If the classify_fill flag is set, then we will use the fill attribute
2865
2880
  # to look for / create a matching raster operation
2866
- raster_candidate = _select_raster_candidate(operations, node, fuzzydistance)
2881
+ raster_candidate = _select_raster_candidate(
2882
+ operations, node, fuzzydistance
2883
+ )
2867
2884
  if raster_candidate is None and self.classify_autogenerate:
2868
2885
  # We need to create one...
2869
2886
  auto_raster_count = _get_next_auto_raster_count(operations)
2870
2887
  raster_candidate = RasterOpNode(
2871
- id = f"AR#{auto_raster_count}",
2872
- label = f"Auto-Raster #{auto_raster_count}",
2873
- color = abs(node.fill),
2874
- output = True,
2888
+ id=f"AR#{auto_raster_count}",
2889
+ label=f"Auto-Raster #{auto_raster_count}",
2890
+ color=abs(node.fill),
2891
+ output=True,
2875
2892
  )
2876
2893
  add_op_function(raster_candidate)
2877
2894
  new_operations_added = True
@@ -2972,14 +2989,16 @@ class Elemental(Service):
2972
2989
  default_candidates = []
2973
2990
  for op in operations:
2974
2991
  if (
2975
- hasattr(op, "classify") and
2976
- getattr(op, "default", False) and
2977
- hasattr(op, "valid_node_for_reference") and
2978
- op.valid_node_for_reference(node)
2992
+ hasattr(op, "classify")
2993
+ and getattr(op, "default", False)
2994
+ and hasattr(op, "valid_node_for_reference")
2995
+ and op.valid_node_for_reference(node)
2979
2996
  ):
2980
2997
  default_candidates.append(op)
2981
2998
  if len(default_candidates) > 1 and debug:
2982
- debug(f"For node {node_desc} there were {len(default_candidates)} default operations available, nb the very first will be taken!")
2999
+ debug(
3000
+ f"For node {node_desc} there were {len(default_candidates)} default operations available, nb the very first will be taken!"
3001
+ )
2983
3002
  for op in default_candidates:
2984
3003
  classified, should_break, feedback = op.classify(
2985
3004
  node,
@@ -3097,7 +3116,7 @@ class Elemental(Service):
3097
3116
  and node.stroke is not None
3098
3117
  and node.stroke.argb is not None
3099
3118
  ):
3100
- fuzzy_param = (False, True) if fuzzy else (False, )
3119
+ fuzzy_param = (False, True) if fuzzy else (False,)
3101
3120
  was_classified = False
3102
3121
  for tempfuzzy in fuzzy_param:
3103
3122
  if debug:
@@ -3179,7 +3198,7 @@ class Elemental(Service):
3179
3198
  "white"
3180
3199
  ) == abs(node.fill)
3181
3200
  node_fill = Color("black") if is_black else abs(node.fill)
3182
- fuzzy_param = (False, True) if fuzzy else (False, )
3201
+ fuzzy_param = (False, True) if fuzzy else (False,)
3183
3202
  was_classified = False
3184
3203
  for tempfuzzy in fuzzy_param:
3185
3204
  if debug:
@@ -3215,14 +3234,18 @@ class Elemental(Service):
3215
3234
  and node.fill is not None
3216
3235
  and node.fill.argb is not None
3217
3236
  ):
3218
- default_color = abs(node.fill) if self.classify_fill else Color("black")
3237
+ default_color = (
3238
+ abs(node.fill) if self.classify_fill else Color("black")
3239
+ )
3219
3240
  default_id = "AR#1" if self.classify_fill else "R1"
3220
- default_label = "Auto-Raster #1" if self.classify_fill else "Standard-Raster"
3241
+ default_label = (
3242
+ "Auto-Raster #1" if self.classify_fill else "Standard-Raster"
3243
+ )
3221
3244
  op = RasterOpNode(
3222
3245
  id=default_id,
3223
3246
  label=default_label,
3224
3247
  color=default_color,
3225
- output = True,
3248
+ output=True,
3226
3249
  )
3227
3250
  stdops.append(op)
3228
3251
  if debug:
@@ -3291,7 +3314,6 @@ class Elemental(Service):
3291
3314
  op.add_reference(node)
3292
3315
  update_debug_set(debug_set, op)
3293
3316
 
3294
-
3295
3317
  self.remove_unused_default_copies()
3296
3318
  if debug:
3297
3319
  debug("Summary:")
@@ -31,6 +31,7 @@ from meerk40t.core.units import Length
31
31
  from meerk40t.svgelements import Circle, Path, Point, Polyline
32
32
  from meerk40t.tools.geomstr import Geomstr
33
33
 
34
+
34
35
  def plugin(kernel, lifecycle=None):
35
36
  _ = kernel.translation
36
37
  if lifecycle == "postboot":
@@ -317,6 +318,7 @@ def generate_hull_shape_hull(data):
317
318
  pts.append(pts[0]) # loop
318
319
  return pts
319
320
 
321
+
320
322
  """
321
323
  There is no need for shape_complex any more as the regular hull routine already uses interpolation
322
324
  def generate_hull_shape_complex(data, resolution=None):
@@ -356,6 +358,7 @@ def generate_hull_shape_complex(data, resolution=None):
356
358
  return hull
357
359
  """
358
360
 
361
+
359
362
  def generate_hull_shape_circle_data(data):
360
363
  pts = []
361
364
  for node in data:
@@ -476,9 +479,7 @@ def init_commands(kernel):
476
479
  method = method.lower()
477
480
  if method not in ("segment", "quick", "hull", "circle"):
478
481
  channel(
479
- _(
480
- "Invalid method, please use one of quick, hull, segment, circle."
481
- )
482
+ _("Invalid method, please use one of quick, hull, segment, circle.")
482
483
  )
483
484
  return
484
485
 
@@ -553,6 +554,9 @@ def init_commands(kernel):
553
554
  # Wait for some seconds
554
555
  yield "wait", 5000
555
556
 
557
+ if hasattr(_spooler.context, "pre_outline"):
558
+ yield from _spooler.context.pre_outline()
559
+
556
560
  yield "wait_finish"
557
561
  yield "rapid_mode"
558
562
  idx = 0
@@ -563,6 +567,8 @@ def init_commands(kernel):
563
567
  Length(amount=p[0]).length_mm,
564
568
  Length(amount=p[1]).length_mm,
565
569
  )
570
+ if hasattr(_spooler.context, "post_outline"):
571
+ yield from _spooler.context.post_outline()
566
572
 
567
573
  _spooler.laserjob(
568
574
  list(trace_hull(startmethod)), label=f"Trace Job: {method}", helper=True
@@ -598,9 +604,7 @@ def init_commands(kernel):
598
604
  method = method.lower()
599
605
  if not method in ("segment", "quick", "hull", "circle"):
600
606
  channel(
601
- _(
602
- "Invalid method, please use one of quick, hull, segment, circle."
603
- )
607
+ _("Invalid method, please use one of quick, hull, segment, circle.")
604
608
  )
605
609
  return
606
610
 
@@ -1282,6 +1282,8 @@ class Node:
1282
1282
  if keep_children is None:
1283
1283
  keep_children = False
1284
1284
  parent = self._parent
1285
+ if parent is None:
1286
+ raise ValueError(f"Cannot replace {self.type}-node without parent.")
1285
1287
  index = parent._children.index(self)
1286
1288
  parent._children.remove(self)
1287
1289
  self.notify_detached(self)
@@ -47,7 +47,7 @@ class PlotPlanner(Parameters):
47
47
  ppi=True,
48
48
  shift=True,
49
49
  group=True,
50
- require_uniform_movement = True,
50
+ require_uniform_movement=True,
51
51
  **kwargs,
52
52
  ):
53
53
  super().__init__(settings, **kwargs)
@@ -69,17 +69,21 @@ class PlotPlanner(Parameters):
69
69
 
70
70
  if single:
71
71
  self.single = Single(self)
72
- if ppi:
73
- self.ppi = PPI(self)
74
72
  if shift:
75
73
  self.shift = Shift(self)
76
74
  if group:
77
75
  self.group = Group(self)
76
+ self.set_ppi(ppi)
78
77
 
79
78
  self.pos_x = None
80
79
  self.pos_y = None
81
80
  self.settings_then_jog = False
82
81
 
82
+ def set_ppi(self, ppi):
83
+ self.ppi = None
84
+ if ppi:
85
+ self.ppi = PPI(self)
86
+
83
87
  def push(self, plot):
84
88
  self.abort = False
85
89
  self.queue.append(plot)
@@ -653,4 +657,3 @@ def grouped(plot):
653
657
  group_y = y
654
658
  # There are no more plots.
655
659
  yield group_x, group_y
656
-