meerk40t 0.9.7010__py2.py3-none-any.whl → 0.9.7030__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 (77) hide show
  1. meerk40t/balormk/galvo_commands.py +1 -2
  2. meerk40t/core/cutcode/cutcode.py +1 -1
  3. meerk40t/core/cutplan.py +70 -2
  4. meerk40t/core/elements/branches.py +18 -4
  5. meerk40t/core/elements/element_treeops.py +43 -7
  6. meerk40t/core/elements/elements.py +49 -63
  7. meerk40t/core/elements/grid.py +8 -1
  8. meerk40t/core/elements/offset_clpr.py +4 -3
  9. meerk40t/core/elements/offset_mk.py +2 -1
  10. meerk40t/core/elements/shapes.py +379 -260
  11. meerk40t/core/elements/testcases.py +105 -0
  12. meerk40t/core/node/node.py +6 -3
  13. meerk40t/core/node/op_cut.py +9 -8
  14. meerk40t/core/node/op_dots.py +8 -8
  15. meerk40t/core/node/op_engrave.py +7 -7
  16. meerk40t/core/node/op_raster.py +8 -8
  17. meerk40t/core/planner.py +23 -0
  18. meerk40t/core/undos.py +1 -1
  19. meerk40t/core/wordlist.py +1 -0
  20. meerk40t/dxf/dxf_io.py +6 -0
  21. meerk40t/extra/encode_detect.py +8 -2
  22. meerk40t/extra/hershey.py +2 -3
  23. meerk40t/extra/inkscape.py +3 -5
  24. meerk40t/extra/mk_potrace.py +1959 -0
  25. meerk40t/extra/outerworld.py +2 -3
  26. meerk40t/extra/param_functions.py +2 -2
  27. meerk40t/extra/potrace.py +14 -10
  28. meerk40t/grbl/device.py +4 -1
  29. meerk40t/grbl/gui/grblcontroller.py +2 -2
  30. meerk40t/grbl/interpreter.py +1 -1
  31. meerk40t/gui/about.py +3 -5
  32. meerk40t/gui/basicops.py +3 -3
  33. meerk40t/gui/busy.py +75 -13
  34. meerk40t/gui/choicepropertypanel.py +365 -379
  35. meerk40t/gui/consolepanel.py +3 -3
  36. meerk40t/gui/gui_mixins.py +4 -1
  37. meerk40t/gui/hersheymanager.py +13 -3
  38. meerk40t/gui/laserpanel.py +12 -7
  39. meerk40t/gui/materialmanager.py +33 -6
  40. meerk40t/gui/plugin.py +9 -3
  41. meerk40t/gui/propertypanels/operationpropertymain.py +1 -1
  42. meerk40t/gui/ribbon.py +4 -1
  43. meerk40t/gui/scene/widget.py +1 -1
  44. meerk40t/gui/scenewidgets/rectselectwidget.py +19 -16
  45. meerk40t/gui/scenewidgets/selectionwidget.py +26 -20
  46. meerk40t/gui/simpleui.py +13 -8
  47. meerk40t/gui/simulation.py +22 -2
  48. meerk40t/gui/spoolerpanel.py +8 -11
  49. meerk40t/gui/themes.py +7 -1
  50. meerk40t/gui/tips.py +2 -3
  51. meerk40t/gui/toolwidgets/toolmeasure.py +4 -1
  52. meerk40t/gui/wxmeerk40t.py +32 -3
  53. meerk40t/gui/wxmmain.py +72 -6
  54. meerk40t/gui/wxmscene.py +95 -6
  55. meerk40t/gui/wxmtree.py +17 -11
  56. meerk40t/gui/wxutils.py +1 -1
  57. meerk40t/image/imagetools.py +21 -6
  58. meerk40t/kernel/kernel.py +31 -6
  59. meerk40t/kernel/settings.py +2 -0
  60. meerk40t/lihuiyu/device.py +9 -3
  61. meerk40t/main.py +22 -5
  62. meerk40t/network/console_server.py +52 -14
  63. meerk40t/network/web_server.py +15 -1
  64. meerk40t/ruida/device.py +5 -1
  65. meerk40t/ruida/gui/gui.py +6 -6
  66. meerk40t/ruida/gui/ruidaoperationproperties.py +1 -10
  67. meerk40t/ruida/rdjob.py +3 -3
  68. meerk40t/tools/geomstr.py +88 -0
  69. meerk40t/tools/polybool.py +2 -1
  70. meerk40t/tools/shxparser.py +92 -34
  71. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/METADATA +1 -1
  72. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/RECORD +77 -75
  73. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/WHEEL +1 -1
  74. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/LICENSE +0 -0
  75. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/entry_points.txt +0 -0
  76. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.dist-info}/top_level.txt +0 -0
  77. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7030.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):
