meerk40t 0.9.7030__py2.py3-none-any.whl → 0.9.7040__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. meerk40t/balormk/clone_loader.py +3 -2
  2. meerk40t/balormk/controller.py +28 -11
  3. meerk40t/balormk/cylindermod.py +1 -0
  4. meerk40t/balormk/device.py +13 -9
  5. meerk40t/balormk/driver.py +9 -2
  6. meerk40t/balormk/galvo_commands.py +3 -1
  7. meerk40t/balormk/gui/gui.py +6 -0
  8. meerk40t/balormk/livelightjob.py +338 -321
  9. meerk40t/balormk/mock_connection.py +4 -3
  10. meerk40t/balormk/usb_connection.py +11 -2
  11. meerk40t/camera/camera.py +19 -14
  12. meerk40t/camera/gui/camerapanel.py +6 -0
  13. meerk40t/core/cutplan.py +109 -51
  14. meerk40t/core/elements/element_treeops.py +435 -140
  15. meerk40t/core/elements/elements.py +100 -9
  16. meerk40t/core/elements/shapes.py +259 -39
  17. meerk40t/core/elements/tree_commands.py +10 -5
  18. meerk40t/core/node/elem_ellipse.py +18 -8
  19. meerk40t/core/node/elem_image.py +51 -19
  20. meerk40t/core/node/elem_line.py +18 -8
  21. meerk40t/core/node/elem_path.py +18 -8
  22. meerk40t/core/node/elem_point.py +10 -4
  23. meerk40t/core/node/elem_polyline.py +19 -11
  24. meerk40t/core/node/elem_rect.py +18 -8
  25. meerk40t/core/node/elem_text.py +11 -5
  26. meerk40t/core/node/filenode.py +2 -8
  27. meerk40t/core/node/groupnode.py +11 -11
  28. meerk40t/core/node/image_processed.py +11 -5
  29. meerk40t/core/node/image_raster.py +11 -5
  30. meerk40t/core/node/node.py +64 -16
  31. meerk40t/core/node/refnode.py +2 -1
  32. meerk40t/core/svg_io.py +91 -34
  33. meerk40t/device/dummydevice.py +7 -1
  34. meerk40t/extra/vtracer.py +222 -0
  35. meerk40t/grbl/device.py +81 -8
  36. meerk40t/gui/about.py +20 -0
  37. meerk40t/gui/devicepanel.py +20 -16
  38. meerk40t/gui/gui_mixins.py +4 -0
  39. meerk40t/gui/icons.py +330 -253
  40. meerk40t/gui/laserpanel.py +8 -3
  41. meerk40t/gui/laserrender.py +41 -21
  42. meerk40t/gui/magnetoptions.py +158 -65
  43. meerk40t/gui/materialtest.py +229 -39
  44. meerk40t/gui/navigationpanels.py +229 -24
  45. meerk40t/gui/propertypanels/hatchproperty.py +2 -0
  46. meerk40t/gui/propertypanels/imageproperty.py +160 -106
  47. meerk40t/gui/ribbon.py +6 -1
  48. meerk40t/gui/scenewidgets/gridwidget.py +29 -32
  49. meerk40t/gui/scenewidgets/rectselectwidget.py +190 -192
  50. meerk40t/gui/simulation.py +75 -77
  51. meerk40t/gui/statusbarwidgets/defaultoperations.py +84 -48
  52. meerk40t/gui/statusbarwidgets/infowidget.py +2 -2
  53. meerk40t/gui/tips.py +15 -1
  54. meerk40t/gui/toolwidgets/toolpointmove.py +3 -1
  55. meerk40t/gui/wxmmain.py +242 -114
  56. meerk40t/gui/wxmscene.py +107 -24
  57. meerk40t/gui/wxmtree.py +4 -2
  58. meerk40t/gui/wxutils.py +60 -15
  59. meerk40t/image/imagetools.py +129 -65
  60. meerk40t/internal_plugins.py +4 -0
  61. meerk40t/kernel/kernel.py +39 -18
  62. meerk40t/kernel/settings.py +28 -9
  63. meerk40t/lihuiyu/device.py +24 -12
  64. meerk40t/main.py +1 -1
  65. meerk40t/moshi/device.py +20 -6
  66. meerk40t/network/console_server.py +22 -6
  67. meerk40t/newly/device.py +10 -3
  68. meerk40t/newly/gui/gui.py +10 -0
  69. meerk40t/ruida/device.py +22 -2
  70. meerk40t/ruida/loader.py +6 -3
  71. meerk40t/tools/geomstr.py +193 -125
  72. meerk40t/tools/rasterplotter.py +179 -93
  73. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/METADATA +1 -1
  74. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/RECORD +79 -78
  75. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/LICENSE +0 -0
  76. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/WHEEL +0 -0
  77. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/entry_points.txt +0 -0
  78. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/top_level.txt +0 -0
  79. {meerk40t-0.9.7030.dist-info → meerk40t-0.9.7040.dist-info}/zip-safe +0 -0
