svg2gcode 3.0.3__tar.gz → 3.2.1__tar.gz

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 (35) hide show
  1. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/PKG-INFO +10 -4
  2. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/README.md +8 -2
  3. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/pyproject.toml +1 -1
  4. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/__init__.py +1 -1
  5. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/__main__.py +14 -13
  6. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/__init__.py +6 -4
  7. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/_compiler.py +339 -145
  8. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/css_color.py +5 -4
  9. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/formulas.py +6 -1
  10. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_elliptical_arc.py +5 -3
  11. svg2gcode-3.2.1/svg2gcode/svg_to_gcode/geometry/_line.py +111 -0
  12. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_line_segment_chain.py +59 -0
  13. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/_path.py +1 -1
  14. svg2gcode-3.0.3/svg2gcode/svg_to_gcode/geometry/_line.py +0 -33
  15. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/LICENSE +0 -0
  16. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/LICENSE +0 -0
  17. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/__init__.py +0 -0
  18. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/__init__.py +0 -0
  19. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/_abstract_interface.py +0 -0
  20. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/_fan_controlled_gcode.py +0 -0
  21. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/compiler/interfaces/_gcode.py +0 -0
  22. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/__init__.py +0 -0
  23. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_abstract_chain.py +0 -0
  24. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_abstract_curve.py +0 -0
  25. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_circular_arc.py +0 -0
  26. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_cubic_bazier.py +0 -0
  27. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_matrix.py +0 -0
  28. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_quadratic_bazier.py +0 -0
  29. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_raster_image.py +0 -0
  30. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_smooth_arc_chain.py +0 -0
  31. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/geometry/_vector.py +0 -0
  32. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/__init__.py +0 -0
  33. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/_parser_methods.py +0 -0
  34. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/_transformation.py +0 -0
  35. {svg2gcode-3.0.3 → svg2gcode-3.2.1}/svg2gcode/svg_to_gcode/svg_parser/debug_methods.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: svg2gcode
3
- Version: 3.0.3
3
+ Version: 3.2.1
4
4
  Summary: svg2gcode: convert an SVG (Scalable Vector Graphic) image to gcode.
5
5
  Keywords: scalable vector graphics,svg,image,laser cutter,laser engraving
6
6
  Author-email: Johannes Noordanus <mailjohannes.mailnoordanus@gmail.com>
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Requires-Dist: numpy >= 1.24.3
11
11
  Requires-Dist: Pillow >= 9.5.0
12
- Requires-Dist: image2gcode >= 2.9.6
12
+ Requires-Dist: image2gcode >= 2.9.8
13
13
  Project-URL: Home, https://github.com/johannesnoordanus/svg2gcode
14
14
 
15
15
  # svg2gcode
@@ -41,12 +41,16 @@ See options ```--speedmoves``` and ```--noise``` for example.
41
41
  Version 3.0.0 and higher have support for 'stroke' (color) and 'stroke-width' attributes, this means that cutting a path works a bit different now.
42
42
  When the *stroke* attribute of a *path* is nonexistent or set to none, the path will be laser burned with the value set by option *--cuttingpower*.
43
43
 
44
+ Version 3.1.0 and higher have support for 'fill' (color) and 'fill-rule' attributes. Currently only fill rule 'evenodd' is supported.
45
+
44
46
  Also, *svg2gcode* option ```--pathcut``` can be used to override all stroke attributes and force cutting of all *paths* of the SVG.
45
- But, see the examples below.
47
+ Option ```--nofill``` (not set by default) is added to disable path fills.
46
48
 
49
+ More info can be obtained by looking at the examples below and from program *image2gcode* and its documentation (for example about callibrating laser engravers).
50
+
47
51
  Please consider supporting me, so I can make this application better and add new functionality to it: <http://paypal.me/johannesnoordanus/5,00>
48
52
 
49
- My next update will be a major one, I'll add support for the *fill* attribute of SVG objects.
53
+ My next update will add fill-rule 'nonzero'.
50
54
 
51
55
 
52
56
  To summarize:
@@ -64,6 +68,7 @@ General optimizations
64
68
  - low burn levels (stray pixels) can be suppressed (default off)
65
69
  - option *--constantburn* selects constant burn mode *M3* (for cutting and engraving) instead of default dynamic burn mode *M4*
66
70
  - borders are drawn in parallel and in one go, following the *path* coordinates.
71
+ - fill has support for alpha channel and 'fill-opacity'
67
72
 
68
73
  **Tip**: use commandline program *grblhud* *(https://github.com/johannesnoordanus/grblhud)* to have full control over gcode execution,
69
74
  also, program *image2gcode* has similar capabilities but handles raster images files (like *png* and *jpg*) directly.
@@ -124,6 +129,7 @@ options:
124
129
  --rotate <default:0> number of degrees to rotate
125
130
  --splitfile split gcode output of SVG path and image objects
126
131
  --pathcut alway cut SVG path objects! (use laser power set with option --cuttingpower)
132
+ --nofill ignore SVG fill attribute
127
133
  --xmaxtravel <default:300>
128
134
  machine x-axis lengh in mm
129
135
  --ymaxtravel <default:400>
@@ -27,12 +27,16 @@ See options ```--speedmoves``` and ```--noise``` for example.
27
27
  Version 3.0.0 and higher have support for 'stroke' (color) and 'stroke-width' attributes, this means that cutting a path works a bit different now.
28
28
  When the *stroke* attribute of a *path* is nonexistent or set to none, the path will be laser burned with the value set by option *--cuttingpower*.
29
29
 
30
+ Version 3.1.0 and higher have support for 'fill' (color) and 'fill-rule' attributes. Currently only fill rule 'evenodd' is supported.
31
+
30
32
  Also, *svg2gcode* option ```--pathcut``` can be used to override all stroke attributes and force cutting of all *paths* of the SVG.
31
- But, see the examples below.
33
+ Option ```--nofill``` (not set by default) is added to disable path fills.
32
34
 
35
+ More info can be obtained by looking at the examples below and from program *image2gcode* and its documentation (for example about callibrating laser engravers).
36
+
33
37
  Please consider supporting me, so I can make this application better and add new functionality to it: <http://paypal.me/johannesnoordanus/5,00>
34
38
 
35
- My next update will be a major one, I'll add support for the *fill* attribute of SVG objects.
39
+ My next update will add fill-rule 'nonzero'.
36
40
 
37
41
 
38
42
  To summarize:
@@ -50,6 +54,7 @@ General optimizations
50
54
  - low burn levels (stray pixels) can be suppressed (default off)
51
55
  - option *--constantburn* selects constant burn mode *M3* (for cutting and engraving) instead of default dynamic burn mode *M4*
52
56
  - borders are drawn in parallel and in one go, following the *path* coordinates.
57
+ - fill has support for alpha channel and 'fill-opacity'
53
58
 
54
59
  **Tip**: use commandline program *grblhud* *(https://github.com/johannesnoordanus/grblhud)* to have full control over gcode execution,
55
60
  also, program *image2gcode* has similar capabilities but handles raster images files (like *png* and *jpg*) directly.
@@ -110,6 +115,7 @@ options:
110
115
  --rotate <default:0> number of degrees to rotate
111
116
  --splitfile split gcode output of SVG path and image objects
112
117
  --pathcut alway cut SVG path objects! (use laser power set with option --cuttingpower)
118
+ --nofill ignore SVG fill attribute
113
119
  --xmaxtravel <default:300>
114
120
  machine x-axis lengh in mm
115
121
  --ymaxtravel <default:400>
@@ -14,7 +14,7 @@ keywords = ["scalable vector graphics", "svg", "image", "laser cutter", "laser e
14
14
  dependencies = [
15
15
  "numpy >= 1.24.3",
16
16
  "Pillow >= 9.5.0",
17
- "image2gcode >= 2.9.6",
17
+ "image2gcode >= 2.9.8",
18
18
  ]
19
19
  requires-python = ">=3.7"
20
20
 
@@ -1,4 +1,4 @@
1
1
  """
2
2
  svg2gcode: convert an SVG (Scalable Vector Graphic) image to gcode.
3
3
  """
4
- __version__ = "3.0.3"
4
+ __version__ = "3.2.1"
@@ -37,7 +37,7 @@ def svg2gcode(args) -> int:
37
37
  'maximum_image_laser_power':args.imagepower, 'image_movement_speed':args.imagespeed, 'fan':args.fan,'rapid_move':args.rapidmove,
38
38
  'showimage':args.showimage, 'x_axis_maximum_travel':args.xmaxtravel,'y_axis_maximum_travel':args.ymaxtravel, 'image_noise':args.noise,
39
39
  'pass_depth':args.pass_depth, 'laser_mode':"constant" if args.constantburn else "dynamic", 'splitfile':args.splitfile, 'pathcut':args.pathcut,
40
- 'image_poweroffset':args.poweroffset, 'image_overscan':args.overscan, 'image_showoverscan':args.showoverscan})
40
+ 'nofill':args.nofill, 'image_poweroffset':args.poweroffset, 'image_overscan':args.overscan, 'image_showoverscan':args.showoverscan})
41
41
 