@@ -355,6 +355,7 @@ def init_commands(kernel):
355
355
  data = list(self.elems(emphasized=True))
356
356
 
357
357
  if len(data) == 0:
358
+ channel(_("No selected elements."))
358
359
  return
359
360
  for node in data:
360
361
  eparent = node.parent
@@ -751,152 +752,154 @@ def init_commands(kernel):
751
752
  e.lock = setval
752
753
  changed.append(e)
753
754
  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)
755
+ # _("Update property")
756
+ with self.undoscope("Update property"):
757
+ for e in data:
758
+ # dbg = ""
759
+ # if hasattr(e, "bounds"):
760
+ # bb = e.bounds
761
+ # dbg += (
762
+ # f"x:{Length(bb[0], digits=2).length_mm}, "
763
+ # + f"y:{Length(bb[1], digits=2).length_mm}, "
764
+ # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
765
+ # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
766
+ # )
767
+ # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
768
+ # print (f"Before: {dbg}")
769
+ if prop in ("x", "y"):
770
+ if not e.can_move(self.lock_allows_move):
771
+ channel(
772
+ _("Element can not be moved: {name}").format(name=str(e))
786
773
  )
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
774
+ continue
775
+ # We need to adjust the matrix
776
+ if hasattr(e, "bounds") and hasattr(e, "matrix"):
777
+ dx = 0
778
+ dy = 0
779
+ bb = e.bounds
780
+ if prop == "x":
781
+ dx = new_value - bb[0]
782
+ else:
783
+ dy = new_value - bb[1]
784
+ e.matrix.post_translate(dx, dy)
806
785
  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)
786
+ channel(
787
+ _("Element has no matrix to modify: {name}").format(
788
+ name=str(e)
789
+ )
813
790
  )
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)
791
+ continue
792
+ elif prop in ("width", "height"):
793
+ if new_value == 0:
794
+ channel(_("Can't set {field} to zero").format(field=prop))
795
+ continue
796
+ if hasattr(e, "can_scale") and not e.can_scale:
797
+ channel(
798
+ _("Element can not be scaled: {name}").format(name=str(e))
821
799
  )
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
800
+ continue
801
+ if hasattr(e, "matrix") and hasattr(e, "bounds"):
802
+ bb = e.bounds
803
+ sx = 1.0
804
+ sy = 1.0
805
+ wd = bb[2] - bb[0]
806
+ ht = bb[3] - bb[1]
807
+ if prop == "width":
808
+ sx = new_value / wd
837
809
  else:
810
+ sy = new_value / ht
811
+ e.matrix.post_scale(sx, sy)
812
+ else:
813
+ channel(
814
+ _("Element has no matrix to modify: {name}").format(
815
+ name=str(e)
816
+ )
817
+ )
818
+ continue
819
+ elif hasattr(e, prop):
820
+ if hasattr(e, "can_modify") and not e.can_modify:
821
+ channel(
822
+ _("Can't modify a locked element: {name}").format(
823
+ name=str(e)
824
+ )
825
+ )
826
+ continue
827
+ try:
828
+ oldval = getattr(e, prop)
829
+ if prevalidated:
838
830
  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
