meerk40t 0.9.7020__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 (98) 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/cutcode/cutcode.py +1 -1
  14. meerk40t/core/cutplan.py +169 -43
  15. meerk40t/core/elements/element_treeops.py +444 -147
  16. meerk40t/core/elements/elements.py +100 -9
  17. meerk40t/core/elements/grid.py +8 -1
  18. meerk40t/core/elements/offset_mk.py +2 -1
  19. meerk40t/core/elements/shapes.py +618 -279
  20. meerk40t/core/elements/tree_commands.py +10 -5
  21. meerk40t/core/node/elem_ellipse.py +18 -8
  22. meerk40t/core/node/elem_image.py +51 -19
  23. meerk40t/core/node/elem_line.py +18 -8
  24. meerk40t/core/node/elem_path.py +18 -8
  25. meerk40t/core/node/elem_point.py +10 -4
  26. meerk40t/core/node/elem_polyline.py +19 -11
  27. meerk40t/core/node/elem_rect.py +18 -8
  28. meerk40t/core/node/elem_text.py +11 -5
  29. meerk40t/core/node/filenode.py +2 -8
  30. meerk40t/core/node/groupnode.py +11 -11
  31. meerk40t/core/node/image_processed.py +11 -5
  32. meerk40t/core/node/image_raster.py +11 -5
  33. meerk40t/core/node/node.py +70 -19
  34. meerk40t/core/node/refnode.py +2 -1
  35. meerk40t/core/planner.py +23 -0
  36. meerk40t/core/svg_io.py +91 -34
  37. meerk40t/core/undos.py +1 -1
  38. meerk40t/core/wordlist.py +1 -0
  39. meerk40t/device/dummydevice.py +7 -1
  40. meerk40t/dxf/dxf_io.py +6 -0
  41. meerk40t/extra/mk_potrace.py +1959 -0
  42. meerk40t/extra/param_functions.py +1 -1
  43. meerk40t/extra/potrace.py +14 -10
  44. meerk40t/extra/vtracer.py +222 -0
  45. meerk40t/grbl/device.py +81 -8
  46. meerk40t/grbl/interpreter.py +1 -1
  47. meerk40t/gui/about.py +21 -3
  48. meerk40t/gui/basicops.py +3 -3
  49. meerk40t/gui/choicepropertypanel.py +1 -4
  50. meerk40t/gui/devicepanel.py +20 -16
  51. meerk40t/gui/gui_mixins.py +8 -1
  52. meerk40t/gui/icons.py +330 -253
  53. meerk40t/gui/laserpanel.py +8 -3
  54. meerk40t/gui/laserrender.py +41 -21
  55. meerk40t/gui/magnetoptions.py +158 -65
  56. meerk40t/gui/materialtest.py +229 -39
  57. meerk40t/gui/navigationpanels.py +229 -24
  58. meerk40t/gui/propertypanels/hatchproperty.py +2 -0
  59. meerk40t/gui/propertypanels/imageproperty.py +160 -106
  60. meerk40t/gui/ribbon.py +6 -1
  61. meerk40t/gui/scenewidgets/gridwidget.py +29 -32
  62. meerk40t/gui/scenewidgets/rectselectwidget.py +190 -192
  63. meerk40t/gui/simulation.py +75 -77
  64. meerk40t/gui/spoolerpanel.py +6 -9
  65. meerk40t/gui/statusbarwidgets/defaultoperations.py +84 -48
  66. meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
  67. meerk40t/gui/themes.py +7 -1
  68. meerk40t/gui/tips.py +15 -1
  69. meerk40t/gui/toolwidgets/toolpointmove.py +3 -1
  70. meerk40t/gui/wxmeerk40t.py +26 -0
  71. meerk40t/gui/wxmmain.py +242 -114
  72. meerk40t/gui/wxmscene.py +180 -4
  73. meerk40t/gui/wxmtree.py +4 -2
  74. meerk40t/gui/wxutils.py +60 -15
  75. meerk40t/image/imagetools.py +130 -66
  76. meerk40t/internal_plugins.py +4 -0
  77. meerk40t/kernel/kernel.py +49 -22
  78. meerk40t/kernel/settings.py +29 -8
  79. meerk40t/lihuiyu/device.py +30 -12
  80. meerk40t/main.py +22 -5
  81. meerk40t/moshi/device.py +20 -6
  82. meerk40t/network/console_server.py +22 -6
  83. meerk40t/newly/device.py +10 -3
  84. meerk40t/newly/gui/gui.py +10 -0
  85. meerk40t/ruida/device.py +22 -2
  86. meerk40t/ruida/gui/gui.py +6 -6
  87. meerk40t/ruida/gui/ruidaoperationproperties.py +1 -10
  88. meerk40t/ruida/loader.py +6 -3
  89. meerk40t/ruida/rdjob.py +3 -3
  90. meerk40t/tools/geomstr.py +195 -39
  91. meerk40t/tools/rasterplotter.py +179 -93
  92. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/METADATA +1 -1
  93. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/RECORD +98 -96
  94. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/WHEEL +1 -1
  95. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/LICENSE +0 -0
  96. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/entry_points.txt +0 -0
  97. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/top_level.txt +0 -0
  98. {meerk40t-0.9.7020.dist-info → meerk40t-0.9.7040.dist-info}/zip-safe +0 -0
@@ -66,7 +66,7 @@ from meerk40t.svgelements import (
66
66
  Polygon,
67
67
  Polyline,
68
68
  )
69
- from meerk40t.tools.geomstr import Geomstr
69
+ from meerk40t.tools.geomstr import Geomstr, stitch_geometries
70
70
 
71
71
 
72
72
  def plugin(kernel, lifecycle=None):
@@ -181,12 +181,18 @@ def init_commands(kernel):
181
181
  @self.console_argument("x_pos", type=Length, help=_("X-coordinate of center"))