@@ -219,7 +219,7 @@ def plugin(kernel, lifecycle=None):
219
219
  "type": bool,
220
220
  "label": _("Track changes and allow undo"),
221
221
  "tip": _(
222
- "MK will save intermediate states to undo/redo changes") + "\n" +
222
+ "MK will save intermediate states to undo/redo changes") + "\n" +
223
223
  _("This may consume a significant amount of memory"),
224
224
  "page": "Start",
225
225
  "section": "_60_Undo",
@@ -311,6 +311,22 @@ def plugin(kernel, lifecycle=None):
311
311
  "page": "Classification",
312
312
  "section": "_10_Assignment-Logic",
313
313
  },
314
+ {
315
+ "attr": "classify_fill",
316
+ "object": elements,
317
+ "default": False,
318
+ "type": bool,
319
+ "label": _("Classify elements on fill"),
320
+ "tip": _(
321
+ "Usually MK will use the fill attribute as an indicator for a raster and will not distinguish between individual colors."
322
+ )
323
+ + "\n"
324
+ + _(
325
+ "If you want to distinguish between different raster types then activate this option."
326
+ ),
327
+ "page": "Classification",
328
+ "section": "_10_Assignment-Logic",
329
+ },
314
330
  {
315
331
  "attr": "classify_default",
316
332
  "object": elements,
@@ -648,6 +664,7 @@ class Elemental(Service):
648
664
  self.setting(bool, "classify_inherit_fill", False)
649
665
  self.setting(bool, "classify_inherit_exclusive", True)
650
666
  self.setting(bool, "update_statusbar_on_material_load", True)
667
+ self.setting(bool, "classify_fill", False)
651
668
  # self.setting(bool, "classify_auto_inherit", False)
652
669
  self.setting(bool, "classify_default", True)
653
670
  self.setting(bool, "op_show_default", False)
@@ -2695,6 +2712,31 @@ class Elemental(Service):
2695
2712
  def emptydebug(value):
2696
2713
  return
2697
2714
 
2715
+ def _get_next_auto_raster_count(operations):
2716
+ auto_raster_count = 0
2717
+ for op in operations:
2718
+ if op.type == "op raster" and op.id is not None and op.id.startswith("AR#"):
2719
+ try:
2720
+ used_id = int(op.id[3:])
2721
+ auto_raster_count = max(auto_raster_count, used_id)
2722
+ except (IndexError, ValueError):
2723
+ pass
2724
+ return auto_raster_count + 1
2725
+
2726
+ def _select_raster_candidate(operations, node, fuzzydistance):
2727
+ candidate = None
2728
+ candidate_dist = float("inf")
2729
+ for cand_op in operations:
2730
+ if cand_op.type != "op raster":
2731
+ continue
2732
+ col_d = Color.distance(cand_op.color, abs(node.fill))
2733
+ if col_d > fuzzydistance:
2734
+ continue
2735
+ if candidate is None or col_d < candidate_dist:
2736
+ candidate = cand_op
2737
+ candidate_dist = col_d
2738
+ return candidate
2739
+
2698
2740
  # I am tired of changing the code all the time, so let's do it properly
2699
2741
  debug = self.kernel.channel("classify", timestamp=True)
2700
2742
 
@@ -2807,17 +2849,58 @@ class Elemental(Service):
2807
2849
  debug(
2808
2850
  f"For {op.type}.{op.id}: black={is_black}, perform={perform_classification}, flag={self.classify_black_as_raster}"
2809
2851
  )
2810
- if hasattr(op, "classify") and perform_classification:
2852
+ if not (hasattr(op, "classify") and perform_classification):
2853
+ continue
2854
+ classified = False
2855
+ classifying_op = None
2856
+ if (
2857
+ self.classify_fill and
2858
+ op.type=="op raster" and
2859
+ hasattr(node, "fill") and node.fill is not None
2860
+ ):
2861
+ # This is a special use case:
2862
+ # Usually we don't distinguish a fill color - all non-transparent objects
2863
+ # are assigned to a single raster operation.
2864
+ # If the classify_fill flag is set, then we will use the fill attribute
2865
+ # to look for / create a matching raster operation
2866
+ raster_candidate = _select_raster_candidate(operations, node, fuzzydistance)
2867
+ if raster_candidate is None and self.classify_autogenerate:
2868
+ # We need to create one...
2869
+ auto_raster_count = _get_next_auto_raster_count(operations)
2870
+ 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,
2875
+ )
2876
+ add_op_function(raster_candidate)
2877
+ new_operations_added = True
2878
+
2879
+ classified, should_break, feedback = raster_candidate.classify(
2880
+ node,
2881
+ fuzzy=tempfuzzy,
2882
+ fuzzydistance=fuzzydistance,
2883
+ usedefault=False,
2884
+ )
2885
+ if classified:
2886
+ classifying_op = raster_candidate
2887
+ should_break = True
2888
+ if debug:
2889
+ debug(
2890
+ f"{node_desc} was color-raster-classified: {sstroke} {sfill} matching operation: {type(classifying_op).__name__}, break={should_break}"
2891
+ )
2892
+
2893
+ if not classified:
2811
2894
  classified, should_break, feedback = op.classify(
2812
2895
  node,
2813
2896
  fuzzy=tempfuzzy,
2814
2897
  fuzzydistance=fuzzydistance,
2815
2898
  usedefault=False,
2816
2899
  )
