setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (132) hide show
  1. setiastro/images/TextureClarity.svg +56 -0
  2. setiastro/images/abeicon.svg +16 -0
  3. setiastro/images/acv_icon.png +0 -0
  4. setiastro/images/colorwheel.svg +97 -0
  5. setiastro/images/cosmic.svg +40 -0
  6. setiastro/images/cosmicsat.svg +24 -0
  7. setiastro/images/first_quarter.png +0 -0
  8. setiastro/images/full_moon.png +0 -0
  9. setiastro/images/graxpert.svg +19 -0
  10. setiastro/images/last_quarter.png +0 -0
  11. setiastro/images/linearfit.svg +32 -0
  12. setiastro/images/narrowbandnormalization.png +0 -0
  13. setiastro/images/new_moon.png +0 -0
  14. setiastro/images/pixelmath.svg +42 -0
  15. setiastro/images/planetarystacker.png +0 -0
  16. setiastro/images/waning_crescent_1.png +0 -0
  17. setiastro/images/waning_crescent_2.png +0 -0
  18. setiastro/images/waning_crescent_3.png +0 -0
  19. setiastro/images/waning_crescent_4.png +0 -0
  20. setiastro/images/waning_crescent_5.png +0 -0
  21. setiastro/images/waning_gibbous_1.png +0 -0
  22. setiastro/images/waning_gibbous_2.png +0 -0
  23. setiastro/images/waning_gibbous_3.png +0 -0
  24. setiastro/images/waning_gibbous_4.png +0 -0
  25. setiastro/images/waning_gibbous_5.png +0 -0
  26. setiastro/images/waxing_crescent_1.png +0 -0
  27. setiastro/images/waxing_crescent_2.png +0 -0
  28. setiastro/images/waxing_crescent_3.png +0 -0
  29. setiastro/images/waxing_crescent_4.png +0 -0
  30. setiastro/images/waxing_crescent_5.png +0 -0
  31. setiastro/images/waxing_gibbous_1.png +0 -0
  32. setiastro/images/waxing_gibbous_2.png +0 -0
  33. setiastro/images/waxing_gibbous_3.png +0 -0
  34. setiastro/images/waxing_gibbous_4.png +0 -0
  35. setiastro/images/waxing_gibbous_5.png +0 -0
  36. setiastro/qml/ResourceMonitor.qml +84 -82
  37. setiastro/saspro/__main__.py +20 -1
  38. setiastro/saspro/_generated/build_info.py +2 -2
  39. setiastro/saspro/abe.py +37 -4
  40. setiastro/saspro/aberration_ai.py +364 -33
  41. setiastro/saspro/aberration_ai_preset.py +29 -3
  42. setiastro/saspro/acv_exporter.py +379 -0
  43. setiastro/saspro/add_stars.py +33 -6
  44. setiastro/saspro/astrospike_python.py +45 -3
  45. setiastro/saspro/backgroundneutral.py +108 -40
  46. setiastro/saspro/blemish_blaster.py +4 -1
  47. setiastro/saspro/blink_comparator_pro.py +150 -55
  48. setiastro/saspro/clahe.py +4 -1
  49. setiastro/saspro/continuum_subtract.py +4 -1
  50. setiastro/saspro/convo.py +13 -7
  51. setiastro/saspro/cosmicclarity.py +129 -18
  52. setiastro/saspro/crop_dialog_pro.py +123 -7
  53. setiastro/saspro/curve_editor_pro.py +181 -64
  54. setiastro/saspro/curves_preset.py +249 -47
  55. setiastro/saspro/doc_manager.py +245 -15
  56. setiastro/saspro/exoplanet_detector.py +120 -28
  57. setiastro/saspro/frequency_separation.py +1158 -204
  58. setiastro/saspro/ghs_dialog_pro.py +81 -16
  59. setiastro/saspro/graxpert.py +1 -0
  60. setiastro/saspro/gui/main_window.py +706 -264
  61. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  62. setiastro/saspro/gui/mixins/file_mixin.py +35 -16
  63. setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
  64. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  65. setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
  66. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  67. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  68. setiastro/saspro/halobgon.py +4 -0
  69. setiastro/saspro/histogram.py +184 -8
  70. setiastro/saspro/image_combine.py +4 -0
  71. setiastro/saspro/image_peeker_pro.py +4 -0
  72. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  73. setiastro/saspro/imageops/serloader.py +1345 -0
  74. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  75. setiastro/saspro/imageops/stretch.py +582 -62
  76. setiastro/saspro/isophote.py +4 -0
  77. setiastro/saspro/layers.py +13 -9
  78. setiastro/saspro/layers_dock.py +183 -3
  79. setiastro/saspro/legacy/image_manager.py +154 -20
  80. setiastro/saspro/legacy/numba_utils.py +68 -48
  81. setiastro/saspro/legacy/xisf.py +240 -98
  82. setiastro/saspro/live_stacking.py +203 -82
  83. setiastro/saspro/luminancerecombine.py +228 -27
  84. setiastro/saspro/mask_creation.py +174 -15
  85. setiastro/saspro/mfdeconv.py +113 -35
  86. setiastro/saspro/mfdeconvcudnn.py +119 -70
  87. setiastro/saspro/mfdeconvsport.py +112 -35
  88. setiastro/saspro/morphology.py +4 -0
  89. setiastro/saspro/multiscale_decomp.py +81 -29
  90. setiastro/saspro/narrowband_normalization.py +1618 -0
  91. setiastro/saspro/numba_utils.py +72 -57
  92. setiastro/saspro/ops/commands.py +18 -18
  93. setiastro/saspro/ops/script_editor.py +10 -2
  94. setiastro/saspro/ops/scripts.py +122 -0
  95. setiastro/saspro/perfect_palette_picker.py +37 -3
  96. setiastro/saspro/plate_solver.py +84 -49
  97. setiastro/saspro/psf_viewer.py +119 -37
  98. setiastro/saspro/remove_green.py +1 -1
  99. setiastro/saspro/resources.py +73 -0
  100. setiastro/saspro/rgbalign.py +460 -12
  101. setiastro/saspro/selective_color.py +4 -1
  102. setiastro/saspro/ser_stack_config.py +82 -0
  103. setiastro/saspro/ser_stacker.py +2321 -0
  104. setiastro/saspro/ser_stacker_dialog.py +1838 -0
  105. setiastro/saspro/ser_tracking.py +206 -0
  106. setiastro/saspro/serviewer.py +1625 -0
  107. setiastro/saspro/sfcc.py +662 -216
  108. setiastro/saspro/shortcuts.py +171 -33
  109. setiastro/saspro/signature_insert.py +692 -33
  110. setiastro/saspro/stacking_suite.py +1347 -485
  111. setiastro/saspro/star_alignment.py +247 -123
  112. setiastro/saspro/star_spikes.py +4 -0
  113. setiastro/saspro/star_stretch.py +38 -3
  114. setiastro/saspro/stat_stretch.py +892 -129
  115. setiastro/saspro/subwindow.py +787 -363
  116. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  117. setiastro/saspro/texture_clarity.py +593 -0
  118. setiastro/saspro/wavescale_hdr.py +4 -1
  119. setiastro/saspro/wavescalede.py +4 -1
  120. setiastro/saspro/whitebalance.py +84 -12
  121. setiastro/saspro/widgets/common_utilities.py +28 -21
  122. setiastro/saspro/widgets/resource_monitor.py +209 -111
  123. setiastro/saspro/widgets/spinboxes.py +10 -13
  124. setiastro/saspro/wimi.py +27 -656
  125. setiastro/saspro/wims.py +13 -3
  126. setiastro/saspro/xisf.py +101 -11
  127. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
  128. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
  129. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
  130. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
  131. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
  132. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
