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
@@ -12,18 +12,96 @@ except Exception:
12
12
  _HAS_PCHIP = False
13
13
 
14
14
 
15
-
16
15
  # ---------------------- preset schema ----------------------
16
+ # Legacy (v1 single-curve) preset:
17
17
  # {
18
+ # "name": "MyPreset",
18
19
  # "mode": "K (Brightness)" | "R" | "G" | "B" | "L*" | "a*" | "b*" | "Chroma" | "Saturation" | aliases ("rgb","k","lum"…),
19
- # "shape": "linear" | "s_mild" | "s_med" | "s_strong" | "lift_shadows" | "crush_shadows"
20
- # | "fade_blacks" | "rolloff_highlights" | "flatten" | "custom",
21
- # "amount": 0..1 (intensity, ignored for custom),
22
- # "points_norm": [[x,y], ...] # optional when shape="custom" (normalized 0..1 domain/range)
20
+ # "shape": "linear" | "s_mild" | ... | "custom",
21
+ # "amount": 0..1,
22
+ # "points_norm": [[x,y], ...] # normalized 0..1 domain/range
23
23
  # }
24
24
  #
25
- # Default if missing: mode="K (Brightness)", shape="linear", amount=0.5
25
+ # New (v2 multi-curve) preset:
26
+ # {
27
+ # "name": "MyPreset",
28
+ # "kind": "curves_multi",
29
+ # "version": 2,
30
+ # "active": "K" | "R" | "G" | "B" | "L*" | "a*" | "b*" | "Chroma" | "Saturation",
31
+ # "modes": {
32
+ # "K": [[x,y], ...],
33
+ # "R": [[x,y], ...],
34
+ # ...
35
+ # }
36
+ # }
37
+
38
+ _MODE_KEY_TO_LABEL = {
39
+ "K": "K (Brightness)",
40
+ "R": "R",
41
+ "G": "G",
42
+ "B": "B",
43
+ "L*": "L*",
44
+ "a*": "a*",
45
+ "b*": "b*",
46
+ "Chroma": "Chroma",
47
+ "Saturation": "Saturation",
48
+ }
26
49
 
50
+
51
+ _LABEL_TO_MODE_KEY = {v: k for k, v in _MODE_KEY_TO_LABEL.items()}
52
+
53
+ _APPLY_ORDER_KEYS = ["K", "R", "G", "B", "L*", "a*", "b*", "Chroma", "Saturation"]
54
+
55
+ def _is_linear_points_norm(pts) -> bool:
56
+ try:
57
+ return (
58
+ isinstance(pts, (list, tuple)) and len(pts) == 2
59
+ and tuple(pts[0]) == (0.0, 0.0)
60
+ and tuple(pts[1]) == (1.0, 1.0)
61
+ )
62
+ except Exception:
63
+ return False
64
+
65
+ def _coerce_points_norm(raw) -> List[Tuple[float, float]] | None:
66
+ if not isinstance(raw, (list, tuple)) or len(raw) < 2:
67
+ return None
68
+ out: List[Tuple[float, float]] = []
69
+ for e in raw:
70
+ if isinstance(e, (list, tuple)) and len(e) >= 2:
71
+ x, y = e[0], e[1]
72
+ elif isinstance(e, dict):
73
+ x, y = e.get("x"), e.get("y")
74
+ else:
75
+ continue
76
+ if x is None or y is None:
77
+ continue
78
+ out.append((float(x), float(y)))
79
+ return out if len(out) >= 2 else None
80
+
81
+ def _coerce_modes_dict(raw) -> Dict[str, List[Tuple[float, float]]]:
82
+ """
83
+ Return {mode_key: points_norm}, filtering junk but keeping backward-compat.
84
+ Accepts keys as internal mode keys ("K") or labels ("K (Brightness)").
85
+ """
86
+ modes: Dict[str, List[Tuple[float, float]]] = {}
87
+ if not isinstance(raw, dict):
88
+ return modes
89
+
90
+ for k, v in raw.items():
91
+ key = str(k)
92
+ # allow labels or keys
93
+ if key in _MODE_KEY_TO_LABEL:
94
+ mode_key = key
95
+ else:
96
+ mode_label = _norm_mode(key) # converts aliases -> proper label
97
+ mode_key = _LABEL_TO_MODE_KEY.get(mode_label, "K")
98
+
99
+ pts = _coerce_points_norm(v)
100
+ if pts is None:
101
+ continue
102
+ modes[mode_key] = pts
103
+
104
+ return modes
27
105
  # ---------------------- shape library (normalized) ----------------------