2817
- else:
2818
- continue
2900
+ if classified:
2901
+ classifying_op = op
2819
2902
  if classified:
2820
- update_debug_set(debug_set, op)
2903
+ update_debug_set(debug_set, classifying_op)
2821
2904
  if feedback is not None and "stroke" in feedback:
2822
2905
  classif_info[0] = True
2823
2906
  if feedback is not None and "fill" in feedback:
@@ -2833,7 +2916,7 @@ class Elemental(Service):
2833
2916
  sfill = ""
2834
2917
  if debug:
2835
2918
  debug(
2836
- f"{node_desc} was classified: {sstroke} {sfill} matching operation: {type(op).__name__}, break={should_break}"
2919
+ f"{node_desc} was classified: {sstroke} {sfill} matching operation: {type(classifying_op).__name__}, break={should_break}"
2837
2920
  )
2838
2921
  if should_break:
2839
2922
  break
@@ -2889,8 +2972,8 @@ class Elemental(Service):
2889
2972
  default_candidates = []
2890
2973
  for op in operations:
2891
2974
  if (
2892
- hasattr(op, "classify") and
2893
- getattr(op, "default", False) and
2975
+ hasattr(op, "classify") and
2976
+ getattr(op, "default", False) and
2894
2977
  hasattr(op, "valid_node_for_reference") and
2895
2978
  op.valid_node_for_reference(node)
2896
2979
  ):
@@ -3132,7 +3215,15 @@ class Elemental(Service):
3132
3215
  and node.fill is not None
3133
3216
  and node.fill.argb is not None
3134
3217
  ):