- )
831
+ else:
832
+ if oldval is not None:
833
+ proptype = type(oldval)
834
+ setval = proptype(new_value)
835
+ if isinstance(oldval, bool):
836
+ if new_value.lower() in ("1", "true"):
837
+ setval = True
838
+ elif new_value.lower() in ("0", "false"):
839
+ setval = False
840
+ else:
841
+ setval = new_value
842
+ setattr(e, prop, setval)
843
+ except TypeError:
844
+ channel(
845
+ _(
846
+ "Can't set '{val}' for {field} (invalid type, old={oldval})."
847
+ ).format(val=new_value, field=prop, oldval=oldval)
848
+ )
849
+ except ValueError:
850
+ channel(
851
+ _(
852
+ "Can't set '{val}' for {field} (invalid value, old={oldval})."
853
+ ).format(val=new_value, field=prop, oldval=oldval)
854
+ )
855
+ except AttributeError:
856
+ channel(
857
+ _(
858
+ "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
859
+ ).format(val=new_value, field=prop, oldval=oldval)
860
+ )
858
861
 
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)
862
+ if "font" in prop:
863
+ # We need to force a recalculation of the underlying wxfont property
864
+ if hasattr(e, "wxfont"):
865
+ delattr(e, "wxfont")
866
+ text_elems.append(e)
867
+ if prop in ("mktext", "mkfont"):
868
+ for property_op in self.kernel.lookup_all("path_updater/.*"):
869
+ property_op(self.kernel.root, e)
870
+ if prop in (
871
+ "dpi",
872
+ "dither",
873
+ "dither_type",
874
+ "invert",
875
+ "red",
876
+ "green",
877
+ "blue",
878
+ "lightness",
879
+ ):
880
+ # Images require some recalculation too
881
+ self.do_image_update(e)
879
882
 
880
- else:
881
- channel(
882
- _("Element {name} has no property {field}").format(
883
- name=str(e), field=prop
883
+ else:
884
+ channel(
885
+ _("Element {name} has no property {field}").format(
886
+ name=str(e), field=prop
887
+ )
884
888
  )
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)
889
+ continue
890
+ e.altered()
891
+ # dbg = ""
892
+ # if hasattr(e, "bounds"):
893
+ # bb = e.bounds
894
+ # dbg += (
895
+ # f"x:{Length(bb[0], digits=2).length_mm}, "
896
+ # + f"y:{Length(bb[1], digits=2).length_mm}, "
897
+ # + f"w:{Length(bb[2]-bb[0], digits=2).length_mm}, "
898
+ # + f"h:{Length(bb[3]-bb[1], digits=2).length_mm}, "
899
+ # )
900
+ # dbg += f"{prop}:{str(getattr(e, prop)) if hasattr(e, prop) else '--'}"
901
+ # print (f"After: {dbg}")
902
+ changed.append(e)
900
903
  if len(changed) > 0:
901
904
  if len(text_elems) > 0:
902
905
  # Recalculate bounds
@@ -981,61 +984,62 @@ def init_commands(kernel):
981
984
 
982
985
 
983
986
  changed = []
984
-
985
- for e in data:
986
- if hasattr(e, prop):
987
- if hasattr(e, "can_modify") and not e.can_modify:
988
- channel(
989
- _("Can't modify a locked element: {name}").format(
990
- name=str(e)
987
+ # _("Update property")
988
+ with self.undoscope("Update property"):
989
+ for e in data:
990
+ if hasattr(e, prop):
991
+ if hasattr(e, "can_modify") and not e.can_modify:
992
+ channel(
993
+ _("Can't modify a locked element: {name}").format(
994
+ name=str(e)
995
+ )
991
996
  )
992
- )
993
- 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:
997
+ continue
998
+ try:
999
+ oldval = getattr(e, prop)
1000
+ if prevalidated:
1008
1001
  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
- )
1002
+ else:
1003
+ if oldval is not None:
1004
+ proptype = type(oldval)
1005
+ setval = proptype(new_value)
1006
+ if isinstance(oldval, bool):
1007
+ if new_value.lower() in ("1", "true"):
1008
+ setval = True
1009
+ elif new_value.lower() in ("0", "false"):
1010
+ setval = False
1011
+ else:
1012
+ setval = new_value
1013
+ setattr(e, prop, setval)
1014
+ except TypeError:
1015
+ channel(
1016
+ _(
1017
+ "Can't set '{val}' for {field} (invalid type, old={oldval})."
1018
+ ).format(val=new_value, field=prop, oldval=oldval)
1019
+ )
1020
+ except ValueError:
1021
+ channel(
1022
+ _(
1023
+ "Can't set '{val}' for {field} (invalid value, old={oldval})."
1024
+ ).format(val=new_value, field=prop, oldval=oldval)
1025
+ )
1026
+ except AttributeError:
1027
+ channel(
1028
+ _(
1029
+ "Can't set '{val}' for {field} (incompatible attribute, old={oldval})."
1030
+ ).format(val=new_value, field=prop, oldval=oldval)
1031
+ )
1028
1032
 
1029
1033
 
1030
- else:
1031
- channel(
1032
- _("Operation {name} has no property {field}").format(
1033
- name=str(e), field=prop
1034
+ else:
1035
+ channel(
1036
+ _("Operation {name} has no property {field}").format(
1037
+ name=str(e), field=prop
1038
+ )
1034
1039
  )
1035
- )
1036
- continue
1037
- e.altered()
1038
- changed.append(e)
1040
+ continue
1041
+ e.altered()
1042
+ changed.append(e)
1039
1043
  if len(changed) > 0:
1040
1044
  self.signal("refresh_scene", "Scene")
1041
1045
  self.signal("element_property_update", changed)
@@ -1093,39 +1097,41 @@ def init_commands(kernel):
1093
1097
  method = "visvalingam"
1094
1098
  if tolerance is None:
1095
1099
  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
1100
+ # _("Simplify")
1101
+ with self.undoscope("Simplify"):
1102
+ for node in data:
1111
1103
  try:
1112
- sub_after = len(list(node.as_geometry().as_subpaths()))
1104
+ sub_before = len(list(node.as_geometry().as_subpaths()))
1113
1105
  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}%)"