42
42
  compiler = init_compiler(args)
43
43
 
@@ -138,6 +138,7 @@ def main() -> int:
138
138
  type=int, help="number of degrees to rotate")
139
139
  parser.add_argument('--splitfile', action='store_true', default=False, help='split gcode output of SVG path and image objects' )
140
140
  parser.add_argument('--pathcut', action='store_true', default=False, help='alway cut SVG path objects! (use laser power set with option --cuttingpower)' )
141
+ parser.add_argument('--nofill', action='store_true', default=False, help='ignore SVG fill attribute' )
141
142
  parser.add_argument('--xmaxtravel', default=cfg["xmaxtravel_default"], metavar="<default:" +str(cfg["xmaxtravel_default"])+ ">",
142
143
  type=int, help="machine x-axis lengh in mm")
143
144
  parser.add_argument('--ymaxtravel', default=cfg["ymaxtravel_default"], metavar="<default:" +str(cfg["ymaxtravel_default"])+ ">",
@@ -147,18 +148,18 @@ def main() -> int:
147
148
 
148
149
  args = parser.parse_args()
149
150
 
150
- try:
151
- if args.origin is not None and args.selfcenter:
152
- print("options --selfcenter and --origin cannot be used at the same time, program abort")
153
- return 1
154
-
155
- return svg2gcode(args)
156
- except KeyboardInterrupt:
157
- print(f"svg2gcode aborted!")
158
- except Exception as error:
159
- print(error)
160
-
161
- return 1
151
+ ## try:
152
+ if args.origin is not None and args.selfcenter:
153
+ print("options --selfcenter and --origin cannot be used at the same time, program abort")
154
+ return 1
155
+
156
+ return svg2gcode(args)
157
+ #### except KeyboardInterrupt:
158
+ ## print(f"svg2gcode aborted!")
159
+ ## except Exception as error:
160
+ ## print(error)
161
+ ##
162
+ ## return 1
162
163
 
163
164
  if __name__ == '__main__':
164
165
  sys.exit(main())
@@ -39,7 +39,8 @@ SETTING = {
39
39
  "pixel_size", # float sets image pixel size (in mm)
40
40
  "showimage", # boolean show image used for conversion to gcode
41
41
  "splitfile", # boolean return SVG path objects to <filename>.<gcext> and SVG image objects to <filename>_images.<gcext>
42
- "pathcut" # boolean always cut SVG path objects! use laser_power setting
42
+ "pathcut", # boolean always cut SVG path objects! use laser_power setting
43
+ "nofill" # boolean ignore SVG fill attribute
43
44
  }
44
45
 
45
46
  # Set defaults 'minimum_laser_power', 'pass_depth', 'dwell_time', 'laser_power', 'laser_mode', 'unit', 'distance_mode'
@@ -71,8 +72,9 @@ DEFAULT_SETTING = {
71
72
  "rapid_move": 10, # default set to true, for images do a rapid move when skipping more than 10mm
72
73
  "pixel_size": 0.1, # laser kerf is mostly < 0.1mm
73
74
  "showimage": False, # default image is not shown
74
- "splitfile": False, # SVG path and image objects are emitted to one file: <filename>.<gcext>
75
- "pathcut": False # always cut SVG path objects! use laser_power setting
75
+ "splitfile": False, # SVG path and image objects are emitted to one file: <filename>.<gcext>
76
+ "pathcut": False, # always cut SVG path objects! use laser_power setting
77
+ "nofill": False # ignore SVG fill attribute
76
78
  }
77
79
 
78
80
  def check_setting(setting: dict[str,Any] =None) -> bool:
@@ -114,7 +116,7 @@ def check_setting(setting: dict[str,Any] =None) -> bool:
114
116
  raise ValueError(f"Unknown '{key}' value '{setting[key]}'. Please specify one of the following: {UNITS}")
115
117
  if key == "distance_mode" and setting[key] not in DISTANCEMODE:
116
118
  raise ValueError(f"Unknown '{key}' value '{setting[key]}'. Please specify one of the following: {DISTANCEMODE}")
117
- if key in {"fan","showimage","splitfile","pathcut","image_showoverscan"} and setting[key] not in {True,False}:
119
+ if key in {"fan","showimage","splitfile","pathcut","nofill","image_showoverscan"} and setting[key] not in {True,False}:
118
120
  raise ValueError(f"Unknown '{key}' value '{setting[key]}'. Please specify one of the following: {{True,False}}")
119
121
  if key == "pixel_size" and setting[key] and (not isinstance(setting[key],(float)) or setting[key] <= 0):
120
122
  raise TypeError(f"'{key}' is of type '{type(setting[key])}' but should be of type {type(1.0)} and have a value > 0.0")
@@ -8,6 +8,8 @@ from io import BytesIO
8
8
  from typing import Any
9
9
 
10
10
  import base64
11
+ from PIL import Image
12
+ from skimage.draw import line as drawline
11
13
  import numpy as np
12
14
 
13
15
  from svg2gcode.svg_to_gcode.compiler.interfaces import Interface
@@ -210,64 +212,6 @@ class Compiler:
210
212
  file.write((self.gcode_file_header() if len(self.body) == 0 else "") + '\n' + self.compile_images() + '\n' + self.interface.program_end() + '\n')
211
213
  logger.info(f"Added image(s) to {file_name}")
212
214
 
213
- def orthogonal_offset(self, offset: float, line: Line) -> Line:
214
-
215
- def offset_direction(line: Line) -> (int,int):
216
- d_x = (line.end.x - line.start.x)
217
- d_y = (line.end.y - line.start.y)
218
- direction = (0,0)
219
-
220
- if d_x > 0 and d_y > 0:
221
- direction = (-1,1)
222
- elif d_x > 0 and d_y == 0:
223
- direction = (0,1)
224
- elif d_x < 0 and d_y == 0:
225
- direction = (0,-1)
226
- elif d_x < 0 and d_y < 0:
227
- direction = (1,-1)
228
- elif d_x == 0 and d_y < 0:
229
- direction = (1,0)
230
- elif d_x == 0 and d_y > 0:
231
- direction = (-1,0)
232
- elif d_x > 0 and d_y < 0:
233
- direction = (1,1)
234
- elif d_x < 0 and d_y > 0:
235
- direction = (-1,-1)
236
-
237
- return direction
238
-
239
- delta_sign = offset_direction(line)
240
-
241
- # calculate ortogonal line vector((0,0),(delta_x,delta_y)) of 'offset' length:
242
- # (1) y = 1/slope * x
243
- #
244
- # of length offset:
245
- # (2) x^2 + y^2 = offset^2 (Pythagoras)
246
- # (3) x^2 + (1/slope * x)^2 = offset^2
247
- # (4) (1 + (1/slope)^2) * x^2 = offset^2
248
- # (5) x^2 = offset^2 / (1 + (1/slope)^2)
249
-
250
- if line.slope == 0:
251
- # vertical line
252
- delta_x = 0
253
- delta_y = delta_sign[1] * offset
254
- elif line.slope == 1:
255
- # horizontal line
256
- delta_x = delta_sign[0] * offset
257
- delta_y = 0
258
- else:
259
- # slope
260
- inv_slope = abs((1/line.slope))
261
- delta_x = round(math.sqrt((offset ** 2) / (1 + inv_slope ** 2)), self.precision)
262
- delta_y = round(inv_slope * delta_x, self.precision)
263
-
264
- # set right direction
265
- delta_x = delta_sign[0] * (-1 if (offset < 0) else 1) * delta_x
266
- delta_y = delta_sign[1] * (-1 if (offset < 0) else 1) * delta_y
267
-
268
- # add delta vector to line start and end vectors
269
- return Line(line.start + Vector(delta_x, delta_y), line.end + Vector(delta_x, delta_y), "offset")
270
-
271
215
  def append_line_chain(self, line_chain: LineSegmentChain, step: float, color: int = None):
272
216
  """
273
217
  Draws a LineSegmentChain by calling interface.linear_move() for each segment. The resulting code is appended to
@@ -400,42 +344,10 @@ class Compiler:
400
344
 
401
345
  return None
402
346
 
403
- def distance(self, A: (float,float),B: (float,float)):
404
- """
405
- Pythagoras
406
- """
407
- # |Ax - Bx|^2 + |Ay - By|^2 = C^2
408
- # distance = √C^2
409
- return math.sqrt(abs(A[0] - B[0])**2 + abs(A[1] - B[1])**2)
410
-
411
- def line_intersection(self, l1: Line, l2:Line) -> Vector:
412
- """
413
- Line intersection (https://en.wikipedia.org/wiki/Line–line_intersection)
414
- """
415
- x1, y1 = l1.start
416
- x2, y2 = l1.end
417
- x3, y3 = l2.start
418
- x4, y4 = l2.end
419
-
420
- # denomintor
421
- d = round((x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4), self.precision)
422
-
423
- # Note that the cutoff below is needed to eliminate line artifacts (spikes) of the render result.
424
- # This is due to calculation (precision) errors resulting in the y coordinate of the intersection to
425
- # be off by a factor (2,3, 10?). It might be that the rounding applied here is not correct, or/and
426
- # that the equations are inherently sensitive to rounding errors at certain inputs.
427
- # TODO: analize this
428
- if abs(d) < 0.01:
429
- # no intersection
430
- return None
431
-
432
- return Vector( round(((x1*y2 - y1*x2) * (x3 - x4) - (x1 - x2) * (x3*y4 - y3*x4))/d, self.precision),
433
- round(((x1*y2 - y1*x2) * (y3 - y4) - (y1 - y2) * (x3*y4 - y3*x4))/d, self.precision) )
434
-
435
- def image2gcode(self, img_attrib: dict[str, Any], img=None, transformation = None):
347
+ def image2gcode(self, img_attrib: dict[str, Any], img=None, transformation = None, power = None):
436
348
 
437
349
  # create image conversion object
438
- convert = Image2gcode(transformation = transformation.apply_affine_transformation if transformation is not None else None)
350
+ convert = Image2gcode(transformation = transformation.apply_affine_transformation if transformation is not None else None, power = power)
439
351
 
440
352
  #
441
353
  # set arguments
@@ -453,6 +365,7 @@ class Compiler:
453
365
  arguments["showoverscan"] = img_attrib['gcode_showoverscan'] if 'gcode_showoverscan' in img_attrib else self.settings["image_showoverscan"]
454
366
  arguments["offset"] = (float(img_attrib['x']), float(img_attrib['y']))
455
367
  arguments["name"] = img_attrib['id']
368
+ arguments["noinvert"] = img_attrib['noinvert'] if 'noinvert' in img_attrib else False
456
369
 
457
370
  # get image parameters
458
371
  params = ''
@@ -477,7 +390,7 @@ class Compiler:
477
390
  for example "fill:#F4CF84;fill-rule:evenodd;stroke:#D07735;"
478
391
  """
479
392
 
480
- style = {'fill' : None, 'fill-rule': None, 'stroke': None, 'stroke-width': None, 'pathcut': None}
393
+ style = {'fill' : None, 'fill-rule': None, 'fill-opacity': None, 'stroke': None, 'stroke-width': None, 'pathcut': None}
481
394
 
482
395
  # parse style attribute
483
396
 
@@ -494,6 +407,11 @@ class Compiler:
494
407
  fill_rule = re.search('fill-rule:#(evenodd|nonzero)',style_str)
495
408
  if fill_rule:
496
409
  style['fill-rule'] = re.search('(evenodd|nonzero)', fill_rule.group(0)).group(0)
410
+ # parse fill-opacity
411
+ if 'fill-opacity' in style_str:
412
+ fill_opacity = re.search('fill-opacity:(\d*\.)?\d+',style_str)
413
+ if fill_opacity:
414
+ style['fill-opacity'] = re.search('(\d*\.)?\d+', fill_opacity.group(0)).group(0)
497
415
  # parse stroke
498
416
  if 'stroke' in style_str:
499
417
  stroke = re.search('stroke:[^;]+;',style_str)
@@ -518,13 +436,17 @@ class Compiler:
518
436
  style['fill'] = curve.path_attrib['fill']
519
437
  if 'fill-rule' in curve.path_attrib:
520
438
  style['fill-rule'] = curve.path_attrib['fill-rule']
439
+ # parse fill-opacity
440
+ if 'fill-opacity' in curve.path_attrib:
441
+ fill_opacity = re.search('fill-opacity:(\d*\.)?\d+',curve.path_attrib)
442
+ if fill_opacity:
443
+ style['fill-opacity'] = re.search('(\d*\.)?\d+', fill_opacity.group(0)).group(0)
521
444
  # parse stroke attribute
522
445
  if 'stroke' in curve.path_attrib:
523
446
  style['stroke'] = curve.path_attrib['stroke']
524
447
  # parse stroke-width attribute
525
448
  if 'stroke-width' in curve.path_attrib:
526
449
  style['stroke-width'] = curve.path_attrib['stroke-width']
527
-
528
450
  # parse gcode_pathcut attribute
529
451
  if 'gcode_pathcut' in curve.path_attrib:
530
452
  style['pathcut'] = curve.path_attrib['gcode_pathcut']
@@ -535,7 +457,17 @@ class Compiler:
535
457
  """
536
458
  Draws curves.
537
459
  """
460
+
461
+ def draw_line(img: np.array, p1: (int,int), p2: (int,int), gray: int):
462
+ """
463
+ Draws a line.
464
+ """
465
+ pixel = 1/self.settings["pixel_size"]
466
+ yy, xx = drawline(int(p1[1]*pixel),int(p1[0]*pixel),int(p2[1]*pixel),int(p2[0]*pixel))
467
+ img[yy,xx] = gray
468
+
538
469
  path_curves = {}
470
+ pixel_size = float(self.settings["pixel_size"])
539
471
 
540
472
  for curve in curves:
541
473
  if isinstance(curve, RasterImage):
@@ -554,7 +486,7 @@ class Compiler:
554
486
 
555
487
  curve_name_id = ""
556
488
  # parse id
557
- if curve.path_attrib:
489
+ if curve.path_attrib and 'id' in curve.path_attrib:
558
490
  curve_name_id = curve.path_attrib['id']
559
491
 
560
492
  if curve_name_id not in path_curves:
@@ -572,19 +504,24 @@ class Compiler:
572
504
  # add line segments to curves having this id
573
505
  path_curves[curve_name_id].append(line_chain)
574
506
 
507
+ # default stroke width is one line (thickness)
508
+ stroke_width = pixel_size
509
+
575
510
  # emit all paths (organized by name id)
576
511
  for name_id in path_curves:
577
- # for all line chains
512
+ steps = []
513
+ fill_color = None
514
+ fill_rule = None
515
+ # set a boundingbox per 'name_id'
516
+ boundingbox = Boundingbox()
578
517
  for line_chain in path_curves[name_id]:
579
518
 
580
- pixel_size = float(self.settings["pixel_size"])
581
519
  # default stroke width is one line (thickness)
582
- stroke_width = pixel_size
583
-
584
- first_line_of_chain = line_chain.get(0)
585
-
586
520
  width = 0
587
521
  stroke_color = ""
522
+
523
+ # get style info for this line chain (of object 'name_id')
524
+ first_line_of_chain = line_chain.get(0)
588
525
  style = self.parse_style_attribute(first_line_of_chain)
589
526
  if style:
590
527
  if not (style['pathcut'] == 'true' or self.settings["pathcut"]) \
@@ -592,62 +529,319 @@ class Compiler:
592
529
  stroke_color = style['stroke']
593
530
  if style['stroke-width'] is not None and style['stroke-width'] != "none":
594
531
  stroke_width = float(style['stroke-width'])
595
- width = int(round(stroke_width/pixel_size, self.precision))
596
-
532
+ width = math.ceil(round(stroke_width/pixel_size, self.precision)/2)
533
+ if style['fill'] is not None:
534
+ # Invert b&w value (beforehand) and apply alpha channel - when available - to inverted b&w value
535
+ # Note that step 6 sets option 'noinvert' to be able to use the value below directly.
536
+ fill_color = Image2gcode.linear_power(css_color.parse_css_color2bw8(style['fill']), self.settings["maximum_image_laser_power"])
537
+ if style['fill-rule'] is not None:
538
+ fill_rule = style['fill-rule']
539
+ if fill_rule == "nonzero":
540
+ logger.warn(f"fill-rule 'nonzero' of object '{name_id}' is currently unsupported!")
541
+ if style['fill-opacity'] is not None:
542
+ alpha = float(style['fill-opacity'])
543
+ if not (alpha >=0 and alpha <= 1):
544
+ logger.warn(f"Opacity value '{alpha}' should be in range [0.0..1.0]!")
545
+ alpha = 1
546
+ else:
547
+ rgba = css_color.parse_css_color(style['fill'])
548
+ alpha = 1
549
+ if len(rgba) == 4:
550
+ alpha = rgba[3]
551
+ fill_color = fill_color * alpha
552
+
553
+ # get steps (offsets) for the lines that make the border
597
554
  steps = [0]
598
555
  if len(stroke_color) and width:
599
556
  for delta in range(width):
600
557
  if delta:
601
558
  steps.append(round(delta * pixel_size, self.precision))
602
- if not (delta + 1 == width and width % 2 == 0):
559
+ if not (delta == width and width % 2 != 0):
603
560
  steps.append(round(-delta * pixel_size, self.precision))
604
561
 
562
+ # Get alpha channel (opacity)
563
+ alpha = 1
564
+ if len(stroke_color):
565
+ rgba = css_color.parse_css_color(stroke_color)
566
+ if len(rgba) == 4:
567
+ alpha = rgba[3]
568
+
569
+ # render svg stroke (of size width)
570
+ # (draw smooth border outlines in gcode)
571
+ # Note the update of the boundingbox.
572
+ self.body.extend([f"\n; border: '{name_id}'"])
605
573
  for step in steps:
606
574
  if step:
607
- delta_chain = LineSegmentChain()
608
-
609
- for line in line_chain:
610
- line_delta = self.orthogonal_offset(step, line)
611
-
612
- if delta_chain.chain_size():
613
- # calculate intersection of previous line and current line
614
- intersect = self.line_intersection(delta_chain.get(-1), line_delta)
615
- large = 200
616
- if intersect is not None:
617
- # set prev_line.end to intersect
618
- prev_line = delta_chain.get(-1)
619
- prev_line.end = intersect
620
- delta_chain.set(-1, prev_line)
621
-
622
- # set line_delta start to intersect
623
- line_delta.start = intersect
624
- else:
625
- # connect line, to prevent ValueErrors from delta_chain.append() below
626
- line_delta.start = delta_chain.get(-1).end
627
-
628
- delta_chain.append(line_delta)
629
-
630
- # check if line chain is a loop
631
- if line_chain.get(0).start == line_chain.get(-1).end:
632
- # fix delta chain
633
- intersect = self.line_intersection(delta_chain.get(0), delta_chain.get(-1))
634
- if intersect is not None:
635
- # update start of loop
636
- start_loop = delta_chain.get(0)
637
- start_loop.start = intersect
638
- delta_chain.set(0, start_loop)
639
-
640
- # update end of loop
641
- end_loop = delta_chain.get(-1)
642
- end_loop.end = intersect
643
- delta_chain.set(-1, end_loop)
644
-
645
- # linear_power(self, pixel: UInt8, maxpower: int, offset: int = 0, invert: bool = True) -> int
646
- inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"])
575
+ delta_chain = LineSegmentChain.delta_chain(line_chain, step)
576
+ # apply alpha channel - when available - to inverted b&w value
577
+ inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"]) * alpha
647
578
  self.append_line_chain(delta_chain, step, inverse_bw)
579
+ # update 'name_id' boundingbox
580
+ for line in delta_chain:
581
+ boundingbox.update(line.start)
582
+ boundingbox.update(line.end)
648
583
  else:
649
- inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"]) if len(stroke_color) else None
584
+ # apply alpha channel - when available - to inverted b&w value
585
+ inverse_bw = Image2gcode.linear_power(css_color.parse_css_color2bw8(stroke_color), self.settings["maximum_image_laser_power"]) * alpha if len(stroke_color) else None
650
586
  self.append_line_chain(line_chain, step, inverse_bw)
587
+ # update 'name_id' boundingbox
588
+ for line in line_chain:
589
+ boundingbox.update(line.start)
590
+ boundingbox.update(line.end)
591
+
592
+ if not self.settings["nofill"] and fill_color is not None:
593
+ # fill a path
594
+ # this is done in 6 steps:
595
+ # step 1: create two raster images matching the bbox
596
+ # step 2: add marker lines just inside the line chains of the path
597
+ # step 3: scan the marker image lines and and apply 'evenodd' fill
598
+ # (fill rule 'nonzero' to be implemented later on see note below)
599
+ # step 4: draw white borders to erase fill overlap
600
+ # step 5: filter stray pixels (to remove noise from the action above)
601
+ # step 6: generate gcode from image_fill
602
+
603
+ # get bounding box info from the path border
604
+ lowerleft = boundingbox.get()[0]
605
+ upperright = boundingbox.get()[1]
606
+
607
+ # normalize origin to (0.0)
608
+ vdXY = Vector(-lowerleft.x, -lowerleft.y)
609
+ dXY = (-lowerleft.x, -lowerleft.y)
610
+
611
+ # get raster image dimensions
612
+ img_height = math.ceil((upperright.y - lowerleft.y)/pixel_size)
613
+ img_width = math.ceil((upperright.x - lowerleft.x)/pixel_size)
614
+
615
+ # default scan error (step 3 below)
616
+ scan_error = 4
617
+
618
+ # step 1: create two raster images matching the bbox
619
+ # init
620
+ image_mark = np.full([img_height + 2, img_width + scan_error], 255, dtype=np.uint8)
621
+ image_fill = np.full([img_height + 2, img_width + scan_error], 0, dtype=np.uint8)
622
+
623
+ # step 2: add marker lines just inside the line chains of the path
624
+ # - determine the inside of the line chain
625
+ # - draw marker lines
626
+ for line_chain in path_curves[name_id]:
627
+ # Note that a svg object with a specific name_id can have multiple line_chains that
628
+ # together, define one shape (circumference). When this the case the boundingbox
629
+ # inside/outside method below is not fullproof. This can be solved to stitch together
630
+ # the line chain parts (TODO)
631
+
632
+ # - determine the inside of the line chain
633
+ # Compare the bbox of the line chain with that of a delta line chain a
634
+ # fixed distance (offset) from the base line chain. Initially we do not know if the
635
+ # delta line chain is inside or outside the base line chain, but when we compare
636
+ # the bbox sizes we know.
637
+ # Note that this method can be used to determine if a line chain is drawn clockwise
638
+ # or anti-clockwise because a posive delta offset should be 'outside' the line chain
639
+ # in this case (depending on the definition/calculation of the delta function).
640
+ # we can use this to implement the other svg fill rule: 'nonzero'.
641
+ bbox = Boundingbox()
642
+ for line in line_chain:
643
+ bbox.update(line.start + vdXY)
644
+ bbox.update(line.end + vdXY)
645
+ bbox_size = bbox.size()
646
+
647
+ bbox = Boundingbox()
648
+ delta_chain = LineSegmentChain.delta_chain(line_chain, pixel_size * 2)
649
+ for line in delta_chain:
650
+ bbox.update(line.start + vdXY)
651
+ bbox.update(line.end + vdXY)
652
+ bbox_deltasize = bbox.size()
653
+
654
+ # compare bboxes and set inside offset
655
+ inside = 1
656
+ if bbox_deltasize > bbox_size:
657
+ inside = -1
658
+
659
+ # Note that some tuning is going on here.
660
+ # This can be remedied in several ways:
661
+ # - use draw lines that have a thickness?
662
+ # - use a higher resolution (pixel grid) to reduce the 'rounding' errors
663
+ offsets = [inside * .5 * pixel_size, inside * pixel_size, inside * 1.5 * pixel_size, inside * 2 * pixel_size]
664
+ # Note that the system sometimes returns line_chains having 1 point and thus having no size, this is 'solved'
665
+ # below, but should not happen (TODO). It is also assumed that line_chain parts that define one shape have
666
+ # similar sizes (TODO).
667
+ if bbox_size > 1 and bbox_size < 50:
668
+ # small area, less margin for error
669
+ scan_error = 1
670
+ del offsets[-2:]
671
+
672
+ ## direct gcode: update this, see step 3 note
673
+ # draw border lines of a specific marker color within line chain
674
+ # Note that the marker values are just a choice and only have to be consistent
675
+ # with the algorithm used (step 3)
676
+ if bbox_size > 1:
677
+ for offset in offsets:
678
+ # make a line chain just one pixel inside the base (step 0) line chain
679
+ delta_chain = LineSegmentChain.delta_chain(line_chain, offset)
680
+ for line in delta_chain:
681
+ draw_line(image_mark, line.start + vdXY, line.end + vdXY, 128)
682
+
683
+ # draw the line chain border using another marker color
684
+ for line in line_chain:
685
+ draw_line(image_mark, line.start + vdXY, line.end + vdXY, 10)
686
+
687
+ # step 3: scan the marker image lines and and apply 'evenodd' fill
688
+ # (fill rule 'nonzero' to be implemented later on)
689
+ # for each line (y):
690
+ # for each point (x) on the line:
691
+ # scan from left to right or reverse:
692
+ # for markers (border,inside border) and apply rule
693
+ # 'evenodd' to fill when an even number of borders
694
+ # is crossed, untill odd
695
+ # Note that the image 'image_fill' is filled not 'image_mark' to make sure marks stay in place.
696
+
697
+ # Note that the fill algorithm can be adapted to support direct rendering of gcode (instead of rendering via 'image_fill' and
698
+ # function 'image2gcode', converting a raster image to gcode). This reduces the number of steps needed: steps 4, 5 and 6 can
699
+ # be left out. It does need a few adaptions in the previous steps, namely drawing the line chain border (color '10') in full width
700
+ # (instead of 1 pixel) and draw the 'inside' line (color 128) one pixel further. In addition to this uncomment the '## direct gcode'
701
+ # lines of step 3 to activate gcode rendering.
702
+
703
+ ## direct gcode
704
+ ## code = [f"\n; fill '{name_id}'"]
705
+ ## code += [self.interface.set_laser_power_value(Image2gcode.linear_power(fill_color, self.settings["maximum_image_laser_power"]))]
706
+
707
+ go_right = True
708
+ # start scanning to the right
709
+ for y in range(image_mark.shape[0]):
710
+ evenodd = 0
711
+
712
+ if go_right:
713
+ # scan to the right
714
+ x = 0
715
+ start = None
716
+ while x < image_mark.shape[1]:
717
+ if image_mark[y,x] == 10 :
718
+ # found a border
719
+ x_b = x
720
+ ## direct gcode
721
+ ## update line below, to be able to skip empty (value 255) pixels
722
+ # scan border, possibly having multiple pixels.
723
+ while x_b < image_mark.shape[1] and image_mark[y,x_b] == 10:
724
+ x_b += 1
725
+
726
+ xscan_b = x - 1
727
+ while xscan_b > 0 and xscan_b > (x - scan_error) and (image_mark[y,xscan_b] != 10 and image_mark[y,xscan_b] != 128):
728
+ xscan_b -= 1
729
+ xscan_a = x_b
730
+ while xscan_a < image_mark.shape[1] and xscan_a < (x_b + scan_error) and (image_mark[y,xscan_a] != 10 and image_mark[y,xscan_a] != 128):
731
+ xscan_a += 1
732
+
733
+ if xscan_b >= 0 and image_mark[y,xscan_b] == 128:
734
+ # found border
735
+ if evenodd % 2 == 0:
736
+ start = (x * pixel_size, y * pixel_size)
737
+ ## direct gcode
738
+ ## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
739
+ else:
740
+ draw_line(image_fill, start + dXY, (x * pixel_size, y * pixel_size) + dXY, fill_color)
741
+ ## direct gcode
742
+ ## code += [self.interface.linear_move(x * pixel_size - dXY[0], y * pixel_size - dXY[1])]
743
+ evenodd = evenodd + 1
744
+ if xscan_a < image_mark.shape[1] and image_mark[y,xscan_a] == 128:
745
+ # found border
746
+ if evenodd % 2 == 0:
747
+ start = (x * pixel_size, y * pixel_size)
748
+ ## direct gcode
749
+ ## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
750
+ else:
751
+ draw_line(image_fill, start + dXY, (x_b * pixel_size, y * pixel_size) + dXY, fill_color)
752
+ ## direct gcode
753
+ ## code += [self.interface.linear_move(x_b * pixel_size - dXY[0], y * pixel_size - dXY[1])]
754
+ evenodd = evenodd + 1
755
+ if x_b > x:
756
+ x = x_b - 1
757
+ x += 1
758
+ else:
759
+ # scan to the left
760
+ start = None
761
+ x = image_mark.shape[1] - 1
762
+ while x >= 0:
763
+ if image_mark[y,x] == 10:
764
+ # found a border
765
+ ## direct gcode
766
+ ## update line below, to be able to skip empty (value 255) pixels
767
+ # scan border, possibly having multiple pixels.
768
+ x_b = x
769
+ while x_b >= 0 and image_mark[y,x_b] == 10:
770
+ x_b -= 1
771
+
772
+ xscan_b = x_b
773
+ while xscan_b > 0 and xscan_b > (x_b - scan_error) and (image_mark[y,xscan_b] != 10 and image_mark[y,xscan_b] != 128):
774
+ xscan_b -= 1
775
+ xscan_a = x + 1
776
+ while xscan_a < image_mark.shape[1] and xscan_a < (x + scan_error) and (image_mark[y,xscan_a] != 10 and image_mark[y,xscan_a] != 128):
777
+ xscan_a += 1
778
+
779
+ if xscan_a < image_mark.shape[1] and image_mark[y,xscan_a] == 128:
780
+ # found border
781
+ if evenodd % 2 == 0:
782
+ start = (x * pixel_size, y * pixel_size)
783
+ ## direct gcode
784
+ ## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
785
+ else:
786
+ draw_line(image_fill, start + dXY, (x * pixel_size, y * pixel_size) + dXY, fill_color)
787
+ ## direct gcode
788
+ ## code += [self.interface.linear_move(x * pixel_size - dXY[0], y * pixel_size - dXY[1])]
789
+ evenodd = evenodd + 1
790
+ if xscan_b >= 0 and image_mark[y,xscan_b] == 128:
791
+ # found border
792
+ if evenodd % 2 == 0:
793
+ start = (x * pixel_size, y * pixel_size)
794
+ ## direct gcode
795
+ ## code += [self.interface.rapid_move(start[0] - dXY[0], start[1] - dXY[1])]
796
+ else:
797
+ draw_line(image_fill, start + dXY, (x_b * pixel_size, y * pixel_size) + dXY, fill_color)
798
+ ## direct gcode
799
+ ## code += [self.interface.linear_move(x_b * pixel_size - dXY[0], y * pixel_size - dXY[1])]
800
+ evenodd = evenodd + 1
801
+ if x_b < x:
802
+ x = x_b + 1
803
+ x -= 1
804
+
805
+ # switch scan direction
806
+ go_right = not go_right
807
+
808
+ ## direct gcode
809
+ ## self.body.extend(code)
810
+
811
+ ## direct gcode: ignore this step
812
+ # step 4: draw white borders to erase fill overlap
813
+ for line_chain in path_curves[name_id]:
814
+ for step in steps:
815
+ if step:
816
+ delta_chain = LineSegmentChain.delta_chain(line_chain, step)
817
+ for line in delta_chain:
818
+ draw_line(image_fill, line.start + vdXY, line.end + vdXY, 0)
819
+ else:
820
+ for line in line_chain:
821
+ draw_line(image_fill, line.start + vdXY, line.end + vdXY, 0)
822
+
823
+ ## direct gcode: ignore this step
824
+ # step 5: filter stray pixels (to remove noise from the action above)
825
+ for y in range(image_fill.shape[0]):
826
+ for x in range(image_fill.shape[1]):
827
+ if image_fill[y,x] != 0:
828
+ if x > 1 and x < (image_fill.shape[1] - 1) and y > 1 and y < (image_fill.shape[0] - 1):
829
+ if image_fill[y,x+1] == 0 and image_fill[y,x-1] == 0 and image_fill[y+1,x] == 0 and image_fill[y-1,x] == 0:
830
+ image_fill[y,x] = 0
831
+
832
+ ## direct gcode: ignore this step
833
+ # step 6: generate gcode from image_fill
834
+ img_attrib = {}
835
+ img_attrib['id'] = name_id
836
+ #img_attrib['gcode_speedmoves'] = 0
837
+ img_attrib['x'] = -dXY[0]
838
+ img_attrib['y'] = -dXY[1]
839
+ img_attrib['noinvert'] = True
840
+
841
+ ## direct gcode: ignore this step
842
+ # start gcode fill
843
+ self.body.extend([f"\n;fill '{name_id}'"])
844
+ self.image2gcode(img_attrib, image_fill)
651
845
 
652
846
  def check_axis_maximum_travel(self):
653
847
  return self.settings["x_axis_maximum_travel"] is not None and self.settings["y_axis_maximum_travel"] is not None
@@ -192,7 +192,7 @@ def hex2rgb(hexcolor: str) -> [int,int,int]:
192
192
 
193
193
  return [r,g,b]
194
194
 
195
- def parse_css_color(color: str) -> [int,int,int]:
195
+ def parse_css_color(color: str) -> [int,int,int,float]:
196
196
  """
197
197
  parse css color to rgb
198
198
  (definition from https://www.w3.org/TR/css-color-3/)
@@ -229,7 +229,8 @@ def parse_css_color(color: str) -> [int,int,int]:
229
229
  else:
230
230
  b = int(rgb[commas_pos[1]+1:])
231
231
 
232
- rgbcolor = [r * a, g * a, b * a]
232
+ #rgbcolor = [r * a, g * a, b * a]
233
+ rgbcolor = [r,g,b,a]
233
234
 
234
235
  else:
235
236
  hsla = re.search(f"hsl(a)?\([0-9]+,[0-9]+,[0-9]+(,{float_re})?", color)
@@ -249,7 +250,7 @@ def parse_css_color(color: str) -> [int,int,int]:
249
250
  l = int(hsl[commas_pos[1]+1:])
250
251
 
251
252
  rgbcolor = hsl2rgb(h, s, l)
252
- rgbcolor = [int(rgbcolor[0] * 255 * a), int(rgbcolor[1] * 255 * a), int(rgbcolor[2] * 255 * a)]
253
+ rgbcolor = [int(rgbcolor[0] * 255), int(rgbcolor[1] * 255), int(rgbcolor[2] * 255), a]
253
254
 
254
255
  if not rgbcolor:
255
256
  logger.warn(f"Not a valid css color: '{color}', color set to 'black'!")
@@ -267,7 +268,7 @@ def rgb24tobw24(rgb24: int) -> int:
267
268
  bnw = int(r * 0.299 + g * 0.587 + b * 0.114) & 0xff;
268
269
  return (bnw << 16) | (bnw << 8) | bnw;
269
270
 
270
- def rgb24tobw8(rgb24: [int,int,int]) -> int:
271
+ def rgb24tobw8(rgb24: [int,int,int,float]) -> int:
271
272
  """
272
273
  convert a 24 bit rgb (color) value to 8 bit grayscale
273
274
  """
@@ -11,7 +11,7 @@ from svg2gcode.svg_to_gcode import TOLERANCES
11
11
  def tolerance_constrain(value, maximum, minimum, tolerance=TOLERANCES["operation"]):
12
12
  """
13
13
  Constrain a value between if it surpasses a limit and is within operational tolerance of the limit. Else return the
14
- value. Useful if you want to correct for flatting point errors but still want to raise an exception if the value is
14
+ value. Useful if you want to correct for floating point errors but still want to raise an exception if the value is
15
15
  out-of-bounds for a different reason.
16
16
  """
17
17
 
@@ -49,6 +49,11 @@ def line_intersect(p1, c1, p2, c2):
49
49
  return (((c1_c - p1_c) * p1 - (c1 - p1) * p1_c) * (c2 - p2) - ((c2_c - p2_c) * p2 - (c2 - p2) * p2_c) * (
50
50
  c1 - p1)) / ((c2 - p2) * (c1_c - p1_c) - (c1 - p1) * (c2_c - p2_c))
51
51
 
52
+ def line_length(A: (float,float),B: (float,float)):
53
+ """ Pythagoras """
54
+ # |Ax - Bx|^2 + |Ay - By|^2 = C^2
55
+ # distance = √C^2
56
+ return math.sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2)
52
57
 
53
58
  def is_on_mid_perpendicular(z, a, b):
54
59
  """Check if a point z is on the line which is perpendicular to ab and passes through the segment's midpoint"""
@@ -8,12 +8,12 @@ from svg2gcode.svg_to_gcode.geometry import Curve
8
8
  class EllipticalArc(Curve):
9
9
  """The EllipticalArc class inherits from the abstract Curve class and describes an elliptical arc."""
10
10
 
11
- __slots__ = 'center', 'radii', 'rotation', 'start_angle', 'sweep_angle', 'end_angle', 'transformation'
11
+ __slots__ = 'center', 'radii', 'rotation', 'start_angle', 'sweep_angle', 'end_angle', 'transformation', 'path_attrib'
12
12
 
13
13
  # ToDo apply transformation beforehand (in Path) for consistency with other geometric objects. If you (the reader)
14
14
  # know how to easily apply an affine transformation to an ellipse feel free to make a pull request.
15
15
  def __init__(self, center: Vector, radii: Vector, rotation: float, start_angle: float, sweep_angle: float,
16
- transformation: None):
16
+ transformation: None, path_attrib = None):
17
17
 
18
18
  # Assign and verify arguments
19
19
  self.center = center
@@ -22,6 +22,7 @@ class EllipticalArc(Curve):
22
22
  self.start_angle = start_angle
23
23
  self.sweep_angle = sweep_angle
24
24
  self.transformation = transformation
25
+ self.path_attrib = path_attrib
25
26
 
26
27
  # Calculate missing data
27
28
  self.end_angle = start_angle + sweep_angle
@@ -32,7 +33,8 @@ class EllipticalArc(Curve):
32
33
 
33
34
  def __repr__(self):
34
35
  return f"EllipticalArc(start: {self.start}, end: {self.end}, center: {self.center}, radii: {self.radii}," \
35
- f" rotation: {self.rotation}, start_angle: {self.start_angle}, sweep_angle: {self.sweep_angle})"
36
+ f" rotation: {self.rotation}, start_angle: {self.start_angle}, sweep_angle: {self.sweep_angle}," \
37
+ f" transformation: {self.transformation}, attrib: {self.path_attrib})"
36
38
 
37
39
  def point(self, t):
38
40
  angle = formulas.linear_map(self.start_angle, self.end_angle, t)
@@ -0,0 +1,111 @@
1
+
2
+ import math
3
+
4
+ from svg2gcode.svg_to_gcode.geometry import Vector
5
+ from svg2gcode.svg_to_gcode.geometry import Curve
6
+ from svg2gcode.svg_to_gcode import formulas
7
+
8
+
9
+ # A line segment
10
+ class Line(Curve):
11
+ """The Line class inherits from the abstract Curve class and describes a straight line segment."""
12
+
13
+ __slots__ = 'slope', 'offset', 'path_attrib'
14
+
15
+ def __init__(self, start, end, path_attrib = None):
16
+ self.start = start
17
+ self.end = end
18
+
19
+ self.slope = formulas.line_slope(start, end)
20
+ self.offset = formulas.line_offset(start, end)
21
+ self.path_attrib = path_attrib
22
+
23
+ def __repr__(self):
24
+ return f"Line(start:{self.start}, end:{self.end}, slope:{self.slope}, offset:{self.offset}, attrib:{self.path_attrib})"
25
+
26
+ def length(self):
27
+ #return abs(self.start - self.end)
28
+ return formulas.line_length(self.start,self.end)
29
+
30
+ def point(self, t):
31
+ x = self.start.x + t * (self.end.x - self.start.x)
32
+ y = self.slope * x + self.offset
33
+
34
+ return Vector(x, y)
35
+
36
+ def derivative(self, t):
37
+ return self.slope
38
+
39
+ @staticmethod
40
+ def offset_line(offset: float, line, precision = 6) -> (Vector,Vector):
41
+ """
42
+ Offset a line perpendicular to the given line.
43
+ """
44
+ def perpendicular(line) -> (int,int):
45
+ """
46
+ Get sign of vector perpendicular to the given line,
47
+ pointing outwards for a clockwise rotation.
48
+ Return (sign x, sign y)
49
+ """
50
+ d_x = line.end.x - line.start.x
51
+ d_y = line.end.y - line.start.y
52
+ # normalize to values 0 or 1 (with sign)
53
+ d_x = 0 if d_x == 0 else d_x/abs(d_x)
54
+ d_y = 0 if d_y == 0 else d_y/abs(d_y)
55
+ return (-d_y, d_x)
56
+
57
+ delta_sign = perpendicular(line)
58
+
59
+ if line.slope == 0:
60
+ # vertical line
61
+ delta_x = 0
62
+ delta_y = delta_sign[1] * offset
63
+ elif line.slope == 1:
64
+ # horizontal line
65
+ delta_x = delta_sign[0] * offset
66
+ delta_y = 0
67
+ else:
68
+ # calculate orthogonal line of 'offset' length:
69
+ #
70
+ # (1) y = 1/slope * x (line formula )
71
+ # (2) x^2 + y^2 = offset^2 (Pythagoras )
72
+ # (3) x^2 + (1/slope * x)^2 = offset^2 (substitue 'y' )
73
+ # (4) (1 + (1/slope)^2) * x^2 = offset^2 (reorder )
74
+ # (5) x^2 = offset^2 / (1 + (1/slope)^2) (get 'x' )
75
+ # slope
76
+ inv_slope = abs((1/line.slope))
77
+ delta_x = round(math.sqrt((offset ** 2) / (1 + inv_slope ** 2)), precision)
78
+ delta_y = round(inv_slope * delta_x, precision)
79
+
80
+ # normalize offset
81
+ offset = 0 if offset == 0 else offset/abs(offset)
82
+ delta_x = delta_sign[0] * offset * delta_x
83
+ delta_y = delta_sign[1] * offset * delta_y
84
+
85
+ # add delta vector to line start and end vectors
86
+ return Line(line.start + Vector(delta_x, delta_y), line.end + Vector(delta_x, delta_y))
87
+
88
+ @staticmethod
89
+ def line_intersection(l1, l2, precision = 6) -> Vector:
90
+ """
91
+ Line intersection (https://en.wikipedia.org/wiki/Line–line_intersection)
92
+ Return intersection point or None when lines do not intersect.
93
+
94
+ """
95
+ x1, y1 = l1.start
96
+ x2, y2 = l1.end
97
+ x3, y3 = l2.start
98
+ x4, y4 = l2.end
99
+
100
+ # denomintor
101
+ #d = round((x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4), self.precision)
102
+ d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4)
103
+
104
+ # Note that the cutoff below is needed to eliminate propagation of rounding errors.
105
+ # The equations below are inherently sensitive to rounding errors at certain inputs.
106
+ if abs(d) < 10**-precision:
107
+ # no intersection
108
+ return None
109
+
110
+ return Vector( round(((x1*y2 - y1*x2) * (x3 - x4) - (x1 - x2) * (x3*y4 - y3*x4))/d, precision),
111
+ round(((x1*y2 - y1*x2) * (y3 - y4) - (y1 - y2) * (x3*y4 - y3*x4))/d, precision) )
@@ -28,6 +28,65 @@ class LineSegmentChain(Chain):
28
28
 