@@ -521,32 +521,6 @@ class CurveEditor(QGraphicsView):
521
521
  if ln is not None:
522
522
  ln.setVisible(False)
523
523
 
524
- def _scene_to_norm_points(self, pts_scene: list[tuple[float,float]]) -> list[tuple[float,float]]:
525
- """(x:[0..360], y:[0..360] down) → (x,y in [0..1] up). Ensures endpoints present & strictly increasing x."""
526
- out = []
527
- lastx = -1e9
528
- for (x, y) in sorted(pts_scene, key=lambda t: t[0]):
529
- x = float(np.clip(x, 0.0, 360.0))
530
- y = float(np.clip(y, 0.0, 360.0))
531
- # strictly increasing X
532
- if x <= lastx:
533
- x = lastx + 1e-3
534
- lastx = x
535
- out.append((x / 360.0, 1.0 - (y / 360.0)))
536
- # ensure endpoints
537
- if not any(abs(px - 0.0) < 1e-6 for px, _ in out): out.insert(0, (0.0, 0.0))
538
- if not any(abs(px - 1.0) < 1e-6 for px, _ in out): out.append((1.0, 1.0))
539
- # clamp
540
- return [(float(np.clip(x,0,1)), float(np.clip(y,0,1))) for (x,y) in out]
541
-
542
- def _collect_points_norm_from_editor(self) -> list[tuple[float,float]]:
543
- """Take endpoints+handles from editor => normalized points."""
544
- pts_scene = []
545
- for p in (self.editor.end_points + self.editor.control_points):
546
- pos = p.scenePos()
547
- pts_scene.append((float(pos.x()), float(pos.y())))
548
- return self._scene_to_norm_points(pts_scene)
549
-
550
524
 