182
182
  @self.console_argument("y_pos", type=Length, help=_("Y-coordinate of center"))
183
183
  @self.console_argument("rx", type=Length, help=_("Primary radius of ellipse"))
184
- @self.console_argument("ry", type=Length, help=_("Secondary radius of ellipse (default equal to primary radius=circle)"))
185
- @self.console_argument("start_angle", type=Angle, help=_("Start angle of arc (default 0°)"))
186
- @self.console_argument("end_angle", type=Angle, help=_("End angle of arc (default 360°)"))
187
- @self.console_option(
188
- "rotation", "r", type=Angle, help=_("Rotation of arc")
184
+ @self.console_argument(
185
+ "ry",
186
+ type=Length,
187
+ help=_("Secondary radius of ellipse (default equal to primary radius=circle)"),
188
+ )
189
+ @self.console_argument(
190
+ "start_angle", type=Angle, help=_("Start angle of arc (default 0°)")
191
+ )
192
+ @self.console_argument(
193
+ "end_angle", type=Angle, help=_("End angle of arc (default 360°)")
189
194
  )
195
+ @self.console_option("rotation", "r", type=Angle, help=_("Rotation of arc"))
190
196
  @self.console_command(
191
197
  "arc",
192
198
  help=_("arc <cx> <cy> <rx> <ry> <start> <end>"),
@@ -195,7 +201,18 @@ def init_commands(kernel):
195
201
  all_arguments_required=True,
196
202
  )
197
203
  def element_arc(
198
- channel, _, x_pos, y_pos, rx, ry=None, start_angle=None, end_angle=None, rotation=None, data=None, post=None, **kwargs
204
+ channel,
205
+ _,
206
+ x_pos,
207
+ y_pos,
208
+ rx,
209
+ ry=None,
210
+ start_angle=None,
211
+ end_angle=None,
212
+ rotation=None,
213
+ data=None,
214
+ post=None,
215
+ **kwargs,
199
216
  ):
200
217
  if start_angle is None:
201
218
  start_angle = Angle("0deg")
@@ -211,7 +228,7 @@ def init_commands(kernel):
211
228
  cy = float(y_pos)
212
229
  geom = Geomstr()
213
230
  geom.arc_as_cubics(
214
- start_t=start_angle.radians,
231
+ start_t=start_angle.radians,
215
232
  end_t=end_angle.radians,
216
233
  rx=rx_val,
217
234
  ry=ry_val,
@@ -355,6 +372,7 @@ def init_commands(kernel):
355
372
  data = list(self.elems(emphasized=True))
356
373
 
357
374
  if len(data) == 0:
375
+ channel(_("No selected elements."))
358
376
  return
359
377
  for node in data:
360
378
  eparent = node.parent
@@ -655,7 +673,83 @@ def init_commands(kernel):
655
673
  height=500,
656
674
  )
657
675
  except Exception:
658
- pass # Not relevant...
676
+ pass # Not relevant...
677
+
678
+ @self.console_argument("prop", type=str, help=_("property to get"))
679
+ @self.console_command(
680
+ "property-get",
681
+ help=_("get property value"),
682
+ input_type=(
683
+ None,
684
+ "elements",
685
+ ),
686
+ output_type="elements",
687
+ )
688
+ def element_property_get(command, channel, _, data, post=None, prop=None, **kwargs):
689
+ def possible_representation(node, prop) -> str:
690
+ def simple_rep(prop, value):
691
+ if isinstance(value, (float, int)) and prop in (
692
+ "x",
693
+ "y",
694
+ "cx",
695
+ "cy",
696
+ "r",
697
+ "rx",
698
+ "ry",
699
+ ):
700
+ try:
701
+ s = Length(value).length_mm
702
+ return s
703
+ except ValueError:
704
+ pass
705
+ elif isinstance(value, Length):
706
+ return value.length_mm
707
+ elif isinstance(value, Angle):
708
+ return value.angle_degrees
709
+ elif isinstance(value, str):
710
+ return f"'{value}'"
711
+ return repr(value)
712
+
713
+ value = getattr(node, prop, None)
714
+ if isinstance(value, (str, float, int)):
715
+ return simple_rep(prop, value)
716
+ elif isinstance(value, (tuple, list)):
717
+ stuff = []
718
+ for v in value:
719
+ stuff.append(simple_rep("x", v))
720
+ return ",".join(stuff)
721
+ return simple_rep(prop, value)
722
+
723
+ if data is None:
724
+ data = list(self.elems(emphasized=True))
725
+ if len(data) == 0:
726
+ channel(_("No selected elements."))
727
+ return
728
+ if prop is None or (prop == "?"):
729
+ channel(_("You need to provide the property to get."))
730
+ identified = []
731
+ for op in data:
732
+ if op.type in identified:
733
+ continue
734
+ identified.append(op.type)
735
+ prop_str = f"{op.type} has the following properties:"
736
+ first = True
737
+ for d in op.__dict__:
738
+ if d.startswith("_"):
739
+ continue
740
+ prop_str = f"{prop_str}{'' if first else ','} {d}"
741
+ first = False
742
+ channel(prop_str)
743
+ return
744
+ for d in data:
745
+ if not hasattr(d, prop):
746
+ channel(
747
+ f"Node: {d.display_label()} (Type: {d.type}) has no property called '{prop}'"
748
+ )
749
+ else:
750
+ channel(
751
+ f"Node: {d.display_label()} (Type: {d.type}): {prop}={getattr(d, prop, '')} ({possible_representation(d, prop)})"
752
+ )
659
753
 
660
754
  @self.console_argument("prop", type=str, help=_("property to set"))
661
755
  @self.console_argument("new_value", type=str, help=_("new property value"))
@@ -680,7 +774,7 @@ def init_commands(kernel):
680
774
  if len(data) == 0:
681
775
  channel(_("No selected elements."))
682
776
  return
683
- if prop is None or (prop == "?" and new_value=="?"):
777
+ if prop is None or (prop == "?" and new_value == "?"):
684
778
  channel(_("You need to provide the property to set."))
685
779
  if prop == "?":
686
780
  identified = []
@@ -688,13 +782,17 @@ def init_commands(kernel):
688
782
  if op.type in identified:
689
783
  continue
690
784
  identified.append(op.type)
691
- prop_str = f"{op.type} has the following properties: "
785
+ prop_str = f"{op.type} has the following properties:"
786
+ first = True
692
787
  for d in op.__dict__:
693
788
  if d.startswith("_"):
694
789
  continue
695
- prop_str = f"{prop_str}, {d}"
790
+ prop_str = f"{prop_str}{'' if first else ','} {d}"
791
+ first = False
696
792
  channel(prop_str)
697
- channel ("Be careful what you do - this is a failsafe method to crash MeerK40t, burn down your house or whatever...")
793
+ channel(
794
+ "Be careful what you do - this is a failsafe method to crash MeerK40t, burn down your house or whatever..."
795
+ )
698
796
  return
699
797
  classify_required = False
700
798
  prop = prop.lower()
@@ -751,152 +849,160 @@ def init_commands(kernel):
751
849
  e.lock = setval
752
850
  changed.append(e)
753
851
  else:
754
- for e in data:
755
- # dbg = ""
756
- # if hasattr(e, "bounds"):
757
- # bb = e.bounds
758
- # dbg += (
759
- # f"x:{Length(bb[0], digits=2).length_mm}, "
760
- # + f"y:{Length(bb[1], digits=2).length_mm}, "
761
- # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
762
- # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
763
- # )
764
- # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
765
- # print (f"Before: {dbg}")
766
- if prop in ("x", "y"):
767
- if not e.can_move(self.lock_allows_move):
768
- channel(
769
- _("Element can not be moved: {name}").format(name=str(e))
770
- )
771
- continue
772
- # We need to adjust the matrix
773
- if hasattr(e, "bounds") and hasattr(e, "matrix"):
774
- dx = 0
775
- dy = 0
776
- bb = e.bounds
777
- if prop == "x":
778
- dx = new_value - bb[0]
779
- else:
780
- dy = new_value - bb[1]
781
- e.matrix.post_translate(dx, dy)
782
- else:
783
- channel(
784
- _("Element has no matrix to modify: {name}").format(
785
- name=str(e)
852
+ # _("Update property")
853
+ with self.undoscope("Update property"):
854
+ for e in data:
855
+ # dbg = ""
856
+ # if hasattr(e, "bounds"):
857
+ # bb = e.bounds
858
+ # dbg += (
859
+ # f"x:{Length(bb[0], digits=2).length_mm}, "
860
+ # + f"y:{Length(bb[1], digits=2).length_mm}, "
861
+ # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
862
+ # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
863
+ # )
864
+ # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
865
+ # print (f"Before: {dbg}")
866
+ if prop in ("x", "y"):
867
+ if not e.can_move(self.lock_allows_move):
868
+ channel(
869
+ _("Element can not be moved: {name}").format(
870
+ name=str(e)
871
+ )
786
872
  )
787
- )
788
- continue
789
- elif prop in ("width", "height"):
790
- if new_value == 0:
791
- channel(_("Can't set {field} to zero").format(field=prop))
792
- continue
793
- if hasattr(e, "can_scale") and not e.can_scale:
794
- channel(
795
- _("Element can not be scaled: {name}").format(name=str(e))
796
- )
797
- continue
798
- if hasattr(e, "matrix") and hasattr(e, "bounds"):
799
- bb = e.bounds
800
- sx = 1.0
801
- sy = 1.0
802
- wd = bb[2] - bb[0]
803
- ht = bb[3] - bb[1]
804
- if prop == "width":
805
- sx = new_value / wd
873
+ continue
874
+ # We need to adjust the matrix
875
+ if hasattr(e, "bounds") and hasattr(e, "matrix"):
876
+ dx = 0
877
+ dy = 0
878
+ bb = e.bounds
879
+ if prop == "x":
880
+ dx = new_value - bb[0]
881
+ else:
882
+ dy = new_value - bb[1]
883
+ e.matrix.post_translate(dx, dy)
806
884
  else:
807
- sy = new_value / ht
808
- e.matrix.post_scale(sx, sy)
809
- else:
810
- channel(
811
- _("Element has no matrix to modify: {name}").format(
812
- name=str(e)
885
+ channel(
886
+ _("Element has no matrix to modify: {name}").format(
887
+ name=str(e)
888
+ )
813
889
  )
814
- )
815
- continue
816
- elif hasattr(e, prop):
817
- if hasattr(e, "can_modify") and not e.can_modify:
818
- channel(
819
- _("Can't modify a locked element: {name}").format(
820
- name=str(e)
890
+ continue
891
+ elif prop in ("width", "height"):
892
+ if new_value == 0:
893
+ channel(_("Can't set {field} to zero").format(field=prop))
894
+ continue
895
+ if hasattr(e, "can_scale") and not e.can_scale:
896
+ channel(
897
+ _("Element can not be scaled: {name}").format(
898
+ name=str(e)
899
+ )
821
900
  )
822
- )
823
- continue
824
- try:
825
- oldval = getattr(e, prop)
826
- if prevalidated:
827
- setval = new_value
828
- else:
829
- if oldval is not None:
830
- proptype = type(oldval)
831
- setval = proptype(new_value)
832
- if isinstance(oldval, bool):
833
- if new_value.lower() in ("1", "true"):
834
- setval = True
835
- elif new_value.lower() in ("0", "false"):
836
- setval = False
901
+ continue
902
+ if hasattr(e, "matrix") and hasattr(e, "bounds"):
903
+ bb = e.bounds
904
+ sx = 1.0
905
+ sy = 1.0
906
+ wd = bb[2] - bb[0]
907
+ ht = bb[3] - bb[1]
908
+ if prop == "width":
909
+ sx = new_value / wd
837
910
  else:
911
+ sy = new_value / ht
912
+ e.matrix.post_scale(sx, sy)
913
+ else:
914
+ channel(
915
+ _("Element has no matrix to modify: {name}").format(
916
+ name=str(e)
917
+ )
918
+ )
919
+ continue
920
+ elif hasattr(e, prop):
921
+ if hasattr(e, "can_modify") and not e.can_modify:
922
+ channel(
923
+ _("Can't modify a locked element: {name}").format(
924
+ name=str(e)
925
+ )
926
+ )
927
+ continue
928
+ try:
929
+ oldval = getattr(e, prop)
930
+ if prevalidated:
838
931
  setval = new_value
839
- setattr(e, prop, setval)
840
- except TypeError:
841
- channel(
842
- _(
843
- "Can't set '{val}' for {field} (invalid type, old={oldval})."
844
- ).format(val=new_value, field=prop, oldval=oldval)
845
- )
846
- except ValueError:
847
- channel(
848
- _(
849
- "Can't set '{val}' for {field} (invalid value, old={oldval})."
850
- ).format(val=new_value, field=prop, oldval=oldval)
851
- )
852
- except AttributeError:
853
- channel(
854
- _(
855
- "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
856
- ).format(val=new_value, field=prop, oldval=oldval)
857
- )
932
+ else:
933
+ if oldval is not None:
934
+ proptype = type(oldval)
935
+ setval = proptype(new_value)
936
+ if isinstance(oldval, bool):
937
+ if new_value.lower() in ("1", "true"):
938
+ setval = True
939
+ elif new_value.lower() in ("0", "false"):
940
+ setval = False
941
+ else:
942
+ setval = new_value
943
+ setattr(e, prop, setval)
944
+ except TypeError:
945
+ channel(
946
+ _(
947
+ "Can't set '{val}' for {field} (invalid type, old={oldval})."
948
+ ).format(val=new_value, field=prop, oldval=oldval)
949
+ )
950
+ except ValueError:
951
+ channel(
952
+ _(
953
+ "Can't set '{val}' for {field} (invalid value, old={oldval})."
954
+ ).format(val=new_value, field=prop, oldval=oldval)
955
+ )
956
+ except AttributeError:
957
+ channel(
958
+ _(
959
+ "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
960
+ ).format(val=new_value, field=prop, oldval=oldval)
961
+ )
858
962
 
859
- if "font" in prop:
860
- # We need to force a recalculation of the underlying wxfont property
861
- if hasattr(e, "wxfont"):
862
- delattr(e, "wxfont")
863
- text_elems.append(e)
864
- if prop in ("mktext", "mkfont"):
865
- for property_op in self.kernel.lookup_all("path_updater/.*"):
866
- property_op(self.kernel.root, e)
867
- if prop in (
868
- "dpi",
869
- "dither",
870
- "dither_type",
871
- "invert",
872
- "red",
873
- "green",
874
- "blue",
875
- "lightness",
876
- ):
877
- # Images require some recalculation too
878
- self.do_image_update(e)
963
+ if "font" in prop:
964
+ # We need to force a recalculation of the underlying wxfont property
965
+ if hasattr(e, "wxfont"):
966
+ delattr(e, "wxfont")
967
+ text_elems.append(e)
968
+ if prop in ("mktext", "mkfont"):
969
+ for property_op in self.kernel.lookup_all(
970
+ "path_updater/.*"
971
+ ):
972
+ property_op(self.kernel.root, e)
973
+ if prop in (
974
+ "dpi",
975
+ "dither",
976
+ "dither_type",
977
+ "invert",
978
+ "red",
979
+ "green",
980
+ "blue",
981
+ "lightness",
982
+ ):
983
+ # Images require some recalculation too
984
+ self.do_image_update(e)
879
985
 
880
- else:
881
- channel(
882
- _("Element {name} has no property {field}").format(
883
- name=str(e), field=prop
986
+ else:
987
+ channel(
988
+ _("Element {name} has no property {field}").format(
989
+ name=str(e), field=prop
990
+ )
884
991
  )
885
- )
886
- continue
887
- e.altered()
888
- # dbg = ""
889
- # if hasattr(e, "bounds"):
890
- # bb = e.bounds
891
- # dbg += (
892
- # f"x:{Length(bb[0], digits=2).length_mm}, "
893
- # + f"y:{Length(bb[1], digits=2).length_mm}, "
894
- # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
895
- # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
896
- # )
897
- # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
898
- # print (f"After: {dbg}")
899
- changed.append(e)
992
+ continue
993
+ e.altered()
994
+ # dbg = ""
995
+ # if hasattr(e, "bounds"):
996
+ # bb = e.bounds
997
+ # dbg += (
998
+ # f"x:{Length(bb[0], digits=2).length_mm}, "
999
+ # + f"y:{Length(bb[1], digits=2).length_mm}, "
1000
+ # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
1001
+ # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
1002
+ # )
1003
+ # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
1004
+ # print (f"After: {dbg}")
1005
+ changed.append(e)
900
1006
  if len(changed) > 0:
901
1007
  if len(text_elems) > 0:
902
1008
  # Recalculate bounds
@@ -932,7 +1038,7 @@ def init_commands(kernel):
932
1038
  if not data:
933
1039
  channel(_("No selected operations."))
934
1040
  return
935
- if prop is None or (prop == "?" and new_value=="?"):
1041
+ if prop is None or (prop == "?" and new_value == "?"):
936
1042
  channel(_("You need to provide the property to set."))
937
1043
  if prop == "?":
938
1044
  identified = []
@@ -946,7 +1052,9 @@ def init_commands(kernel):
946
1052
  continue
947
1053
  prop_str = f"{prop_str}, {d}"
948
1054
  channel(prop_str)
949
- channel ("Be careful what you do - this is a failsafe method to crash MeerK40t, burn down your house or whatever...")
1055
+ channel(
1056
+ "Be careful what you do - this is a failsafe method to crash MeerK40t, burn down your house or whatever..."
1057
+ )
950
1058
  return
951
1059
  prop = prop.lower()
952
1060
  if len(new_value) == 0:
@@ -979,63 +1087,62 @@ def init_commands(kernel):
979
1087
  new_value = testval
980
1088
  prevalidated = True
981
1089
 
982
-
983
1090
  changed = []
1091
+ # _("Update property")
1092
+ with self.undoscope("Update property"):
1093
+ for e in data:
1094
+ if hasattr(e, prop):
1095
+ if hasattr(e, "can_modify") and not e.can_modify:
1096
+ channel(
1097
+ _("Can't modify a locked element: {name}").format(
1098
+ name=str(e)
1099
+ )
1100
+ )
1101
+ continue
1102
+ try:
1103
+ oldval = getattr(e, prop)
1104
+ if prevalidated:
1105
+ setval = new_value
1106
+ else:
1107
+ if oldval is not None:
1108
+ proptype = type(oldval)
1109
+ setval = proptype(new_value)
1110
+ if isinstance(oldval, bool):
1111
+ if new_value.lower() in ("1", "true"):
1112
+ setval = True
1113
+ elif new_value.lower() in ("0", "false"):
1114
+ setval = False
1115
+ else:
1116
+ setval = new_value
1117
+ setattr(e, prop, setval)
1118
+ except TypeError:
1119
+ channel(
1120
+ _(
1121
+ "Can't set '{val}' for {field} (invalid type, old={oldval})."
1122
+ ).format(val=new_value, field=prop, oldval=oldval)
1123
+ )
1124
+ except ValueError:
1125
+ channel(
1126
+ _(
1127
+ "Can't set '{val}' for {field} (invalid value, old={oldval})."
1128
+ ).format(val=new_value, field=prop, oldval=oldval)
1129
+ )
1130
+ except AttributeError:
1131
+ channel(
1132
+ _(
1133
+ "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
1134
+ ).format(val=new_value, field=prop, oldval=oldval)
1135
+ )
984
1136
 
985
- for e in data:
986
- if hasattr(e, prop):
987
- if hasattr(e, "can_modify") and not e.can_modify:
1137
+ else:
988
1138
  channel(
989
- _("Can't modify a locked element: {name}").format(
990
- name=str(e)
1139
+ _("Operation {name} has no property {field}").format(
1140
+ name=str(e), field=prop
991
1141
  )
992
1142
  )
993
1143
  continue
994
- try:
995
- oldval = getattr(e, prop)
996
- if prevalidated:
997
- setval = new_value
998
- else:
999
- if oldval is not None:
1000
- proptype = type(oldval)
1001
- setval = proptype(new_value)
1002
- if isinstance(oldval, bool):
1003
- if new_value.lower() in ("1", "true"):
1004
- setval = True
1005
- elif new_value.lower() in ("0", "false"):
1006
- setval = False
1007
- else:
1008
- setval = new_value
1009
- setattr(e, prop, setval)
1010
- except TypeError:
1011
- channel(
1012
- _(
1013
- "Can't set '{val}' for {field} (invalid type, old={oldval})."
1014
- ).format(val=new_value, field=prop, oldval=oldval)
1015
- )
1016
- except ValueError:
1017
- channel(
1018
- _(
1019
- "Can't set '{val}' for {field} (invalid value, old={oldval})."
1020
- ).format(val=new_value, field=prop, oldval=oldval)
1021
- )
1022
- except AttributeError:
1023
- channel(
1024
- _(
1025
- "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
1026
- ).format(val=new_value, field=prop, oldval=oldval)
1027
- )
1028
-
1029
-
1030
- else:
1031
- channel(
1032
- _("Operation {name} has no property {field}").format(
1033
- name=str(e), field=prop
1034
- )
1035
- )
1036
- continue
1037
- e.altered()
1038
- changed.append(e)
1144
+ e.altered()
1145
+ changed.append(e)
1039
1146
  if len(changed) > 0:
1040
1147
  self.signal("refresh_scene", "Scene")
1041
1148
  self.signal("element_property_update", changed)
@@ -1093,39 +1200,41 @@ def init_commands(kernel):
1093
1200
  method = "visvalingam"
1094
1201
  if tolerance is None:
1095
1202
  tolerance = 25 # About 1/1000 mil
1096
- for node in data:
1097
- try:
1098
- sub_before = len(list(node.as_geometry().as_subpaths()))
1099
- except AttributeError:
1100
- sub_before = 0
1101
- if hasattr(node, "geometry"):
1102
- geom = node.geometry
1103
- seg_before = node.geometry.index
1104
- if method == "douglaspeucker":
1105
- node.geometry = geom.simplify(tolerance)
1106
- else:
1107
- # Let's try Visvalingam line simplification
1108
- node.geometry = geom.simplify_geometry(threshold=tolerance)
1109
- node.altered()
1110
- seg_after = node.geometry.index
1203
+ # _("Simplify")
1204
+ with self.undoscope("Simplify"):
1205
+ for node in data:
1111
1206
  try:
1112
- sub_after = len(list(node.as_geometry().as_subpaths()))
1207
+ sub_before = len(list(node.as_geometry().as_subpaths()))
1113
1208
  except AttributeError:
1114
- sub_after = 0
1115
- channel(
1116
- f"Simplified {node.type} ({node.display_label()}), tolerance: {tolerance}={Length(tolerance, digits=4).length_mm})"
1117
- )
1118
- if seg_before:
1119
- saving = f"({(seg_before - seg_after)/seg_before*100:.1f}%)"
1209
+ sub_before = 0
1210
+ if hasattr(node, "geometry"):
1211
+ geom = node.geometry
1212
+ seg_before = node.geometry.index
1213
+ if method == "douglaspeucker":
1214
+ node.geometry = geom.simplify(tolerance)
1215
+ else:
1216
+ # Let's try Visvalingam line simplification
1217
+ node.geometry = geom.simplify_geometry(threshold=tolerance)
1218
+ node.altered()
1219
+ seg_after = node.geometry.index
1220
+ try:
1221
+ sub_after = len(list(node.as_geometry().as_subpaths()))
1222
+ except AttributeError:
1223
+ sub_after = 0
1224
+ channel(
1225
+ f"Simplified {node.type} ({node.display_label()}), tolerance: {tolerance}={Length(tolerance, digits=4).length_mm})"
1226
+ )
1227
+ if seg_before:
1228
+ saving = f"({(seg_before - seg_after) / seg_before * 100:.1f}%)"
1229
+ else:
1230
+ saving = ""
1231
+ channel(f"Subpaths before: {sub_before} to {sub_after}")
1232
+ channel(f"Segments before: {seg_before} to {seg_after} {saving}")
1233
+ data_changed.append(node)
1120
1234
  else:
1121
- saving = ""
1122
- channel(f"Subpaths before: {sub_before} to {sub_after}")
1123
- channel(f"Segments before: {seg_before} to {seg_after} {saving}")
1124
- data_changed.append(node)
1125
- else:
1126
- channel(
1127
- f"Invalid node for simplify {node.type} ({node.display_label()})"
1128
- )
1235
+ channel(
1236
+ f"Invalid node for simplify {node.type} ({node.display_label()})"
1237
+ )
1129
1238
  if len(data_changed) > 0:
1130
1239
  self.signal("element_property_update", data_changed)
1131
1240
  self.signal("refresh_scene", "Scene")
@@ -1353,25 +1462,28 @@ def init_commands(kernel):
1353
1462
  if len(data) == 0:
1354
1463
  channel(_("No selected elements."))
1355
1464
  return
1356
- for e in data:
1357
- if hasattr(e, "lock") and e.lock:
1358
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1359
- continue
1360
- e.stroke_width = stroke_width
1361
- try:
1362
- e.stroke_width_zero()
1363
- except AttributeError:
1364
- pass
1365
- # No full modified required, we are effectively only adjusting
1366
- # the painted_bounds
1367
- e.translated(0, 0)
1465
+ # _("Set stroke-width")
1466
+ with self.undoscope("Set stroke-width"):
1467
+ for e in data:
1468
+ if hasattr(e, "lock") and e.lock:
1469
+ channel(
1470
+ _("Can't modify a locked element: {name}").format(name=str(e))
1471
+ )
1472
+ continue
1473
+ e.stroke_width = stroke_width
1474
+ try:
1475
+ e.stroke_width_zero()
1476
+ except AttributeError:
1477
+ pass
1478
+ # No full modified required, we are effectively only adjusting
1479
+ # the painted_bounds
1480
+ e.translated(0, 0)
1368
1481
  self.signal("element_property_update", data)
1369
1482
  self.signal("refresh_scene", "Scene")
1370
1483
  return "elements", data
1371
1484
 
1372
1485
  @self.console_command(
1373
1486
  ("enable_stroke_scale", "disable_stroke_scale"),
1374
- help=_("stroke-width <length>"),
1375
1487
  input_type=(
1376
1488
  None,
1377
1489
  "elements",
@@ -1385,12 +1497,16 @@ def init_commands(kernel):
1385
1497
  if len(data) == 0:
1386
1498
  channel(_("No selected elements."))
1387
1499
  return
1388
- for e in data:
1389
- if hasattr(e, "lock") and e.lock:
1390
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1391
- continue
1392
- e.stroke_scaled = command == "enable_stroke_scale"
1393
- e.altered()
1500
+ # _("Update stroke-scale")
1501
+ with self.undoscope("Update stroke-scale"):
1502
+ for e in data:
1503
+ if hasattr(e, "lock") and e.lock:
1504
+ channel(
1505
+ _("Can't modify a locked element: {name}").format(name=str(e))
1506
+ )
1507
+ continue
1508
+ e.stroke_scaled = command == "enable_stroke_scale"
1509
+ e.altered()
1394
1510
  self.signal("element_property_update", data)
1395
1511
  self.signal("refresh_scene", "Scene")
1396
1512
  return "elements", data
@@ -1691,7 +1807,9 @@ def init_commands(kernel):
1691
1807
  for e in apply:
1692
1808
  if hasattr(e, "lock") and e.lock:
1693
1809
  channel(
1694
- _("Can't modify a locked element: {name}").format(name=str(e))
1810
+ _("Can't modify a locked element: {name}").format(
1811
+ name=str(e)
1812
+ )
1695
1813
  )
1696
1814
  continue
1697
1815
  e.stroke = None
@@ -1703,7 +1821,9 @@ def init_commands(kernel):
1703
1821
  for e in apply:
1704
1822
  if hasattr(e, "lock") and e.lock:
1705
1823
  channel(
1706
- _("Can't modify a locked element: {name}").format(name=str(e))
1824
+ _("Can't modify a locked element: {name}").format(
1825
+ name=str(e)
1826
+ )
1707
1827
  )
1708
1828
  continue
1709
1829
  e.stroke = Color(color)
@@ -1794,13 +1914,14 @@ def init_commands(kernel):
1794
1914
  return "elements", data
1795
1915
  # _("Set fill")
1796
1916
  with self.undoscope("Set fill"):
1797
-
1798
1917
  if color == "none":
1799
1918
  self.set_start_time("fill")
1800
1919
  for e in apply:
1801
1920
  if hasattr(e, "lock") and e.lock:
1802
1921
  channel(
1803
- _("Can't modify a locked element: {name}").format(name=str(e))
1922
+ _("Can't modify a locked element: {name}").format(
1923
+ name=str(e)
1924
+ )
1804
1925
  )
1805
1926
  continue
1806
1927
  e.fill = None
@@ -1812,7 +1933,9 @@ def init_commands(kernel):
1812
1933
  for e in apply:
1813
1934
  if hasattr(e, "lock") and e.lock:
1814
1935
  channel(
1815
- _("Can't modify a locked element: {name}").format(name=str(e))
1936
+ _("Can't modify a locked element: {name}").format(
1937
+ name=str(e)
1938
+ )
1816
1939
  )
1817
1940
  continue
1818
1941
  e.fill = Color(color)
@@ -1963,28 +2086,30 @@ def init_commands(kernel):
1963
2086
  if cy is None:
1964
2087
  cy = (bounds[3] + bounds[1]) / 2.0
1965
2088
  images = []
1966
- try:
1967
- if not absolute:
1968
- for node in data:
1969
- if hasattr(node, "lock") and node.lock:
1970
- continue
1971
- node.matrix.post_rotate(angle, cx, cy)
1972
- node.modified()
1973
- if hasattr(node, "update"):
1974
- images.append(node)
1975
- else:
1976
- for node in data:
1977
- if hasattr(node, "lock") and node.lock:
1978
- continue
1979
- start_angle = node.matrix.rotation
1980
- node.matrix.post_rotate(angle - start_angle, cx, cy)
1981
- node.modified()
1982
- if hasattr(node, "update"):
1983
- images.append(node)
1984
- except ValueError:
1985
- raise CommandSyntaxError
1986
- for node in images:
1987
- self.do_image_update(node)
2089
+ # _("Rotate")
2090
+ with self.undoscope("Rotate"):
2091
+ try:
2092
+ if not absolute:
2093
+ for node in data:
2094
+ if hasattr(node, "lock") and node.lock:
2095
+ continue
2096
+ node.matrix.post_rotate(angle, cx, cy)
2097
+ node.modified()
2098
+ if hasattr(node, "update"):
2099
+ images.append(node)
2100
+ else:
2101
+ for node in data:
2102
+ if hasattr(node, "lock") and node.lock:
2103
+ continue
2104
+ start_angle = node.matrix.rotation
2105
+ node.matrix.post_rotate(angle - start_angle, cx, cy)
2106
+ node.modified()
2107
+ if hasattr(node, "update"):
2108
+ images.append(node)
2109
+ except ValueError:
2110
+ raise CommandSyntaxError
2111
+ for node in images:
2112
+ self.do_image_update(node)
1988
2113
 
1989
2114
  self.signal("refresh_scene", "Scene")
1990
2115
  return "elements", data
@@ -2615,4 +2740,218 @@ def init_commands(kernel):
2615
2740
  self.first_emphasized = None
2616
2741
  return "elements", data
2617
2742
 
2743
+ @self.console_argument(
2744
+ "tolerance", type=str, help=_("Tolerance to stitch paths together")
2745
+ )
2746
+ @self.console_option(
2747
+ "keep",
2748
+ "k",
2749
+ type=bool,
2750
+ action="store_true",
2751
+ default=False,
2752
+ help=_("Keep original paths"),
2753
+ )
2754
+ @self.console_command(
2755
+ "stitch",
2756
+ help=_("stitch selected elements"),
2757
+ input_type=(None, "elements"),
2758
+ output_type="elements",
2759
+ )
2760
+ def stitched(
2761
+ command, channel, _, data=None, tolerance=None, keep=None, post=None, **kwargs
2762
+ ):
2763
+ def _prepare_stitching_params(channel, data, tolerance, keep):
2764
+ if data is None:
2765
+ data = list(self.elems(emphasized=True))
2766
+ if len(data) == 0:
2767
+ channel("There is nothing to be stitched together")
2768
+ return data, tolerance, keep, False
2769
+ if keep is None:
2770
+ keep = False
2771
+ if tolerance is None:
2772
+ tolerance_val = 0
2773
+ else:
2774
+ try:
2775
+ tolerance_val = float(Length(tolerance))
2776
+ except ValueError as e:
2777
+ channel(f"Invalid tolerance value: {tolerance}")
2778
+ return data, tolerance, keep, False
2779
+ return data, tolerance_val, keep, True
2780
+
2781
+ def stitcheable_nodes(data, tolerance) -> list:
2782
+ out = []
2783
+ geoms = []
2784
+ # Store all geometries together with an indicator, to which node they belong
2785
+ for idx, node in enumerate(data):
2786
+ if not hasattr(node, "as_geometry"):
2787
+ continue
2788
+ for g1 in node.as_geometry().as_contiguous():
2789
+ geoms.append((idx, g1))
2790
+ if tolerance == 0:
2791
+ tolerance = 1e-6
2792
+ for idx1, (nodeidx1, g1) in enumerate(geoms):
2793
+ for idx2 in range(idx1 + 1, len(geoms)):
2794
+ nodeidx2 = geoms[idx2][0]
2795
+ g2 = geoms[idx2][1]
2796
+ fp1 = g1.first_point
2797
+ fp2 = g2.first_point
2798
+ lp1 = g1.last_point
2799
+ lp2 = g2.last_point
2800
+ if (
2801
+ abs(lp1 - lp2) <= tolerance
2802
+ or abs(lp1 - fp2) <= tolerance
2803
+ or abs(fp1 - fp2) <= tolerance
2804
+ or abs(fp1 - lp2) <= tolerance
2805
+ ):
2806
+ if nodeidx1 not in out:
2807
+ out.append(nodeidx1)
2808
+ if nodeidx2 not in out:
2809
+ out.append(nodeidx2)
2810
+
2811
+ return [data[idx] for idx in out]
2812
+
2813
+ data, tolerance, keep, valid = _prepare_stitching_params(
2814
+ channel, data, tolerance, keep
2815
+ )
2816
+ if not valid:
2817
+ return
2818
+ s_data = stitcheable_nodes(data, tolerance)
2819
+ if not s_data:
2820
+ channel("No stitcheable nodes found")
2821
+ return
2822
+
2823
+ geoms = []
2824
+ data_out = []
2825
+ to_be_deleted = []
2826
+ # _("Stitch paths")
2827
+ with self.undoscope("Stitch paths"):
2828
+ default_stroke = None
2829
+ default_strokewidth = None
2830
+ default_fill = None
2831
+ for node in s_data:
2832
+ if hasattr(node, "as_geometry"):
2833
+ geom: Geomstr = node.as_geometry()
2834
+ geoms.extend(iter(geom.as_contiguous()))
2835
+ if default_stroke is None and hasattr(node, "stroke"):
2836
+ default_stroke = node.stroke
2837
+ if default_strokewidth is None and hasattr(node, "stroke_width"):
2838
+ default_strokewidth = node.stroke_width
2839
+ to_be_deleted.append(node)
2840
+ prev_len = len(geoms)
2841
+ if geoms:
2842
+ result = stitch_geometries(geoms, tolerance)
2843
+ if result is None:
2844
+ channel("Could not stitch anything")
2845
+ return
2846
+ if not keep:
2847
+ for node in to_be_deleted:
2848
+ node.remove_node()
2849
+ for idx, g in enumerate(result):
2850
+ node = self.elem_branch.add(
2851
+ label=f"Stitch # {idx + 1}",
2852
+ stroke=default_stroke,
2853
+ stroke_width=default_strokewidth,
2854
+ fill=default_fill,
2855
+ geometry=g,
2856
+ type="elem path",
2857
+ )
2858
+ data_out.append(node)
2859
+ new_len = len(data_out)
2860
+ channel(
2861
+ f"Sub-Paths before: {prev_len} -> consolidated to {new_len} sub-paths"
2862
+ )
2863
+
2864
+ post.append(classify_new(data_out))
2865
+ self.set_emphasis(data_out)
2866
+ return "elements", data_out
2867
+
2868
+ @self.console_argument("xpos", type=Length, help=_("X-Position of cross center"))
2869
+ @self.console_argument("ypos", type=Length, help=_("Y-Position of cross center"))
2870
+ @self.console_argument("diameter", type=Length, help=_("Diameter of cross"))
2871
+ @self.console_option(
2872
+ "circle",
2873
+ "c",
2874
+ type=bool,
2875
+ action="store_true",
2876
+ default=False,
2877
+ help=_("Draw a circle around cross"),
2878
+ )
2879
+ @self.console_option(
2880
+ "diagonal",
2881
+ "d",
2882
+ type=bool,
2883
+ action="store_true",
2884
+ default=False,
2885
+ help=_("Draw the cross diagonally"),
2886
+ )
2887
+ @self.console_command(
2888
+ "cross",
2889
+ help=_("Create a small cross at the given position"),
2890
+ input_type=None,
2891
+ output_type="elements",
2892
+ )
2893
+ def cross(
2894
+ command,
2895
+ channel,
2896
+ _,
2897
+ data=None,
2898
+ xpos=None,
2899
+ ypos=None,
2900
+ diameter=None,
2901
+ circle=None,
2902
+ diagonal=None,
2903
+ post=None,
2904
+ **kwargs,
2905
+ ):
2906
+ if xpos is None or ypos is None or diameter is None:
2907
+ channel(_("You need to provide center-point and diameter: cross x y d"))
2908
+ return
2909
+ try:
2910
+ xp = float(xpos)
2911
+ yp = float(ypos)
2912
+ dia = float(diameter)
2913
+ except ValueError:
2914
+ channel(_("Invalid values given"))
2915
+ return
2916
+ if circle is None:
2917
+ circle = False
2918
+ if diagonal is None:
2919
+ diagonal = False
2920
+ geom = Geomstr()
2921
+ if diagonal:
2922
+ sincos45 = dia / 2 * sqrt(2) / 2
2923
+ geom.line(
2924
+ complex(xp - sincos45, yp - sincos45),
2925
+ complex(xp + sincos45, yp + sincos45),
2926
+ )
2927
+ geom.line(
2928
+ complex(xp + sincos45, yp - sincos45),
2929
+ complex(xp - sincos45, yp + sincos45),
2930
+ )
2931
+ else:
2932
+ geom.line(complex(xp - dia / 2, yp), complex(xp + dia / 2, yp))
2933
+ geom.line(complex(xp, yp - dia / 2), complex(xp, yp + dia / 2))
2934
+ if circle:
2935
+ geom.append(Geomstr.circle(dia / 2, xp, yp))
2936
+ # _("Create cross") - hint for translator
2937
+ with self.undoscope("Create cross"):
2938
+ node = self.elem_branch.add(
2939
+ label=_("Cross at ({xp}, {yp})").format(
2940
+ xp=xpos.length_mm, yp=ypos.length_mm
2941
+ ),
2942
+ geometry=geom,
2943
+ stroke=self.default_stroke,
2944
+ stroke_width=self.default_strokewidth,
2945
+ fill=None,
2946
+ type="elem path",
2947
+ )
2948
+ if data is None:
2949
+ data = []
2950
+ data.append(node)
2951
+
2952
+ # Newly created! Classification needed?
2953
+ post.append(classify_new(data))
2954
+ self.signal("refresh_scene", "Scene")
2955
+ return "elements", data
2956
+
2618
2957
  # --------------------------- END COMMANDS ------------------------------