3135
- op = RasterOpNode(color="black")
3218
+ default_color = abs(node.fill) if self.classify_fill else Color("black")
3219
+ default_id = "AR#1" if self.classify_fill else "R1"
3220
+ default_label = "Auto-Raster #1" if self.classify_fill else "Standard-Raster"
3221
+ op = RasterOpNode(
3222
+ id=default_id,
3223
+ label=default_label,
3224
+ color=default_color,
3225
+ output = True,
3226
+ )
3136
3227
  stdops.append(op)
3137
3228
  if debug:
3138
3229
  debug("add an op raster due to fill")
@@ -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°)")
189
191
  )
192
+ @self.console_argument(
193
+ "end_angle", type=Angle, help=_("End angle of arc (default 360°)")
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,
@@ -656,7 +673,83 @@ def init_commands(kernel):
656
673
  height=500,
657
674
  )
658
675
  except Exception:
659
- 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
+ )
660
753
 
661
754
  @self.console_argument("prop", type=str, help=_("property to set"))
662
755
  @self.console_argument("new_value", type=str, help=_("new property value"))
@@ -681,7 +774,7 @@ def init_commands(kernel):
681
774
  if len(data) == 0:
682
775
  channel(_("No selected elements."))
683
776
  return
684
- if prop is None or (prop == "?" and new_value=="?"):
777
+ if prop is None or (prop == "?" and new_value == "?"):
685
778
  channel(_("You need to provide the property to set."))
686
779
  if prop == "?":
687
780
  identified = []
@@ -689,13 +782,17 @@ def init_commands(kernel):
689
782
  if op.type in identified:
690
783
  continue
691
784
  identified.append(op.type)
692
- prop_str = f"{op.type} has the following properties: "
785
+ prop_str = f"{op.type} has the following properties:"
786
+ first = True
693
787
  for d in op.__dict__:
694
788
  if d.startswith("_"):
695
789
  continue
696
- prop_str = f"{prop_str}, {d}"
790
+ prop_str = f"{prop_str}{'' if first else ','} {d}"
791
+ first = False
697
792
  channel(prop_str)
698
- 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
+ )
699
796
  return
700
797
  classify_required = False
701
798
  prop = prop.lower()
@@ -769,7 +866,9 @@ def init_commands(kernel):
769
866
  if prop in ("x", "y"):
770
867
  if not e.can_move(self.lock_allows_move):
771
868
  channel(
772
- _("Element can not be moved: {name}").format(name=str(e))
869
+ _("Element can not be moved: {name}").format(
870
+ name=str(e)
871
+ )
773
872
  )
774
873
  continue
775
874
  # We need to adjust the matrix
@@ -795,7 +894,9 @@ def init_commands(kernel):
795
894
  continue
796
895
  if hasattr(e, "can_scale") and not e.can_scale:
797
896
  channel(
798
- _("Element can not be scaled: {name}").format(name=str(e))
897
+ _("Element can not be scaled: {name}").format(
898
+ name=str(e)
899
+ )
799
900
  )
800
901
  continue
801
902
  if hasattr(e, "matrix") and hasattr(e, "bounds"):
@@ -865,7 +966,9 @@ def init_commands(kernel):
865
966
  delattr(e, "wxfont")
866
967
  text_elems.append(e)
867
968
  if prop in ("mktext", "mkfont"):
868
- for property_op in self.kernel.lookup_all("path_updater/.*"):
969
+ for property_op in self.kernel.lookup_all(
970
+ "path_updater/.*"
971
+ ):
869
972
  property_op(self.kernel.root, e)
870
973
  if prop in (
871
974
  "dpi",
@@ -935,7 +1038,7 @@ def init_commands(kernel):
935
1038
  if not data:
936
1039
  channel(_("No selected operations."))
937
1040
  return
938
- if prop is None or (prop == "?" and new_value=="?"):
1041
+ if prop is None or (prop == "?" and new_value == "?"):
939
1042
  channel(_("You need to provide the property to set."))
940
1043
  if prop == "?":
941
1044
  identified = []
@@ -949,7 +1052,9 @@ def init_commands(kernel):
949
1052
  continue
950
1053
  prop_str = f"{prop_str}, {d}"
951
1054
  channel(prop_str)
952
- 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
+ )
953
1058
  return
954
1059
  prop = prop.lower()