29
29
  self._curves.append(line2)
30
30
 
31
+ @staticmethod
32
+ def clockwise(line_chain, delta: float = .1) -> bool:
33
+ """
34
+ Return true when line chain rotates clockwise using sample delta
35
+
36
+ """
37
+ # bbox_line_chain = bbox(line_chain)
38
+ # bbox_delta_chain = bbox(delta_chain(line_chain, delta))
39
+ # if bbox_delta_chain > bbox_line_chain:
40
+ # return true
41
+ # return false
42
+ pass
43
+
44
+ @staticmethod
45
+ def delta_chain(line_chain, offset: float) -> "LineSegmentChain":
46
+ """
47
+ Return a line chain offset to the given chain. If the chain rotates clockwise a positive offset
48
+ results in a delta chain outwards of (enclosing) the given line chain.
49
+
50
+ """
51
+ delta_chain = LineSegmentChain()
52
+
53
+ for line in line_chain:
54
+ line_delta = Line.offset_line(offset, line)
55
+
56
+ if delta_chain.chain_size():
57
+ # calculate intersection of previous line and current line
58
+ intersect = Line.line_intersection(delta_chain.get(-1), line_delta)
59
+ if intersect is not None:
60
+ # set prev_line.end to intersect
61
+ prev_line = delta_chain.get(-1)
62
+ prev_line.end = intersect
63
+ delta_chain.set(-1, prev_line)
64
+
65
+ # set line_delta start to intersect
66
+ line_delta.start = intersect
67
+ else:
68
+ # connect line, to prevent ValueErrors from delta_chain.append() below
69
+ line_delta.start = delta_chain.get(-1).end
70
+
71
+ delta_chain.append(line_delta)
72
+
73
+ # check if line chain is a loop
74
+ if line_chain.get(0).start == line_chain.get(-1).end:
75
+ # fix delta chain
76
+ intersect = Line.line_intersection(delta_chain.get(0), delta_chain.get(-1))
77
+ if intersect is not None:
78
+ # update start of loop
79
+ start_loop = delta_chain.get(0)
80
+ start_loop.start = intersect
81
+ delta_chain.set(0, start_loop)
82
+
83
+ # update end of loop
84
+ end_loop = delta_chain.get(-1)
85
+ end_loop.end = intersect
86
+ delta_chain.set(-1, end_loop)
87
+
88
+ return delta_chain
89
+
31
90
  @staticmethod
