gcode_parser.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import numpy as np
  2. import gc
  3. class GCodeParser:
  4. def __init__(self, stats):
  5. self.stats = stats
  6. self.offX = stats.get("offX", 0)
  7. self.offY = stats.get("offY", 0)
  8. self.min_pwr = stats.get("min_power", 0)
  9. self.rect_h = stats.get("rect_h", 0)
  10. @staticmethod
  11. def _extract(line, char):
  12. """Extrait la valeur numérique qui suit `char`. Retourne (valeur, True) ou (None, False)."""
  13. pos = line.find(char)
  14. if pos == -1:
  15. return None, False
  16. s = e = pos + 1
  17. while e < len(line) and (line[e].isdigit() or line[e] in '.-+'):
  18. e += 1
  19. if e > s:
  20. try:
  21. return float(line[s:e]), True
  22. except ValueError:
  23. pass
  24. return None, False
  25. @staticmethod
  26. def _is_g0(line):
  27. """True si la ligne contient un G0 (déplacement rapide, pas G0.x)."""
  28. idx = line.find('G0')
  29. if idx == -1:
  30. return False
  31. after = line[idx + 2] if idx + 2 < len(line) else ' '
  32. return after in (' ', '\t', '') or (after.isdigit() and after != '.')
  33. def parse(self, gcode_text):
  34. """Parse principal — retourne (points_array, 0.0, limits).
  35. Corrections vs version précédente :
  36. - G0 : position mémorisée, puissance forcée à 0, EXCLUS des bounds.
  37. Les overscan (ex: X=-2) ne gonflent plus le cadre de rendu.
  38. - parseScmd en double supprimé.
  39. - parseQcmd ajouté (alias propre).
  40. """
  41. if not gcode_text:
  42. return None, 0.0, (0.0, 0.0, 0.0, 0.0)
  43. lines = gcode_text.splitlines()
  44. n_lines = len(lines)
  45. if n_lines == 0:
  46. return None, 0.0, (0.0, 0.0, 0.0, 0.0)
  47. gc_was_enabled = gc.isenabled()
  48. gc.disable()
  49. points_array = np.zeros((n_lines * 2, 5), dtype=np.float32)
  50. idx_point = 0
  51. curr_x = curr_y = 0.0
  52. curr_f = 1000.0
  53. curr_pwr = 0.0
  54. min_x = min_y = float('inf')
  55. max_x = max_y = float('-inf')
  56. for line_idx, raw_line in enumerate(lines, start=1):
  57. line = raw_line.strip().upper()
  58. if not line or line.startswith(('(', ';')):
  59. continue
  60. is_rapid = self._is_g0(line)
  61. changed = False
  62. power_changed = False
  63. # ── puissance (Q prioritaire sur S) ───────────────────────────
  64. for char_p in ('Q', 'S'):
  65. val, found = self._extract(line, char_p)
  66. if found:
  67. # G0 : on force la puissance à 0 même si Q/S est présent
  68. effective = 0.0 if is_rapid else val
  69. if effective != curr_pwr:
  70. curr_pwr = effective
  71. power_changed = True
  72. break
  73. # ── feedrate ──────────────────────────────────────────────────
  74. val_f, found_f = self._extract(line, 'F')
  75. if found_f:
  76. curr_f = val_f
  77. # ── coordonnées ───────────────────────────────────────────────
  78. val_x, found_x = self._extract(line, 'X')
  79. if found_x:
  80. curr_x = val_x
  81. changed = True
  82. val_y, found_y = self._extract(line, 'Y')
  83. if found_y:
  84. curr_y = val_y
  85. changed = True
  86. # ── enregistrement ────────────────────────────────────────────
  87. if changed or power_changed:
  88. pwr_to_store = curr_pwr if curr_pwr > self.min_pwr else 0.0
  89. # Bounds : tous les mouvements G1 (pas les G0 d'overscan)
  90. # On inclut les Q=0 de début/fin de ligne car ils définissent
  91. # la vraie largeur du passage laser (nécessaire pour le buffer renderer)
  92. if changed and not is_rapid:
  93. if curr_x < min_x: min_x = curr_x
  94. if curr_x > max_x: max_x = curr_x
  95. if curr_y < min_y: min_y = curr_y
  96. if curr_y > max_y: max_y = curr_y
  97. px = curr_x - self.offX
  98. py = curr_y - self.offY
  99. if idx_point < points_array.shape[0]:
  100. points_array[idx_point, :] = (px, py, pwr_to_store,
  101. float(line_idx), curr_f)
  102. idx_point += 1
  103. if gc_was_enabled:
  104. gc.enable()
  105. if idx_point == 0:
  106. return None, 0.0, (0.0, 0.0, 0.0, 0.0)
  107. # Fallback si aucun G1 trouvé (gcode sans laser)
  108. if min_x == float('inf'):
  109. pts = points_array[:idx_point]
  110. min_x, max_x = float(pts[:, 0].min()), float(pts[:, 0].max())
  111. min_y, max_y = float(pts[:, 1].min()), float(pts[:, 1].max())
  112. return points_array[:idx_point], 0.0, (min_x, max_x, min_y, max_y)
  113. # ──────────────────────────────────────────────────────────────────────────
  114. def parseScmd(self, gcode_text):
  115. """Parser mode S — repère image (Y inversé via rect_h)."""
  116. lines = gcode_text.splitlines()
  117. n_lines = len(lines)
  118. points_array = np.zeros((n_lines, 5), dtype=np.float32)
  119. idx_point = 0
  120. curr_x = curr_y = 0.0
  121. curr_f = 1000.0
  122. curr_pwr = 0.0
  123. for line_idx, raw_line in enumerate(lines, start=1):
  124. line = raw_line.strip().upper()
  125. if not line or line.startswith(('(', ';')):
  126. continue
  127. is_rapid = self._is_g0(line)
  128. changed = False
  129. for char_p in ('S', 'Q'):
  130. val, found = self._extract(line, char_p)
  131. if found:
  132. curr_pwr = 0.0 if is_rapid else val
  133. break
  134. val_f, found_f = self._extract(line, 'F')
  135. if found_f:
  136. curr_f = val_f
  137. val_x, found_x = self._extract(line, 'X')
  138. if found_x:
  139. curr_x = val_x
  140. changed = True
  141. val_y, found_y = self._extract(line, 'Y')
  142. if found_y:
  143. curr_y = val_y
  144. changed = True
  145. if changed and not is_rapid:
  146. pwr_to_store = curr_pwr if curr_pwr > self.min_pwr else 0.0
  147. px = curr_x - self.offX
  148. py = self.rect_h - (curr_y - self.offY)
  149. points_array[idx_point] = (px, py, pwr_to_store,
  150. float(line_idx), curr_f)
  151. idx_point += 1
  152. if idx_point == 0:
  153. return None, 0.0
  154. return points_array[:idx_point], 0.0
  155. def parseQcmd(self, gcode_text):
  156. """Parser mode Q — alias propre de parseScmd (Q déjà géré dedans)."""
  157. return self.parseScmd(gcode_text)
  158. def parse_auto(self, gcode_text, mode='S'):
  159. """Wrapper de détection automatique. mode: 'S' ou 'Q'."""
  160. if not gcode_text:
  161. return None, 0.0
  162. if mode.upper() == 'Q':
  163. return self.parseQcmd(gcode_text)
  164. return self.parseScmd(gcode_text)