28
106
  def _shape_points_norm(shape: str, amount: float) -> List[Tuple[float, float]]:
29
107
  a = float(max(0.0, min(1.0, amount)))
@@ -141,32 +219,38 @@ def _unwrap_preset_dict(preset: Dict) -> Dict:
141
219
  """
142
220
  Accept a variety of containers and peel down to the actual curve data.
143
221
 
144
- Examples we handle:
145
- {"step_name":"Curves","mode":..., "preset":{...}}
146
- {"curves": {...}}
147
- {"state": {...}} # if state contains curve points
222
+ Handles:
223
+ - {"preset": {...}}
224
+ - {"curves": {...}}
225
+ - {"state": {...}}
226
+ - multi presets with {"modes": {...}}
148
227
  """
149
228
  p = dict(preset or {})
150
229
 
151
- # Case 1: full metadata from doc.apply_edit: {"step_name":"Curves", "mode":..., "preset": {...}}
230
+ def _looks_like_curve_dict(d: dict) -> bool:
231
+ return (
232
+ isinstance(d, dict)
233
+ and (
234
+ "points_norm" in d
235
+ or "handles" in d
236
+ or "points_scene" in d
237
+ or "scene_points" in d
238
+ or "modes" in d # <-- multi
239
+ )
240
+ )
241
+
152
242
  inner = p.get("preset")
153
- if isinstance(inner, dict) and ("points_norm" in inner or "handles" in inner
154
- or "points_scene" in inner or "scene_points" in inner):
243
+ if _looks_like_curve_dict(inner):
155
244
  return inner
156
245
 
157
- # Case 2: payloads like {"curves": {...}}
158
246
  inner = p.get("curves")
159
- if isinstance(inner, dict) and ("points_norm" in inner or "handles" in inner
160
- or "points_scene" in inner or "scene_points" in inner):
247
+ if _looks_like_curve_dict(inner):
161
248
  return inner
162
249
 
163
- # Case 3: {"state": {...}} (if you stored the curve state under that key)
164
250
  inner = p.get("state")
165
- if isinstance(inner, dict) and ("points_norm" in inner or "handles" in inner
166
- or "points_scene" in inner or "scene_points" in inner):
251
+ if _looks_like_curve_dict(inner):
167
252
  return inner
168
253
 
169
- # Otherwise assume p already *is* the preset
170
254
  return p
171
255
 
172
256
 
@@ -179,7 +263,9 @@ def _settings() -> QSettings | None:
179
263
  return None
180
264
 
181
265
  def list_custom_presets() -> list[dict]:
182
- """Return a list of dicts: {"name", "mode", "shape":"custom", "points_norm":[[x,y],...]}"""
266
+ """
267
+ Returns a list of preset dicts (v1 single-curve or v2 multi-curve).
268
+ """
183
269
  s = _settings()
184
270
  if not s:
185
271
  return []
@@ -187,29 +273,82 @@ def list_custom_presets() -> list[dict]:
187
273
  try:
188
274
  lst = json.loads(raw)
189
275
  if isinstance(lst, list):
190
- return [p for p in lst if isinstance(p, dict)]
276
+ out = [p for p in lst if isinstance(p, dict)]
277
+ # normalize missing name fields (defensive)
278
+ for p in out:
279
+ if "name" not in p:
280
+ p["name"] = "(unnamed)"
281
+ return out
191
282
  except Exception:
192
283
  pass
193
284
  return []
194
285
 
195
- def save_custom_preset(name: str, mode: str, points_norm: list[tuple[float,float]]) -> bool:
196
- """Create/overwrite by name."""
286
+ def save_custom_preset(name: str, mode_or_preset, points_norm: list[tuple[float, float]] | None = None) -> bool:
287
+ """
288
+ Save a custom preset. Supports:
289
+ - legacy: save_custom_preset(name, mode, points_norm)
290
+ - new: save_custom_preset(name, preset_dict)
291
+ """
197
292
  s = _settings()
198
293
  if not s:
199
294
  return False
295
+
200
296
  name = (name or "").strip()
201
297
  if not name:
202
298
  return False