32
91
  def line_segment_approximation(shape, increment_growth=11 / 10, error_cap=None, error_floor=None)\
33
92
  -> "LineSegmentChain":
@@ -261,7 +261,7 @@ class Path:
261
261
  radii, center, start_angle, sweep_angle = formulas.endpoint_to_center_parameterization(
262
262
  start, end, radii, rotation_rad, large_arc_flag, sweep_flag)
263
263
 
264
- arc = EllipticalArc(center, radii, rotation_rad, start_angle, sweep_angle, transformation=self.transformation)
264
+ arc = EllipticalArc(center, radii, rotation_rad, start_angle, sweep_angle, transformation=self.transformation, path_attrib=self.path_attrib)
265
265
 
266
266
  self.current_point = end
267
267
  return arc
@@ -1,33 +0,0 @@
1
- from svg2gcode.svg_to_gcode.geometry import Vector
2
- from svg2gcode.svg_to_gcode.geometry import Curve
3
- from svg2gcode.svg_to_gcode import formulas
4
-
5
-
6
- # A line segment
7
- class Line(Curve):
8
- """The Line class inherits from the abstract Curve class and describes a straight line segment."""
9
-
10
- __slots__ = 'slope', 'offset', 'path_attrib'
11
-
12
- def __init__(self, start, end, path_attrib = None):
13
- self.start = start
14
- self.end = end
15
-
16
- self.slope = formulas.line_slope(start, end)
17
- self.offset = formulas.line_offset(start, end)
18
- self.path_attrib = path_attrib
19
-
20
- def __repr__(self):
21
- return f"Line(start:{self.start}, end:{self.end}, slope:{self.slope}, offset:{self.offset}, attrib:{self.path_attrib})"
22
-
23
- def length(self):
24
- return abs(self.start - self.end)
25
-
26
- def point(self, t):
27
- x = self.start.x + t * (self.end.x - self.start.x)
28
- y = self.slope * x + self.offset
29
-
30
- return Vector(x, y)
31
-
32
- def derivative(self, t):
33
- return self.slope
File without changes