551
525
  def redistributeHandlesByPivot(self, u: float):
552
526
  """
@@ -1022,10 +996,18 @@ class CurvesDialogPro(QDialog):
1022
996
  self._main = parent
1023
997
  self.doc = document
1024
998
 
1025
- # Connect to active document change signal
999
+ self._follow_conn = False
1026
1000
  if hasattr(self._main, "currentDocumentChanged"):
1027
- self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
1028
-
1001
+ try:
1002
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
1003
+ self._follow_conn = True
1004
+ except Exception:
1005
+ self._follow_conn = False
1006
+ try:
1007
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
1008
+ except Exception:
1009
+ pass # older PyQt6 versions
1010
+ self.finished.connect(self._cleanup_connections)
1029
1011
  self._preview_img = None # downsampled float01
1030
1012
  self._full_img = None # full-res float01
1031
1013
  self._pix = None
@@ -1040,7 +1022,14 @@ class CurvesDialogPro(QDialog):
1040
1022
  self._cdf = None
1041
1023
  self._cdf_bins = 1024
1042
1024
  self._cdf_total = 0
1043
-
1025
+ # Debounce: coalesce rapid curve edits into one rebuild
1026
+ self._curve_debounce_ms = 120 # tweak: 80–200ms feels good
1027
+ self._curve_debounce = QTimer(self)
1028
+ self._curve_debounce.setSingleShot(True)
1029
+ self._curve_debounce.timeout.connect(self._rebuild_preview_from_curve_debounced)
1030
+
1031
+ # Optional: generation counter so stale results can't “win”
1032
+ self._curve_gen = 0
1044
1033
  self._clip_scale = 1.0 # preview→full multiplier
1045
1034
  self._cdf_total_full = 0 # total pixels in full image (H*W)
1046
1035
  self._cdf_total_preview = 0 # total pixels in preview (H*W)
@@ -1212,18 +1201,34 @@ class CurvesDialogPro(QDialog):
1212
1201
 
1213
1202
  def _on_editor_curve_changed(self, _lut8=None):
1214
1203
  """
