gcode_engine_numba.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. """
  2. A.L.I.G. Project - Core Engine
  3. Industrial Raster Engine Version
  4. """
  5. # TO DO : improve file siez estimation
  6. import numpy as np
  7. from PIL import Image
  8. import io
  9. try:
  10. from numba import njit
  11. _NUMBA = True
  12. except ImportError:
  13. # Fallback sans Numba — fonctionne mais plus lent
  14. def njit(*args, **kwargs):
  15. return lambda f: f
  16. _NUMBA = False
  17. # ─────────────────────────────────────────────────────────────────
  18. # Fonction JIT (compilée Numba au 1er appel, ~50ms de warm-up)
  19. # Calcule les segments de puissance pour une ligne de scan.
  20. # Retourne deux arrays : positions et puissances des points de rupture.
  21. # ─────────────────────────────────────────────────────────────────
  22. @njit(cache=True)
  23. def _compute_segments(vals, targets, start_pos, ctrl_max):
  24. """
  25. Parcourt les pixels et fusionne les segments de même puissance.
  26. Retourne (seg_positions, seg_powers, n_segs).
  27. Pré-alloue au maximum (len(vals)) pour éviter les resize.
  28. """
  29. n = len(vals)
  30. # Pré-allocation max
  31. seg_pos = np.empty(n + 1, dtype=np.float64)
  32. seg_pwr = np.empty(n + 1, dtype=np.float64)
  33. n_segs = 0
  34. current_p = vals[0]
  35. current_pos = start_pos
  36. for i in range(n):
  37. p_val = vals[i]
  38. target = targets[i]
  39. if abs(p_val - current_p) > 0.001:
  40. # Changement de puissance : enregistrer le segment
  41. if abs(target - current_pos) > 0.0001:
  42. seg_pos[n_segs] = target
  43. seg_pwr[n_segs] = current_p
  44. n_segs += 1
  45. current_pos = target
  46. current_p = p_val
  47. # Dernier segment
  48. if n > 0:
  49. seg_pos[n_segs] = targets[n - 1]
  50. seg_pwr[n_segs] = current_p
  51. n_segs += 1
  52. return seg_pos, seg_pwr, n_segs
  53. class GCodeEngine:
  54. def __init__(self):
  55. self.matrix = None
  56. self.stats = {}
  57. self.last_gcode_body = []
  58. # =========================================================
  59. # IMAGE PROCESSING PIPELINE (Industrial Stable Raster Core)
  60. # =========================================================
  61. def process_image_logic(self, image_path, s, source_img_cache=None):
  62. """
  63. Traite l'image et calcule toute la géométrie.
  64. Force EXACTEMENT la dimension choisie par l'utilisateur.
  65. """
  66. # -------------------------------------------------
  67. # 1) PARAMÈTRES SÉCURISÉS
  68. # -------------------------------------------------
  69. l_step_val = max(0.0001, float(s.get("line_step", 0.1)))
  70. dpi_val = max(1, int(s.get("dpi", 254)))
  71. # Pas théorique basé sur le DPI
  72. theoretical_scan_step = 25.4 / dpi_val
  73. raster_mode = str(s.get("raster_mode", "horizontal")).strip().lower()
  74. feedrate = max(1.0, float(s.get("feedrate", 3000)))
  75. force_dim = s.get("force_dim", False)
  76. target_dim = float(s.get("ui_dimension", 10.0))
  77. # -------------------------------------------------
  78. # 2) CHARGEMENT IMAGE
  79. # -------------------------------------------------
  80. try:
  81. img = source_img_cache if source_img_cache else Image.open(image_path).convert("L")
  82. except Exception as e:
  83. print(f"Erreur chargement image: {e}")
  84. return None, None, None, False
  85. orig_w, orig_h = img.size
  86. img_ratio = orig_h / orig_w if orig_w != 0 else 1.0
  87. # -------------------------------------------------
  88. # 3) CALCUL GÉOMÉTRIE (FORCE EXACT DIMENSION)
  89. # -------------------------------------------------
  90. if raster_mode == "horizontal":
  91. # 1. Calcul du nombre de pixels pour la largeur
  92. w_px = max(2, int(round(target_dim / theoretical_scan_step)) + 1)
  93. # 2. Détermination du scan_step (ajusté si force_dim)
  94. if force_dim:
  95. scan_step = target_dim / (w_px - 1)
  96. real_w = target_dim
  97. else:
  98. scan_step = theoretical_scan_step
  99. real_w = (w_px - 1) * scan_step
  100. # 3. La hauteur reste dictée par le ratio et le line_step
  101. real_h = real_w * img_ratio
  102. h_px = max(2, int(round(real_h / l_step_val)) + 1)
  103. real_h = (h_px - 1) * l_step_val
  104. else: # Mode Vertical
  105. # 1. Calcul du nombre de pixels pour la hauteur
  106. h_px = max(2, int(round(target_dim / theoretical_scan_step)) + 1)
  107. # 2. Détermination du scan_step (ajusté si force_dim)
  108. if force_dim:
  109. scan_step = target_dim / (h_px - 1)
  110. real_h = target_dim
  111. else:
  112. scan_step = theoretical_scan_step
  113. real_h = (h_px - 1) * scan_step
  114. # 3. La largeur reste dictée par le ratio et le line_step
  115. real_w = real_h / img_ratio if img_ratio != 0 else real_h
  116. w_px = max(2, int(round(real_w / l_step_val)) + 1)
  117. real_w = (w_px - 1) * l_step_val
  118. # -------------------------------------------------
  119. # 4) LIMITE MÉMOIRE (10MP) - RECALCUL SÉCURISÉ
  120. # -------------------------------------------------
  121. MAX_TOTAL_PIXELS = 10_000_000
  122. current_pixels = w_px * h_px
  123. mem_warn = current_pixels > 2_000_000
  124. if current_pixels > MAX_TOTAL_PIXELS:
  125. scale = np.sqrt(MAX_TOTAL_PIXELS / current_pixels)
  126. w_px = max(2, int(w_px * scale))
  127. h_px = max(2, int(h_px * scale))
  128. # Mise à jour des dimensions réelles avec le scan_step calculé au point 3
  129. if raster_mode == "horizontal":
  130. real_w = (w_px - 1) * scan_step
  131. real_h = (h_px - 1) * l_step_val
  132. else:
  133. real_w = (w_px - 1) * l_step_val
  134. real_h = (h_px - 1) * scan_step
  135. # -------------------------------------------------
  136. # 5) REDIMENSIONNEMENT IMAGE
  137. # -------------------------------------------------
  138. img_resized = img.resize((w_px, h_px), Image.Resampling.BICUBIC)
  139. arr = np.asarray(img_resized, dtype=np.float32) / 255.0
  140. # Inversion laser
  141. if not s.get("invert"):
  142. arr = 1.0 - arr
  143. # Contraste
  144. contrast = float(s.get("contrast", 0))
  145. if contrast != 0:
  146. f = (259 * (contrast + 1.0)) / (255 * (259 - contrast)) * 255
  147. arr = np.clip((arr - 0.5) * f + 0.5, 0, 1)
  148. # Gamma + thermique
  149. gamma = float(s.get("gamma", 1.0))
  150. thermal = float(s.get("thermal", 1.0))
  151. combined_exp = gamma * thermal
  152. if combined_exp != 1.0:
  153. arr = np.power(arr, combined_exp)
  154. # -------------------------------------------------
  155. # 6) QUANTIFICATION
  156. # -------------------------------------------------
  157. QUANT_LEVEL = max(2, int(s.get("gray_steps", 255)))
  158. min_p = float(s.get("min_p", 0))
  159. max_p = float(s.get("max_p", 255))
  160. norm = np.clip(arr, 0, 1)
  161. quant = np.round(norm * (QUANT_LEVEL - 1)) / (QUANT_LEVEL - 1)
  162. matrix = min_p + quant * (max_p - min_p)
  163. matrix *= (arr >= 0.005).astype(np.float32)
  164. # -------------------------------------------------
  165. # 7) OVERSCAN + RECTANGLES
  166. # -------------------------------------------------
  167. overscan_dist = float(s.get("premove", 2.0))
  168. if raster_mode == "horizontal":
  169. num_lines = h_px
  170. dist_per_line = real_w + (2 * overscan_dist)
  171. rect_full = (-overscan_dist, 0, real_w + overscan_dist, real_h)
  172. x_step = scan_step
  173. y_step = l_step_val
  174. else:
  175. num_lines = w_px
  176. dist_per_line = real_h + (2 * overscan_dist)
  177. rect_full = (0, -overscan_dist, real_w, real_h + overscan_dist)
  178. x_step = l_step_val
  179. y_step = scan_step
  180. dist_decalage_total = (num_lines - 1) * l_step_val
  181. # -------------------------------------------------
  182. # 8) ESTIMATION TEMPS (FIABLE)
  183. # -------------------------------------------------
  184. dist_gravure = num_lines * dist_per_line
  185. total_dist = dist_gravure + dist_decalage_total
  186. est_min = (total_dist / feedrate)
  187. # -------------------------------------------------
  188. # 9) ESTIMATION TAILLE GCODE
  189. # -------------------------------------------------
  190. gc_params_est = {
  191. "use_s_mode": s.get("use_s_mode", False),
  192. "raster_mode": raster_mode,
  193. "ctrl_max": max_p
  194. }
  195. est_size_str, _ = self.get_gcode_statistics(matrix, s, gc_params_est)
  196. # -------------------------------------------------
  197. # 10) GEOM FINAL CONSOLIDÉ
  198. # -------------------------------------------------
  199. geom = {
  200. "w_px": w_px,
  201. "h_px": h_px,
  202. "w_px": w_px,
  203. "h_px": h_px,
  204. "real_w": real_w,
  205. "real_h": real_h,
  206. "x_step": x_step,
  207. "y_step": y_step,
  208. "l_step": l_step_val,
  209. "scan_step": scan_step,
  210. "overscan_dist": overscan_dist,
  211. "est_min": est_min,
  212. "rect_burn": (0, 0, real_w, real_h),
  213. "rect_full": rect_full,
  214. "file_size_str": est_size_str,
  215. "raster_mode": raster_mode
  216. }
  217. return matrix, img, geom, mem_warn
  218. # =========================================================
  219. # INDUSTRIAL RASTER GCODE GENERATOR
  220. # =========================================================
  221. # ─────────────────────────────────────────────────────────────────
  222. # Génération vectorisée : NumPy pour les segments + threads pour
  223. # les lignes. hyst_p est local à chaque ligne (pas d'état partagé).
  224. # ─────────────────────────────────────────────────────────────────
  225. def generate_gcode_list(self, matrix, h_px, w_px, l_step, x_st, offX, offY, gc):
  226. """
  227. Génération G-Code optimisée.
  228. La boucle interne (pixels) est vectorisée via NumPy :
  229. np.diff détecte les changements de puissance en O(n) sans boucle Python.
  230. """
  231. e_num = gc.get("e_num", 0)
  232. use_s_mode = gc.get("use_s_mode", False)
  233. ratio = gc.get("ratio", 1.0)
  234. ctrl_max = gc.get("ctrl_max", 255)
  235. pre = gc.get("premove", 2.0)
  236. feed = gc.get("feedrate", 3000)
  237. offset_latence = gc.get("offset_latence", 0.0)
  238. raster_mode = str(gc.get("raster_mode", "horizontal")).lower().strip()
  239. parts = []
  240. W = parts.append
  241. W("G1 F%s" % feed)
  242. if not use_s_mode:
  243. W("M67 E%s Q0.00" % e_num)
  244. W("G4 P0.1")
  245. p_matrix = np.clip(matrix * ratio, 0.0, ctrl_max)
  246. if raster_mode == "horizontal":
  247. outer_range = h_px
  248. inner_count = w_px
  249. step_main = l_step
  250. step_scan = x_st
  251. axis = "X"
  252. else:
  253. outer_range = w_px
  254. inner_count = h_px
  255. step_main = x_st
  256. step_scan = l_step
  257. axis = "Y"
  258. real_scan_dist = (inner_count - 1) * step_scan
  259. # Positions de scan pré-calculées (partagées par toutes les lignes)
  260. scan_pos_fwd = np.arange(1, inner_count + 1) * step_scan # positions fwd
  261. scan_pos_rev = np.arange(inner_count - 1, -1, -1) * step_scan # positions rev
  262. for outer_idx in range(outer_range):
  263. is_fwd = (outer_idx % 2 == 0)
  264. scan_dir = 1 if is_fwd else -1
  265. corr = -offset_latence * scan_dir
  266. if raster_mode == "horizontal":
  267. main_pos = outer_idx * step_main + offY
  268. row_data = p_matrix[(h_px - 1) - outer_idx]
  269. scan_offset = offX
  270. else:
  271. main_pos = outer_idx * step_main + offX
  272. row_data = p_matrix[::-1, outer_idx]
  273. scan_offset = offY
  274. scan_start = (0.0 if is_fwd else real_scan_dist) + scan_offset
  275. scan_end = (real_scan_dist if is_fwd else 0.0) + scan_offset
  276. pre_start = scan_start - pre * scan_dir
  277. pre_end = scan_end + pre * scan_dir
  278. # Positionnement initial
  279. if raster_mode == "horizontal":
  280. W("G1 X%.4f Y%.4f" % (pre_start, main_pos))
  281. else:
  282. W("G1 X%.4f Y%.4f" % (main_pos, pre_start))
  283. start_with_corr = scan_start + corr
  284. if abs(start_with_corr - pre_start) > 0.0001:
  285. if not use_s_mode:
  286. W("M67 E%s Q0.00 G1 %s%.4f" % (e_num, axis, start_with_corr))
  287. else:
  288. W("G1 %s%.4f S0" % (axis, start_with_corr))
  289. # ── Boucle pixel via Numba JIT ────────────────────────────
  290. vals = np.ascontiguousarray(
  291. row_data if is_fwd else row_data[::-1], dtype=np.float64)
  292. base_pos = scan_pos_fwd if is_fwd else scan_pos_rev
  293. targets = np.ascontiguousarray(
  294. base_pos + scan_offset + corr, dtype=np.float64)
  295. seg_pos, seg_pwr, n_segs = _compute_segments(
  296. vals, targets, start_with_corr, ctrl_max)
  297. current_pos = start_with_corr
  298. for si in range(n_segs):
  299. p_val = seg_pwr[si]
  300. seg_end_pos = seg_pos[si]
  301. if abs(seg_end_pos - current_pos) > 0.0001:
  302. if not use_s_mode:
  303. W("M67 E%s Q%.3f G1 %s%.4f" % (e_num, p_val, axis, seg_end_pos))
  304. else:
  305. W("G1 %s%.4f S%.3f" % (axis, seg_end_pos, p_val))
  306. current_pos = seg_end_pos
  307. end_with_corr = scan_end + corr
  308. last_p = float(vals[-1]) if len(vals) > 0 else 0.0
  309. if abs(end_with_corr - current_pos) > 0.0001:
  310. if not use_s_mode:
  311. W("M67 E%s Q%.3f G1 %s%.4f" % (e_num, last_p, axis, end_with_corr))
  312. else:
  313. W("G1 %s%.4f S%.3f" % (axis, end_with_corr, last_p))
  314. current_pos = end_with_corr
  315. # Overscan de sortie
  316. overscan_step = step_scan * 4
  317. dist_to_go = abs(pre_end - current_pos)
  318. num_steps_overscan = int(dist_to_go / overscan_step)
  319. for _ in range(num_steps_overscan):
  320. current_pos += overscan_step * scan_dir
  321. if not use_s_mode:
  322. W("M67 E%s Q0.00 G1 %s%.4f" % (e_num, axis, current_pos))
  323. else:
  324. W("G1 %s%.4f S0" % (axis, current_pos))
  325. if abs(pre_end - current_pos) > 0.0001:
  326. if not use_s_mode:
  327. W("M67 E%s Q0.00 G1 %s%.4f" % (e_num, axis, pre_end))
  328. else:
  329. W("G1 %s%.4f S0" % (axis, pre_end))
  330. return "\n".join(parts) + "\n"
  331. def generate_framing_gcode(self, w, h, offX, offY, power,
  332. feedrate, pause_cmd=None,
  333. use_s_mode=True, e_num=0):
  334. lines = ["( --- FRAMING START --- )"]
  335. off_cmd = "S0" if use_s_mode else f"M67 E{e_num} Q0"
  336. p_cmd = f"S{power:.2f}" if use_s_mode else f"M67 E{e_num} Q{power:.2f}"
  337. # 1. Laser OFF + sync avant déplacement
  338. if not use_s_mode:
  339. lines.append(f"M67 E{e_num} Q0 (Laser OFF)")
  340. lines.append("G4 P0.1 (Sync)")
  341. else:
  342. lines.append(f"{off_cmd} (Laser OFF)")
  343. # 2. Approche à vide vers le point de départ
  344. lines.append(f"G0 X{offX:.3f} Y{offY:.3f} F3000")
  345. # 3. Vitesse de travail
  346. lines.append(f"G1 F{feedrate}")
  347. # 4. Dessin du rectangle
  348. if not use_s_mode:
  349. # M67 AVANT G1 sur chaque segment
  350. lines.append(f"M67 E{e_num} Q{power:.2f} G1 X{offX+w:.3f} Y{offY:.3f}")
  351. lines.append(f"M67 E{e_num} Q{power:.2f} G1 X{offX+w:.3f} Y{offY+h:.3f}")
  352. lines.append(f"M67 E{e_num} Q{power:.2f} G1 X{offX:.3f} Y{offY+h:.3f}")
  353. lines.append(f"M67 E{e_num} Q{power:.2f} G1 X{offX:.3f} Y{offY:.3f}")
  354. else:
  355. lines.append(f"G1 X{offX+w:.3f} Y{offY:.3f} {p_cmd}")
  356. lines.append(f"G1 X{offX+w:.3f} Y{offY+h:.3f} {p_cmd}")
  357. lines.append(f"G1 X{offX:.3f} Y{offY+h:.3f} {p_cmd}")
  358. lines.append(f"G1 X{offX:.3f} Y{offY:.3f} {p_cmd}")
  359. # 5. Extinction finale
  360. if not use_s_mode:
  361. lines.append(f"M67 E{e_num} Q0 G1 (Laser OFF)")
  362. else:
  363. lines.append(f"{off_cmd} (Laser OFF)")
  364. # 6. Pause optionnelle
  365. if pause_cmd:
  366. lines.append(f"{pause_cmd} (Framing done, check position)")
  367. lines.append("( --- FRAMING END --- )")
  368. return "\n".join(lines) + "\n"
  369. def assemble_gcode(self, body, header_custom,
  370. footer_custom, settings, metadata):
  371. e_num = settings["e_num"]
  372. use_s_mode = settings["use_s_mode"]
  373. firing_cmd = metadata["firing_cmd"]
  374. init_safety = (
  375. "M5 S0\nG4 P0.5"
  376. if use_s_mode
  377. else f"M67 E{e_num} Q0.00\nG4 P0.2\nM5\nG4 P0.3"
  378. )
  379. buf = io.StringIO()
  380. buf.write(f"( A.L.I.G. v{metadata['version']} )\n")
  381. buf.write(f"( Mode: {metadata['mode']} )\n")
  382. buf.write(f"( Firing Mode: {metadata['firing_cmd']} )\n")
  383. buf.write(f"( Grayscale Levels: {metadata['gray_steps']} )\n")
  384. buf.write("G21 G90 G17 G94\n")
  385. buf.write(header_custom + "\n")
  386. buf.write(init_safety + "\n\n")
  387. if metadata.get("framing_code"):
  388. buf.write(metadata["framing_code"] + "\n")
  389. buf.write(f"{firing_cmd} ( Re-arming laser )\n")
  390. else:
  391. buf.write(f"{firing_cmd}\n")
  392. buf.write(body)
  393. if not use_s_mode:
  394. buf.write(f"M67 E{e_num} Q0.00\n")
  395. buf.write("\nM5 S0 ( Ensure laser is off )\n")
  396. if footer_custom:
  397. buf.write("\n" + footer_custom + "\n")
  398. buf.write("M30\n")
  399. return buf.getvalue()
  400. def build_final_gcode(self,
  401. matrix,
  402. dims,
  403. offsets,
  404. settings_raw,
  405. text_blocks,
  406. metadata_raw):
  407. # Calcul de la latence (compensation matérielle)
  408. latency_mm = (
  409. settings_raw["feedrate"] *
  410. settings_raw["laser_latency"]
  411. ) / 60000
  412. gc_settings = {
  413. "e_num": settings_raw["e_num"],
  414. "use_s_mode": settings_raw["use_s_mode"],
  415. "ratio": settings_raw["ctrl_max"] / 100.0, # À vérifier selon ton calcul de puissance
  416. "ctrl_max": settings_raw["ctrl_max"],
  417. "premove": settings_raw["premove"],
  418. "feedrate": settings_raw["feedrate"],
  419. "offset_latence": latency_mm,
  420. "raster_mode": settings_raw.get("raster_mode", "horizontal")
  421. }
  422. # Désassemblage du tuple dims (envoyé par generate_gcode)
  423. # Rappel : dims = (h_px, w_px, y_step, x_step)
  424. h_px, w_px, y_st, x_st = dims
  425. offX, offY = offsets
  426. # Appel au générateur de liste de lignes
  427. # On s'assure de passer y_st et x_st dans le bon ordre
  428. gcode_body = self.generate_gcode_list(
  429. matrix,
  430. h_px,
  431. w_px,
  432. y_st, # Anciennement l_step
  433. x_st,
  434. offX,
  435. offY,
  436. gc_settings
  437. )
  438. final_text = self.assemble_gcode(
  439. gcode_body,
  440. text_blocks["header"],
  441. text_blocks["footer"],
  442. gc_settings,
  443. metadata_raw
  444. )
  445. return final_text, latency_mm
  446. def generate_pointing_gcode(self, offX, offY, power,
  447. pause_cmd=None,
  448. use_s_mode=True,
  449. e_num=0):
  450. lines = ["( --- POINTING START --- )"]
  451. if not use_s_mode:
  452. lines.append(f"M67 E{e_num} Q0")
  453. lines.append("G4 P0.1")
  454. lines.append("M5")
  455. lines.append(f"G0 X{offX:.3f} Y{offY:.3f} F3000")
  456. lines.append("M3")
  457. if use_s_mode:
  458. lines.append(f"G1 X{offX + 0.01:.3f} F100 S{power:.1f}")
  459. lines.append(f"G1 X{offX:.3f} F100")
  460. else:
  461. lines.append(f"M67 E{e_num} Q{power:.2f}")
  462. lines.append(f"G1 X{offX + 0.01:.3f} F100")
  463. lines.append(f"G1 X{offX:.3f} F100")
  464. lines.append(f"G1 X{offX:.3f}")
  465. lines.append("G4 P0.1")
  466. if pause_cmd:
  467. lines.append(f"{pause_cmd} (Press Cycle Start to continue)")
  468. lines.append("( --- POINTING END --- )")
  469. return "\n".join(lines) + "\n"
  470. def prepare_framing(self, config, dims, offsets):
  471. framing_gcode = ""
  472. real_w, real_h = dims
  473. offX, offY = offsets
  474. if config['is_pointing'] or config['is_framing']:
  475. try:
  476. pwr = float(config['f_pwr'])
  477. ratio = float(config['f_ratio']) / 100.0
  478. f_feed = int(config['base_feedrate'] * ratio)
  479. except:
  480. pwr, f_feed = 0.0, 600
  481. # Pointing laser test position
  482. if config['is_pointing']:
  483. framing_gcode += self.generate_pointing_gcode(
  484. offX, offY, pwr,
  485. pause_cmd=config['f_pause'],
  486. use_s_mode=config['use_s_mode'],
  487. e_num=config['e_num']
  488. ) + "\n"
  489. # Rectangle framing contour preview
  490. if config['is_framing']:
  491. framing_gcode += self.generate_framing_gcode(
  492. real_w, real_h,
  493. offX, offY,
  494. power=pwr,
  495. feedrate=f_feed,
  496. pause_cmd=config['f_pause'],
  497. use_s_mode=config['use_s_mode'],
  498. e_num=config['e_num']
  499. )
  500. return framing_gcode
  501. def calculate_offsets(self,
  502. selected_origin,
  503. real_w,
  504. real_h,
  505. custom_x=0,
  506. custom_y=0):
  507. offX, offY = 0, 0
  508. if selected_origin == "Upper-Left":
  509. offY = -real_h
  510. elif selected_origin == "Lower-Right":
  511. offX = -real_w
  512. elif selected_origin == "Upper-Right":
  513. offX, offY = -real_w, -real_h
  514. elif selected_origin == "Center":
  515. offX, offY = -real_w / 2, -real_h / 2
  516. elif selected_origin == "Custom":
  517. offX, offY = -custom_x, -custom_y
  518. return offX, offY
  519. # def compute_geometry(self, s, matrix_shape=None):
  520. # self.stats = {}
  521. # # 1. Constantes de base
  522. # scan_step = 25.4 / max(1, float(s.get("dpi", 254)))
  523. # l_step = float(s.get("line_step", 0.1))
  524. # # On force la casse pour la comparaison
  525. # raster_mode = str(s.get("raster_mode", "horizontal")).lower().strip()
  526. # target_w = float(s.get("width", 10.0))
  527. # # 2. Détermination des dimensions en pixels (Logique "Force Exact")
  528. # if matrix_shape:
  529. # img_h, img_w = matrix_shape
  530. # aspect_ratio = img_h / img_w
  531. # if raster_mode == "horizontal":
  532. # # Pour faire exactement target_w, il faut (target_w / step) + 1 pixels
  533. # w_px = max(2, int(round(target_w / scan_step)) + 1)
  534. # # On ajuste h_px selon le ratio de l'image
  535. # real_w_tmp = (w_px - 1) * scan_step
  536. # h_px = max(2, int(round((real_w_tmp * aspect_ratio) / l_step)) + 1)
  537. # else: # Vertical
  538. # # En vertical, target_w est la longueur de scan (axe Y)
  539. # h_px = max(2, int(round(target_w / scan_step)) + 1)
  540. # real_h_tmp = (h_px - 1) * scan_step
  541. # w_px = max(2, int(round((real_h_tmp / aspect_ratio) / l_step)) + 1)
  542. # else:
  543. # # Valeurs par défaut si pas d'image
  544. # w_px = max(2, int(target_w / scan_step))
  545. # h_px = max(2, int(10.0 / l_step))
  546. # # 3. Calcul des dimensions physiques finales
  547. # if raster_mode == "Horizontal":
  548. # real_w = (w_px - 1) * scan_step
  549. # real_h = (h_px - 1) * l_step
  550. # else:
  551. # real_w = (w_px - 1) * l_step
  552. # real_h = (h_px - 1) * scan_step
  553. # # 4. Calcul de l'Overscan
  554. # overscan_dist = float(s.get("premove", 2.0))
  555. # # 5. Définition des zones
  556. # rect_burn = (0, 0, real_w, real_h)
  557. # if raster_mode == "horizontal":
  558. # rect_full = (-overscan_dist, 0, real_w + overscan_dist, real_h)
  559. # else:
  560. # rect_full = (0, -overscan_dist, real_w, real_h + overscan_dist)
  561. # # 6. Estimation du temps (Correction de la clé speed -> feedrate)
  562. # num_passes = h_px if raster_mode == "horizontal" else w_px
  563. # dist_per_pass = (real_w if raster_mode == "horizontal" else real_h) + (2 * overscan_dist)
  564. # total_dist_mm = num_passes * dist_per_pass
  565. # speed_mm_min = float(s.get("feedrate", 3000))
  566. # # Calcul temps
  567. # est_min = total_dist_mm / max(1.0, speed_mm_min)
  568. # # Ajout temps de latence accélération (0.1s par ligne est plus réaliste)
  569. # est_min += (num_passes * 0.1) / 60
  570. # return {
  571. # "w_px": w_px,
  572. # "h_px": h_px,
  573. # "real_w": real_w,
  574. # "real_h": real_h,
  575. # "x_step": scan_step if raster_mode == "horizontal" else l_step,
  576. # "y_step": l_step if raster_mode == "horizontal" else scan_step,
  577. # "scan_step": scan_step,
  578. # "l_step": l_step,
  579. # "overscan_dist": overscan_dist,
  580. # "est_min": est_min,
  581. # "rect_burn": rect_burn,
  582. # "rect_full": rect_full,
  583. # "raster_mode": raster_mode
  584. # }
  585. def simulate_gcode_size(self, matrix, raster_mode, g_steps):
  586. """
  587. Version NumPy ultra-rapide pour estimer le nombre de lignes G-Code.
  588. """
  589. # On s'assure que la matrice est en entiers pour des comparaisons exactes
  590. m = matrix.astype(np.int16)
  591. if raster_mode == "horizontal":
  592. # Différence entre chaque pixel et son voisin de gauche
  593. # np.diff renvoie une matrice de taille (H, W-1)
  594. diffs = np.diff(m, axis=1)
  595. # Un changement survient si la différence est non nulle
  596. changes = np.count_nonzero(diffs)
  597. # On ajoute le premier pixel de chaque ligne s'il n'est pas blanc
  598. starts = np.count_nonzero(m[:, 0] > 0)
  599. else:
  600. # Idem pour le mode vertical (axis=0)
  601. diffs = np.diff(m, axis=0)
  602. changes = np.count_nonzero(diffs)
  603. starts = np.count_nonzero(m[0, :] > 0)
  604. # Calcul des lignes :
  605. # Chaque 'change' est une nouvelle commande G1 ou S
  606. # On ajoute les retours à la ligne (H ou W selon le mode)
  607. n_gcode_lines = changes + starts + (m.shape[0] if raster_mode == "horizontal" else m.shape[1])
  608. # Estimation du poids :
  609. # 16 octets est une moyenne réaliste pour "G1X123.45S255\n"
  610. total_chars = n_gcode_lines * 16
  611. return total_chars, n_gcode_lines
  612. def get_gcode_statistics(self, matrix, s, gc_params):
  613. try:
  614. h_px, w_px = matrix.shape
  615. raster_mode = str(gc_params.get("raster_mode", "horizontal")).lower().strip()
  616. g_steps = int(s.get("grayscale_steps", 256))
  617. # Quantification Vectorisée (Ultra rapide)
  618. if g_steps < 256:
  619. factor = 255 / (g_steps - 1)
  620. sim_matrix = np.round(matrix / factor) * factor
  621. else:
  622. sim_matrix = matrix
  623. # Simulation NumPy
  624. total_bytes, n_gcode_lines = self.simulate_gcode_size(sim_matrix, raster_mode, g_steps)
  625. # Header/Footer forfaitaire
  626. total_bytes += 1500
  627. if total_bytes < 1024 * 1024:
  628. est_size_str = f"{total_bytes / 1024:.1f} KB"
  629. else:
  630. est_size_str = f"{total_bytes / (1024 * 1024):.2f} MB"
  631. return est_size_str, int(n_gcode_lines)
  632. except Exception as e:
  633. print(f"Estimation error: {e}")
  634. return "0 KB", 0