203
- preset = {
204
- "name": name,
205
- "mode": _norm_mode(mode),
206
- "shape": "custom",
207
- "amount": 1.0,
208
- "points_norm": [(float(x), float(y)) for (x, y) in points_norm],
209
- }
299
+
300
+ # --- build preset dict ---
301
+ preset: dict
302
+
303
+ if isinstance(mode_or_preset, dict):
304
+ # v2 (or v1 dict passed directly)
305
+ preset = dict(mode_or_preset)
306
+ preset["name"] = name # enforce
307
+
308
+ # If it's a multi preset, sanitize modes
309
+ if preset.get("kind") == "curves_multi" or isinstance(preset.get("modes"), dict):
310
+ modes = _coerce_modes_dict(preset.get("modes", {}))
311
+ # fill missing keys with linear so UI always has a full set
312
+ full_modes = {}
313
+ for k in _MODE_KEY_TO_LABEL.keys():
314
+ full_modes[k] = modes.get(k, [(0.0, 0.0), (1.0, 1.0)])
315
+ preset = {
316
+ "name": name,
317
+ "kind": "curves_multi",
318
+ "version": 2,
319
+ "active": str(preset.get("active") or "K"),
320
+ "modes": {k: [(float(x), float(y)) for (x, y) in v] for k, v in full_modes.items()},
321
+ }
322
+
323
+ else:
324
+ # treat as v1 single-curve dict
325
+ mode_label = _norm_mode(preset.get("mode"))
326
+ pts = _coerce_points_norm(preset.get("points_norm")) or [(0.0, 0.0), (1.0, 1.0)]
327
+ preset = {
328
+ "name": name,
329
+ "mode": mode_label,
330
+ "shape": str(preset.get("shape", "custom")),
331
+ "amount": float(preset.get("amount", 1.0)),
332
+ "points_norm": [(float(x), float(y)) for (x, y) in pts],
333
+ }
334
+
335
+ else:
336
+ # legacy signature
337
+ mode = _norm_mode(str(mode_or_preset))
338
+ pts = points_norm or [(0.0, 0.0), (1.0, 1.0)]
339
+ preset = {
340
+ "name": name,
341
+ "mode": mode,
342
+ "shape": "custom",
343
+ "amount": 1.0,
344
+ "points_norm": [(float(x), float(y)) for (x, y) in pts],
345
+ }
346
+
347
+ # --- upsert by name (case-insensitive) ---
210
348
  lst = list_custom_presets()
211
- lst = [p for p in lst if (p.get("name","").lower() != name.lower())]
349
+ lst = [p for p in lst if (p.get("name", "").lower() != name.lower())]
212
350
  lst.append(preset)
351
+
213
352
  s.setValue(_SETTINGS_KEY, json.dumps(lst))
214
353
  s.sync()
215
354
  return True
@@ -218,8 +357,11 @@ def delete_custom_preset(name: str) -> bool:
218
357
  s = _settings()
219
358
  if not s:
220
359
  return False
360
+ nm = (name or "").strip().lower()
361
+ if not nm:
362
+ return False
221
363
  lst = list_custom_presets()
222
- lst = [p for p in lst if (p.get("name","").lower() != (name or "").strip().lower())]
364
+ lst = [p for p in lst if (p.get("name", "").strip().lower() != nm)]
223
365
  s.setValue(_SETTINGS_KEY, json.dumps(lst))
224
366
  s.sync()
225
367
  return True
@@ -228,7 +370,7 @@ def delete_custom_preset(name: str) -> bool:
228
370
  # ---------------------- headless apply ----------------------
229
371
  def apply_curves_via_preset(main_window, doc, preset: Dict):
230
372
  import numpy as _np
231
- from setiastro.saspro.curves_preset import _lut_from_preset, _unwrap_preset_dict # self
373
+ from setiastro.saspro.curves_preset import _unwrap_preset_dict # self
232
374
  # lazy import to avoid cycle
233
375
  from setiastro.saspro.curve_editor_pro import _apply_mode_any
234
376
 
@@ -236,8 +378,7 @@ def apply_curves_via_preset(main_window, doc, preset: Dict):
236
378
  if img is None:
237
379
  return
238
380
 
239
- # Accept full last-action dicts and unwrap down to the actual curve definition
240
- core_preset = _unwrap_preset_dict(preset or {})
381
+ core = _unwrap_preset_dict(preset or {})
241
382
 
242
383
  arr = _np.asarray(img)
243
384
  if arr.dtype.kind in "ui":
@@ -248,18 +389,52 @@ def apply_curves_via_preset(main_window, doc, preset: Dict):
248
389
  else:
249
390
  arr01 = arr.astype(_np.float32)
250
391
 