1215
- Called on every editor redraw/drag. Persist the currently edited curve
1216
- into the store, refresh overlays, and do a realtime preview.
1204
+ Called on every editor redraw/drag. Persist points and refresh overlays.
1205
+ Preview rebuild is DEBOUNCED to avoid spamming.
1217
1206
  """
1218
1207
  try:
1219
1208
  self._curves_store[self._current_mode_key] = self._editor_points_norm()
1220
1209
  except Exception:
1221
1210
  pass
1222
- # show the true shapes of other channels too
1211
+
1212
+ # cheap: overlay redraw is fine every move (or you can debounce this too)
1223
1213
  self._refresh_overlays()
1224
- # now build from *all* current curves (including the just-edited one)
1225
- self._quick_preview()
1226
1214
 
1215
+ # expensive: debounce the preview rebuild
1216
+ self._curve_gen += 1
1217
+ self._curve_debounce.start(self._curve_debounce_ms)
1218
+
1219
+ def _rebuild_preview_from_curve_debounced(self):
1220
+ """
1221
+ Runs after the user pauses dragging for _curve_debounce_ms.
1222
+ Only rebuild if we have images loaded.
1223
+ """
1224
+ if self._preview_orig is None and self._preview_img is None:
1225
+ return
1226
+ # If your preview toggle is off, you may want to skip:
1227
+ if not getattr(self, "btn_preview", None) or not self.btn_preview.isChecked():
1228
+ return
1229
+
1230
+ # Do the real work (what you were doing before)
1231
+ self._quick_preview()
1227
1232
 
1228
1233
  def _active_mode_key(self) -> str:
1229
1234
  for b in self.mode_group.buttons():
@@ -1670,29 +1675,53 @@ class CurvesDialogPro(QDialog):
1670
1675
 
1671
1676
  # 1) Put this helper inside CurvesDialogPro (near other helpers)
1672
1677
  def _map_label_xy_to_image_ij(self, x: float, y: float):
1673
- """Map label-local coords (x,y) to _preview_img pixel (i,j). Returns (ix, iy) or None."""
1678
+ """
1679
+ Map label-local coords (x,y) to _preview_img pixel (ix, iy).
1680
+ Correct even when the pixmap is centered inside a larger label.
1681
+ Returns None if cursor is outside the displayed pixmap area.
1682
+ """
1674
1683
  if self._pix is None:
1675
1684
  return None
1685
+
1676
1686
  pm_disp = self.label.pixmap()
1677
1687
  if pm_disp is None or pm_disp.isNull():
1678
1688
  return None
1679
1689
 
1680
- src_w = self._pix.width() # size of the *source* pixmap (preview image)
1681
- src_h = self._pix.height()
1682
- disp_w = pm_disp.width() # size of the *displayed* pixmap on the label
1690
+ # Displayed pixmap size (after zoom)
1691
+ disp_w = pm_disp.width()
1683
1692
  disp_h = pm_disp.height()
1684
- if src_w <= 0 or src_h <= 0 or disp_w <= 0 or disp_h <= 0:
1693
+
1694
+ # Label may be bigger -> pixmap is centered with margins
1695
+ lbl_w = self.label.width()
1696
+ lbl_h = self.label.height()
1697
+
1698
+ off_x = max(0, (lbl_w - disp_w) // 2)
1699
+ off_y = max(0, (lbl_h - disp_h) // 2)
1700
+
1701
+ # Remove margins: label-local -> pixmap-local
1702
+ px = float(x) - float(off_x)
1703
+ py = float(y) - float(off_y)
1704
+
1705
+ if px < 0 or py < 0 or px >= disp_w or py >= disp_h:
1706
+ return None # outside actual image area
1707
+
1708
+ # Now convert displayed pixmap pixel -> source preview pixel
1709
+ src_w = self._pix.width()
1710
+ src_h = self._pix.height()
1711
+ if src_w <= 0 or src_h <= 0:
1685
1712
  return None
1686
1713
 
1687
1714
  sx = disp_w / float(src_w)
1688
1715
  sy = disp_h / float(src_h)
1689
1716
 
1690
- ix = int(x / sx)
1691
- iy = int(y / sy)
1717
+ ix = int(px / sx)
1718
+ iy = int(py / sy)
1719
+
1692
1720
  if ix < 0 or iy < 0 or ix >= src_w or iy >= src_h:
1693
1721
  return None
1694
1722
  return ix, iy
1695
1723
 
1724
+
1696
1725
  def _scene_to_norm_points(self, pts_scene: list[tuple[float,float]]) -> list[tuple[float,float]]:
1697
1726
  """(x:[0..360], y:[0..360] down) → (x,y in [0..1] up). Ensures endpoints present & strictly increasing x."""
1698
1727
  out = []
@@ -1740,14 +1769,38 @@ class CurvesDialogPro(QDialog):
1740
1769
  name, ok = QInputDialog.getText(self, self.tr("Save Curves Preset"), self.tr("Preset name:"))
1741
1770
  if not ok or not name.strip():
1742
1771
  return
1743
- pts_norm = self._collect_points_norm_from_editor()
1744
- mode = self._current_mode()
1745
- if save_custom_preset(name.strip(), mode, pts_norm):
1746
- self._set_status(self.tr("Saved preset “{0}”.").format(name.strip()))
1772
+ name = name.strip()
1773
+
1774
+ # 0) flush active editor -> store (CRITICAL)
1775
+ try:
1776
+ self._curves_store[self._current_mode_key] = self._editor_points_norm()
1777
+ except Exception:
1778
+ pass
1779
+
1780
+ # 1) build a full multi-curve payload
1781
+ modes = {}
1782
+ for k, pts in self._curves_store.items():
1783
+ if not isinstance(pts, (list, tuple)) or len(pts) < 2:
1784
+ continue
1785
+ # ensure floats (QSettings/JSON safety)
1786
+ modes[k] = [(float(x), float(y)) for (x, y) in pts]
1787
+
1788
+ preset = {
1789
+ "name": name,
1790
+ "version": 2,
1791
+ "kind": "curves_multi",
1792
+ "active": self._current_mode_key, # "K", "R", ...
1793
+ "modes": modes,
1794
+ }
1795
+
1796
+ # 2) save via your existing persistence
1797
+ if save_custom_preset(name, preset): # <-- change signature (see section 3)
1798
+ self._set_status(self.tr("Saved preset “{0}”.").format(name))
1747
1799
  self._rebuild_presets_menu()
1748
1800
  else:
1749
1801
  QMessageBox.warning(self, self.tr("Save failed"), self.tr("Could not save preset."))
1750
1802
 
1803
+
1751
1804
  def _rebuild_presets_menu(self):
1752
1805
  m = QMenu(self)
1753
1806
  # Built-in shapes under K (Brightness)
@@ -2178,6 +2231,44 @@ class CurvesDialogPro(QDialog):
2178
2231
 
2179
2232
  return (m * out + (1.0 - m) * src).astype(np.float32, copy=False)
2180
2233
 
2234
+ def closeEvent(self, ev):
2235
+ self._cleanup_connections()
2236
+ super().closeEvent(ev)
2237
+
2238
+ def _cleanup_connections(self):
2239
+ # disconnect the "follow active doc" hook
2240
+ try:
2241
+ if self._follow_conn and hasattr(self._main, "currentDocumentChanged"):
2242
+ self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
2243
+ except Exception:
2244
+ pass
2245
+ self._follow_conn = False
2246
+
2247
+ # stop/kill any running worker thread(s)
2248
+ try:
2249
+ thr = getattr(self, "_thr", None)
2250
+ if thr is not None:
2251
+ try:
2252
+ thr.requestInterruption()
2253
+ except Exception:
2254
+ pass
2255
+ try:
2256
+ thr.quit()
2257
+ except Exception:
2258
+ pass
2259
+ try:
2260
+ thr.wait(250)
2261
+ except Exception:
2262
+ pass
2263
+ except Exception:
2264
+ pass
2265
+
2266
+ # optional: drop refs that can keep things alive
2267
+ try:
2268
+ self._thr = None
2269
+ except Exception:
2270
+ pass
2271
+
2181
2272
 
2182
2273
  # zoom/pan
2183
2274
  def _apply_zoom(self):
@@ -2333,43 +2424,69 @@ class CurvesDialogPro(QDialog):
2333
2424
  def _apply_preset_dict(self, preset: dict):
2334
2425
  preset = preset or {}
2335
2426
 
2336
- # 1) set mode radio
2427
+ # -------- MULTI-CURVE (v2) --------
2428
+ if preset.get("kind") == "curves_multi" or ("modes" in preset and isinstance(preset.get("modes"), dict)):
2429
+ modes = preset.get("modes", {}) or {}
2430
+
2431
+ # 0) load all curves into store (fill missing keys with linear)
2432
+ for k in self._curves_store.keys():
2433
+ pts = modes.get(k)
2434
+ if isinstance(pts, (list, tuple)) and len(pts) >= 2:
2435
+ self._curves_store[k] = [(float(x), float(y)) for (x, y) in pts]
2436
+ else:
2437
+ self._curves_store[k] = [(0.0, 0.0), (1.0, 1.0)]
2438
+
2439
+ # 1) choose active key (default to K)
2440
+ active = str(preset.get("active") or "K")
2441
+ if active not in self._curves_store:
2442
+ active = "K"
2443
+ self._current_mode_key = active
2444
+
2445
+ # 2) set radio button that corresponds to active key
2446
+ # map internal key -> radio label
2447
+ key_to_label = {v: k for (k, v) in self._mode_key_map.items()} # "K"->"K (Brightness)"
2448
+ want_label = key_to_label.get(active, "K (Brightness)")
2449
+ for b in self.mode_group.buttons():
2450
+ if b.text() == want_label:
2451
+ b.setChecked(True)
2452
+ break
2453
+
2454
+ # 3) push active curve into editor
2455
+ self._editor_set_from_norm(self._curves_store[active])
2456
+
2457
+ # 4) refresh overlays + preview
2458
+ self._refresh_overlays()
2459
+ self._quick_preview()
2460
+
2461
+ self._set_status(self.tr("Preset: {0} [multi]").format(preset.get("name", self.tr("(built-in)"))))
2462
+ return
2463
+
2464
+ # -------- LEGACY SINGLE-CURVE --------
2465
+ # your existing single-curve behavior (slightly adjusted: store it too)
2337
2466
  want = _norm_mode(preset.get("mode"))
2338
2467
  for b in self.mode_group.buttons():
2339
2468
  if b.text().lower() == want.lower():
2340
2469
  b.setChecked(True)
2341
2470
  break
2342
2471
 
2343
- # 2) get points_norm — if absent, build from shape/amount (built-ins)
2344
2472
  ptsN = preset.get("points_norm")
2345
- shape = preset.get("shape") # may be None for custom presets
2473
+ shape = preset.get("shape")
2346
2474
  amount = float(preset.get("amount", 1.0))
2347
2475
 
2348
2476
  if not (isinstance(ptsN, (list, tuple)) and len(ptsN) >= 2):
2349
2477
  try:
2350
- # build from a named shape (built-ins); default to linear
2351
2478
  ptsN = _shape_points_norm(str(shape or "linear"), amount)
2352
2479
  except Exception:
2353
- ptsN = [(0.0, 0.0), (1.0, 1.0)] # safe fallback
2480
+ ptsN = [(0.0, 0.0), (1.0, 1.0)]
2354
2481
 
2355
- # 3) apply handles to the editor (strip exact endpoints)
2356
- pts_scene = _points_norm_to_scene(ptsN)
2357
- filt = [(x, y) for (x, y) in pts_scene if 1e-6 < x < 360.0 - 1e-6]
2358
-
2359
- if hasattr(self.editor, "clearSymmetryLine"):
2360
- self.editor.clearSymmetryLine()
2361
-
2362
- self.editor.setControlHandles(filt)
2363
- self.editor.updateCurve() # ensure redraw
2364
-
2365
- # persist into store & refresh
2482
+ self._editor_set_from_norm(ptsN)
2366
2483
  self._curves_store[self._current_mode_key] = self._editor_points_norm()
2367
2484
  self._refresh_overlays()
2368
2485
  self._quick_preview()
2369
2486
 
2370
- # 4) status: don’t assume shape exists
2371
2487
  shape_tag = f"[{shape}]" if shape else "[custom]"
2372
- self._set_status(self.tr("Preset: {0} {1}").format(preset.get('name', self.tr('(built-in)')), shape_tag))
2488
+ self._set_status(self.tr("Preset: {0} {1}").format(preset.get("name", self.tr("(built-in)")), shape_tag))
2489
+
2373
2490
 
2374
2491
 
2375
2492
  def apply_curves_ops(doc, op: dict):