955
1060
  if len(new_value) == 0:
@@ -982,7 +1087,6 @@ def init_commands(kernel):
982
1087
  new_value = testval
983
1088
  prevalidated = True
984
1089
 
985
-
986
1090
  changed = []
987
1091
  # _("Update property")
988
1092
  with self.undoscope("Update property"):
@@ -1030,7 +1134,6 @@ def init_commands(kernel):
1030
1134
  ).format(val=new_value, field=prop, oldval=oldval)
1031
1135
  )
1032
1136
 
1033
-
1034
1137
  else:
1035
1138
  channel(
1036
1139
  _("Operation {name} has no property {field}").format(
@@ -1122,7 +1225,7 @@ def init_commands(kernel):
1122
1225
  f"Simplified {node.type} ({node.display_label()}), tolerance: {tolerance}={Length(tolerance, digits=4).length_mm})"
1123
1226
  )
1124
1227
  if seg_before:
1125
- saving = f"({(seg_before - seg_after)/seg_before*100:.1f}%)"
1228
+ saving = f"({(seg_before - seg_after) / seg_before * 100:.1f}%)"
1126
1229
  else:
1127
1230
  saving = ""
1128
1231
  channel(f"Subpaths before: {sub_before} to {sub_after}")
@@ -1363,7 +1466,9 @@ def init_commands(kernel):
1363
1466
  with self.undoscope("Set stroke-width"):
1364
1467
  for e in data:
1365
1468
  if hasattr(e, "lock") and e.lock:
1366
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1469
+ channel(
1470
+ _("Can't modify a locked element: {name}").format(name=str(e))
1471
+ )
1367
1472
  continue
1368
1473
  e.stroke_width = stroke_width
1369
1474
  try:
@@ -1396,7 +1501,9 @@ def init_commands(kernel):
1396
1501
  with self.undoscope("Update stroke-scale"):
1397
1502
  for e in data:
1398
1503
  if hasattr(e, "lock") and e.lock:
1399
- channel(_("Can't modify a locked element: {name}").format(name=str(e)))
1504
+ channel(
1505
+ _("Can't modify a locked element: {name}").format(name=str(e))
1506
+ )
1400
1507
  continue
1401
1508
  e.stroke_scaled = command == "enable_stroke_scale"
1402
1509
  e.altered()
@@ -1700,7 +1807,9 @@ def init_commands(kernel):
1700
1807
  for e in apply:
1701
1808
  if hasattr(e, "lock") and e.lock:
1702
1809
  channel(
1703
- _("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
+ )
1704
1813
  )
1705
1814
  continue
1706
1815
  e.stroke = None
@@ -1712,7 +1821,9 @@ def init_commands(kernel):
1712
1821
  for e in apply:
1713
1822
  if hasattr(e, "lock") and e.lock:
1714
1823
  channel(
1715
- _("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
+ )
1716
1827
  )
1717
1828
  continue
1718
1829
  e.stroke = Color(color)
@@ -1803,13 +1914,14 @@ def init_commands(kernel):
1803
1914
  return "elements", data
1804
1915
  # _("Set fill")
1805
1916
  with self.undoscope("Set fill"):
1806
-
1807
1917
  if color == "none":
1808
1918
  self.set_start_time("fill")
1809
1919
  for e in apply:
1810
1920
  if hasattr(e, "lock") and e.lock:
1811
1921
  channel(
1812
- _("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
+ )
1813
1925
  )
1814
1926
  continue
1815
1927
  e.fill = None
@@ -1821,7 +1933,9 @@ def init_commands(kernel):
1821
1933
  for e in apply:
1822
1934
  if hasattr(e, "lock") and e.lock:
1823
1935
  channel(
1824
- _("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
+ )
1825
1939
  )
1826
1940
  continue
1827
1941
  e.fill = Color(color)
@@ -2626,15 +2740,26 @@ def init_commands(kernel):
2626
2740
  self.first_emphasized = None
2627
2741
  return "elements", data
2628
2742
 
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"))
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
+ )
2631
2754
  @self.console_command(
2632
2755
  "stitch",
2633
2756
  help=_("stitch selected elements"),
2634
2757
  input_type=(None, "elements"),
2635
2758
  output_type="elements",
2636
2759
  )
