gcode_engine.py 27 KB

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