1106
+ sub_before = 0
1107
+ if hasattr(node, "geometry"):
1108
+ geom = node.geometry
1109
+ seg_before = node.geometry.index
1110
+ if method == "douglaspeucker":
1111
+ node.geometry = geom.simplify(tolerance)
1112
+ else:
1113
+ # Let's try Visvalingam line simplification
1114
+ node.geometry = geom.simplify_geometry(threshold=tolerance)
1115
+ node.altered()
1116
+ seg_after = node.geometry.index
1117
+ try:
1118
+ sub_after = len(list(node.as_geometry().as_subpaths()))
1119
+ except AttributeError:
1120
+ sub_after = 0
1121
+ channel(
1122
+ f"Simplified {node.type} ({node.display_label()}), tolerance: {tolerance}={Length(tolerance, digits=4).length_mm})"
1123
+ )
1124
+ if seg_before:
1125
+ saving = f"({(seg_before - seg_after)/seg_before*100:.1f}%)"
1126
+ else:
1127
+ saving = ""
1128
+ channel(f"Subpaths before: {sub_before} to {sub_after}")
1129
+ channel(f"Segments before: {seg_before} to {seg_after} {saving}")
1130
+ data_changed.append(node)
1120
1131
  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
- )
1132
+ channel(
1133
+ f"Invalid node for simplify {node.type} ({node.display_label()})"
1134
+ )
1129
1135
  if len(data_changed) > 0:
1130
1136
  self.signal("element_property_update", data_changed)
1131
1137
  self.signal("refresh_scene", "Scene")
@@ -1353,25 +1359,26 @@ def init_commands(kernel):
1353
1359
  if len(data) == 0:
1354
1360
  channel(_("No selected elements."))
1355
1361
  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)