251
- lut01, mode = _lut_from_preset(core_preset)
392
+ # -------- MULTI --------
393
+ if core.get("kind") == "curves_multi" or isinstance(core.get("modes"), dict):
394
+ modes = _coerce_modes_dict(core.get("modes", {}))
395
+
396
+ out01 = arr01
397
+ used_any = False
398
+
399
+ for mode_key in _APPLY_ORDER_KEYS:
400
+ pts = modes.get(mode_key)
401
+ if not pts:
402
+ continue
403
+ if _is_linear_points_norm(pts):
404
+ continue
405
+
406
+ pts_scene = _points_norm_to_scene(pts)
407
+ fn = _interpolator_from_scene_points(pts_scene)
408
+ lut01 = build_curve_lut(fn, size=65536)
409
+
410
+ mode_label = _MODE_KEY_TO_LABEL.get(mode_key, "K (Brightness)")
411
+ out01 = _apply_mode_any(out01, mode_label, lut01)
412
+ used_any = True
413
+
414
+ if not used_any:
415
+ return
416
+
417
+ meta = {
418
+ "step_name": "Curves",
419
+ "mode": _MODE_KEY_TO_LABEL.get(str(core.get("active") or "K"), "K (Brightness)"),
420
+ "preset": dict(core),
421
+ }
422
+ doc.apply_edit(out01, metadata=meta, step_name="Curves")
423
+ return
424
+
425
+ # -------- LEGACY SINGLE --------
426
+ lut01, mode = _lut_from_preset(core)
252
427
  out01 = _apply_mode_any(arr01, mode, lut01)
253
428
 
254
429
  meta = {
255
430
  "step_name": "Curves",
256
431
  "mode": mode,
257
- "preset": dict(core_preset), # store the normalized core preset
432
+ "preset": dict(core),
258
433
  }
259
434
  doc.apply_edit(out01, metadata=meta, step_name="Curves")
260
435
 
261
436
 
262
- # ---------------------- open UI with preset ----------------------
437
+
263
438
  # ---------------------- open UI with preset ----------------------
264
439
  def open_curves_with_preset(main_window, preset: Dict | None = None):
265
440
  # lazy import UI to avoid cycle
@@ -274,20 +449,47 @@ def open_curves_with_preset(main_window, preset: Dict | None = None):
274
449
 
275
450
  dlg = CurvesDialogPro(main_window, doc)
276
451
 
277
- # Peel down any wrapper (metadata / last-action container) to the actual curve definition
278
- core_preset = _unwrap_preset_dict(preset or {})
452
+ core = _unwrap_preset_dict(preset or {})
453
+
454
+ # -------- MULTI --------
455
+ if core.get("kind") == "curves_multi" or isinstance(core.get("modes"), dict):
456
+ modes = _coerce_modes_dict(core.get("modes", {}))
457
+
458
+ # Fill dialog store for every key
459
+ for k in getattr(dlg, "_curves_store", {}).keys():
460
+ dlg._curves_store[k] = modes.get(k, [(0.0, 0.0), (1.0, 1.0)])
461
+
462
+ # Choose active key
463
+ active_key = str(core.get("active") or "K")
464
+ if active_key not in dlg._curves_store:
465
+ active_key = "K"
466
+ dlg._current_mode_key = active_key
467
+
468
+ # Set the radio to match active
469
+ want_label = _MODE_KEY_TO_LABEL.get(active_key, "K (Brightness)")
470
+ for b in dlg.mode_group.buttons():
471
+ if b.text().lower() == want_label.lower():
472
+ b.setChecked(True)
473
+ break
474
+
475
+ # Push active curve into editor
476
+ dlg._editor_set_from_norm(dlg._curves_store.get(active_key, [(0.0, 0.0), (1.0, 1.0)]))
477
+ dlg._refresh_overlays()
478
+ dlg._quick_preview()
479
+
480
+ dlg.show()
481
+ dlg.raise_()
482
+ dlg.activateWindow()
483
+ return
279
484
 
280
- # set mode radio from the *core* preset
281
- want = _norm_mode(core_preset.get("mode"))
485
+ # -------- LEGACY SINGLE --------
486
+ want = _norm_mode(core.get("mode"))
282
487
  for b in dlg.mode_group.buttons():
283
488
  if b.text().lower() == want.lower():
284
489
  b.setChecked(True)
285
490
  break
286
491
 
287
- # Seed control handles from the same logic used by LUT building
288
- pts_scene = _scene_points_from_preset(core_preset)
289
-
290
- # remove exact endpoints if present; editor expects control handles only
492
+ pts_scene = _scene_points_from_preset(core)
291
493
  filt = [(x, y) for (x, y) in pts_scene if x > 0.0 + 1e-6 and x < 360.0 - 1e-6]
292
494
  dlg.editor.setControlHandles(filt)
293
495