2637
- def stitched(command, channel, _, data=None, tolerance=None, keep=None, post=None, **kwargs):
2760
+ def stitched(
2761
+ command, channel, _, data=None, tolerance=None, keep=None, post=None, **kwargs
2762
+ ):
2638
2763
  def _prepare_stitching_params(channel, data, tolerance, keep):
2639
2764
  if data is None:
2640
2765
  data = list(self.elems(emphasized=True))
@@ -2652,7 +2777,7 @@ def init_commands(kernel):
2652
2777
  channel(f"Invalid tolerance value: {tolerance}")
2653
2778
  return data, tolerance, keep, False
2654
2779
  return data, tolerance_val, keep, True
2655
-
2780
+
2656
2781
  def stitcheable_nodes(data, tolerance) -> list:
2657
2782
  out = []
2658
2783
  geoms = []
@@ -2662,6 +2787,8 @@ def init_commands(kernel):
2662
2787
  continue
2663
2788
  for g1 in node.as_geometry().as_contiguous():
2664
2789
  geoms.append((idx, g1))
2790
+ if tolerance == 0:
2791
+ tolerance = 1e-6
2665
2792
  for idx1, (nodeidx1, g1) in enumerate(geoms):
2666
2793
  for idx2 in range(idx1 + 1, len(geoms)):
2667
2794
  nodeidx2 = geoms[idx2][0]
@@ -2671,10 +2798,10 @@ def init_commands(kernel):
2671
2798
  lp1 = g1.last_point
2672
2799
  lp2 = g2.last_point
2673
2800
  if (
2674
- abs(lp1 - lp2) <= tolerance or
2675
- abs(lp1 - fp2) <= tolerance or
2676
- abs(fp1 - fp2) <= tolerance or
2677
- abs(fp1 - lp2) <= tolerance
2801
+ abs(lp1 - lp2) <= tolerance
2802
+ or abs(lp1 - fp2) <= tolerance
2803
+ or abs(fp1 - fp2) <= tolerance
2804
+ or abs(fp1 - lp2) <= tolerance
2678
2805
  ):
2679
2806
  if nodeidx1 not in out:
2680
2807
  out.append(nodeidx1)
@@ -2683,7 +2810,9 @@ def init_commands(kernel):
2683
2810
 
2684
2811
  return [data[idx] for idx in out]
2685
2812
 
2686
- data, tolerance, keep, valid = _prepare_stitching_params(channel, data, tolerance, keep)
2813
+ data, tolerance, keep, valid = _prepare_stitching_params(
2814
+ channel, data, tolerance, keep
2815
+ )
2687
2816
  if not valid:
2688
2817
  return
2689
2818
  s_data = stitcheable_nodes(data, tolerance)
@@ -2701,7 +2830,7 @@ def init_commands(kernel):
2701
2830
  default_fill = None
2702
2831
  for node in s_data:
2703
2832
  if hasattr(node, "as_geometry"):
2704
- geom : Geomstr = node.as_geometry()
2833
+ geom: Geomstr = node.as_geometry()
2705
2834
  geoms.extend(iter(geom.as_contiguous()))
2706
2835
  if default_stroke is None and hasattr(node, "stroke"):
2707
2836
  default_stroke = node.stroke
@@ -2728,10 +2857,101 @@ def init_commands(kernel):
2728
2857
  )
2729
2858
  data_out.append(node)
2730
2859
  new_len = len(data_out)
2731
- channel(f"Sub-Paths before: {prev_len} -> consolidated to {new_len} sub-paths")
2732
-
2860
+ channel(
2861
+ f"Sub-Paths before: {prev_len} -> consolidated to {new_len} sub-paths"
2862
+ )
2863
+
2733
2864
  post.append(classify_new(data_out))
2734
2865
  self.set_emphasis(data_out)
2735
2866
  return "elements", data_out
2736
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
+
2737
2957
  # --------------------------- END COMMANDS ------------------------------