1362
+ # _("Set stroke-width")
1363
+ with self.undoscope("Set stroke-width"):
1364
+ for e in data:
1365
+ if hasattr(e, "lock") and e.lock:
1366
+ channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1367
+ continue
1368
+ e.stroke_width = stroke_width
1369
+ try:
1370
+ e.stroke_width_zero()
1371
+ except AttributeError:
1372
+ pass
1373
+ # No full modified required, we are effectively only adjusting
1374
+ # the painted_bounds
1375
+ e.translated(0, 0)
1368
1376
  self.signal("element_property_update", data)
1369
1377
  self.signal("refresh_scene", "Scene")
1370
1378
  return "elements", data
1371
1379
 
1372
1380
  @self.console_command(
1373
1381
  ("enable_stroke_scale", "disable_stroke_scale"),
1374
- help=_("stroke-width <length>"),
1375
1382
  input_type=(
1376
1383
  None,
1377
1384
  "elements",
@@ -1385,12 +1392,14 @@ def init_commands(kernel):
1385
1392
  if len(data) == 0:
1386
1393
  channel(_("No selected elements."))
1387
1394
  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()
1395
+ # _("Update stroke-scale")
1396
+ with self.undoscope("Update stroke-scale"):
1397
+ for e in data:
1398
+ if hasattr(e, "lock") and e.lock:
1399
+ channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1400
+ continue
1401
+ e.stroke_scaled = command == "enable_stroke_scale"
1402
+ e.altered()
1394
1403
  self.signal("element_property_update", data)
1395
1404
  self.signal("refresh_scene", "Scene")
1396
1405
  return "elements", data
@@ -1729,7 +1738,7 @@ def init_commands(kernel):
1729
1738
  # self.signal("rebuild_tree")
1730
1739
  self.signal("refresh_tree", apply)
1731
1740
  else:
1732
- self.signal("element_property_update", apply)
1741
+ self.signal("element_property_reload", apply)
1733
1742
  self.signal("refresh_scene", "Scene")
1734
1743
  return "elements", data
1735
1744
 
@@ -1963,28 +1972,30 @@ def init_commands(kernel):
1963
1972
  if cy is None:
1964
1973
  cy = (bounds[3] + bounds[1]) / 2.0
1965
1974
  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)
1975
+ # _("Rotate")
1976
+ with self.undoscope("Rotate"):
1977
+ try:
1978
+ if not absolute:
1979
+ for node in data:
1980
+ if hasattr(node, "lock") and node.lock:
1981
+ continue
1982
+ node.matrix.post_rotate(angle, cx, cy)
1983
+ node.modified()
1984
+ if hasattr(node, "update"):
1985
+ images.append(node)
1986
+ else:
1987
+ for node in data:
1988
+ if hasattr(node, "lock") and node.lock:
1989
+ continue
1990
+ start_angle = node.matrix.rotation
1991
+ node.matrix.post_rotate(angle - start_angle, cx, cy)
1992
+ node.modified()
1993
+ if hasattr(node, "update"):
1994
+ images.append(node)
1995
+ except ValueError:
1996
+ raise CommandSyntaxError
1997
+ for node in images:
1998
+ self.do_image_update(node)
1988
1999
 
1989
2000
  self.signal("refresh_scene", "Scene")
1990
2001
  return "elements", data
@@ -2615,4 +2626,112 @@ def init_commands(kernel):
2615
2626
  self.first_emphasized = None
2616
2627
  return "elements", data
2617
2628
 
2629
+ @self.console_argument("tolerance", type=str, help=_("Tolerance to stitch paths together"))
2630
+ @self.console_option("keep", "k", type=bool, action="store_true", default=False, help=_("Keep original paths"))
2631
+ @self.console_command(
2632
+ "stitch",
2633
+ help=_("stitch selected elements"),
2634
+ input_type=(None, "elements"),
2635
+ output_type="elements",
2636
+ )
2637
+ def stitched(command, channel, _, data=None, tolerance=None, keep=None, post=None, **kwargs):
2638
+ def _prepare_stitching_params(channel, data, tolerance, keep):
2639
+ if data is None:
2640
+ data = list(self.elems(emphasized=True))
2641
+ if len(data) == 0:
2642
+ channel("There is nothing to be stitched together")
2643
+ return data, tolerance, keep, False
2644
+ if keep is None:
2645
+ keep = False
2646
+ if tolerance is None:
2647
+ tolerance_val = 0
2648
+ else:
2649
+ try:
2650
+ tolerance_val = float(Length(tolerance))
2651
+ except ValueError as e:
2652
+ channel(f"Invalid tolerance value: {tolerance}")
2653
+ return data, tolerance, keep, False
2654
+ return data, tolerance_val, keep, True
2655
+
2656
+ def stitcheable_nodes(data, tolerance) -> list:
2657
+ out = []
2658
+ geoms = []
2659
+ # Store all geometries together with an indicator, to which node they belong
2660
+ for idx, node in enumerate(data):
2661
+ if not hasattr(node, "as_geometry"):
2662
+ continue
2663
+ for g1 in node.as_geometry().as_contiguous():
2664
+ geoms.append((idx, g1))
2665
+ for idx1, (nodeidx1, g1) in enumerate(geoms):
2666
+ for idx2 in range(idx1 + 1, len(geoms)):
2667
+ nodeidx2 = geoms[idx2][0]
2668
+ g2 = geoms[idx2][1]
2669
+ fp1 = g1.first_point
2670
+ fp2 = g2.first_point
2671
+ lp1 = g1.last_point
2672
+ lp2 = g2.last_point
2673
+ if (
2674
+ abs(lp1 - lp2) <= tolerance or
2675
+ abs(lp1 - fp2) <= tolerance or
2676
+ abs(fp1 - fp2) <= tolerance or
2677
+ abs(fp1 - lp2) <= tolerance
2678
+ ):
2679
+ if nodeidx1 not in out:
2680
+ out.append(nodeidx1)
2681
+ if nodeidx2 not in out:
2682
+ out.append(nodeidx2)
2683
+
2684
+ return [data[idx] for idx in out]
2685
+
2686
+ data, tolerance, keep, valid = _prepare_stitching_params(channel, data, tolerance, keep)
2687
+ if not valid:
2688
+ return
2689
+ s_data = stitcheable_nodes(data, tolerance)
2690
+ if not s_data:
2691
+ channel("No stitcheable nodes found")
2692
+ return
2693
+
2694
+ geoms = []
2695
+ data_out = []
2696
+ to_be_deleted = []
2697
+ # _("Stitch paths")
2698
+ with self.undoscope("Stitch paths"):
2699
+ default_stroke = None
2700
+ default_strokewidth = None
2701
+ default_fill = None
2702
+ for node in s_data:
2703
+ if hasattr(node, "as_geometry"):
2704
+ geom : Geomstr = node.as_geometry()
2705
+ geoms.extend(iter(geom.as_contiguous()))
2706
+ if default_stroke is None and hasattr(node, "stroke"):
2707
+ default_stroke = node.stroke
2708
+ if default_strokewidth is None and hasattr(node, "stroke_width"):
2709
+ default_strokewidth = node.stroke_width
2710
+ to_be_deleted.append(node)
2711
+ prev_len = len(geoms)
2712
+ if geoms:
2713
+ result = stitch_geometries(geoms, tolerance)
2714
+ if result is None:
2715
+ channel("Could not stitch anything")
2716
+ return
2717
+ if not keep:
2718
+ for node in to_be_deleted:
2719
+ node.remove_node()
2720
+ for idx, g in enumerate(result):
2721
+ node = self.elem_branch.add(
2722
+ label=f"Stitch # {idx + 1}",
2723
+ stroke=default_stroke,
2724
+ stroke_width=default_strokewidth,
2725
+ fill=default_fill,
2726
+ geometry=g,
2727
+ type="elem path",
2728
+ )
2729
+ data_out.append(node)
2730
+ new_len = len(data_out)
2731
+ channel(f"Sub-Paths before: {prev_len} -> consolidated to {new_len} sub-paths")
2732
+
2733
+ post.append(classify_new(data_out))
2734
+ self.set_emphasis(data_out)
2735
+ return "elements", data_out
2736
+
2618
2737
  # --------------------------- END COMMANDS ------------------------------