setiastrosuitepro 1.7.1.post2__py3-none-any.whl → 1.7.3__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.
- setiastro/images/3dplanet.png +0 -0
- setiastro/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +41 -2
- setiastro/saspro/gui/mixins/file_mixin.py +6 -2
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +8 -1
- setiastro/saspro/imageops/serloader.py +101 -17
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/resources.py +2 -0
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +21 -1
- setiastro/saspro/ser_stacker.py +8 -2
- setiastro/saspro/ser_stacker_dialog.py +37 -10
- setiastro/saspro/ser_tracking.py +57 -35
- setiastro/saspro/serviewer.py +164 -16
- setiastro/saspro/subwindow.py +36 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +28 -26
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/layers_dock.py
CHANGED
|
@@ -8,19 +8,141 @@ from PyQt6.QtCore import Qt, pyqtSignal, QByteArray, QTimer
|
|
|
8
8
|
from PyQt6.QtWidgets import (
|
|
9
9
|
QDockWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
|
10
10
|
QListWidget, QListWidgetItem, QAbstractItemView, QSlider, QCheckBox,
|
|
11
|
-
QPushButton, QFrame, QMessageBox
|
|
11
|
+
QPushButton, QFrame, QMessageBox,QDialog, QFormLayout, QDoubleSpinBox, QDialogButtonBox
|
|
12
12
|
)
|
|
13
13
|
from PyQt6.QtGui import QIcon, QDragEnterEvent, QDropEvent, QPixmap, QCursor
|
|
14
14
|
|
|
15
|
+
from setiastro.saspro.layers import LayerTransform
|
|
15
16
|
from setiastro.saspro.dnd_mime import MIME_VIEWSTATE, MIME_MASK
|
|
16
17
|
from setiastro.saspro.layers import composite_stack, ImageLayer, BLEND_MODES
|
|
17
18
|
|
|
19
|
+
class _TransformDialog(QDialog):
|
|
20
|
+
"""
|
|
21
|
+
Live-preview transform dialog.
|
|
22
|
+
- Updates the layer transform as the user edits values (debounced).
|
|
23
|
+
- Cancel restores original transform.
|
|
24
|
+
- OK keeps current previewed transform.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, parent=None, *, t: LayerTransform | None = None,
|
|
27
|
+
apply_cb=None, cancel_cb=None, debounce_ms: int = 200):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.setWindowTitle("Layer Transform")
|
|
30
|
+
self.setModal(True)
|
|
31
|
+
|
|
32
|
+
self._apply_cb = apply_cb
|
|
33
|
+
self._cancel_cb = cancel_cb
|
|
34
|
+
|
|
35
|
+
self._t0 = t or LayerTransform()
|
|
36
|
+
# snapshot original so Cancel can revert even if layer mutated live
|
|
37
|
+
self._orig = LayerTransform(
|
|
38
|
+
tx=float(self._t0.tx), ty=float(self._t0.ty),
|
|
39
|
+
rot_deg=float(self._t0.rot_deg),
|
|
40
|
+
sx=float(self._t0.sx), sy=float(self._t0.sy),
|
|
41
|
+
pivot_x=self._t0.pivot_x, pivot_y=self._t0.pivot_y,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# debounce timer
|
|
45
|
+
self._tmr = QTimer(self)
|
|
46
|
+
self._tmr.setSingleShot(True)
|
|
47
|
+
self._tmr.timeout.connect(self._apply_live)
|
|
48
|
+
self._debounce_ms = int(debounce_ms)
|
|
49
|
+
|
|
50
|
+
lay = QVBoxLayout(self)
|
|
51
|
+
form = QFormLayout()
|
|
52
|
+
lay.addLayout(form)
|
|
53
|
+
|
|
54
|
+
def mk_spin(minv, maxv, step, dec, val):
|
|
55
|
+
s = QDoubleSpinBox(self)
|
|
56
|
+
s.setRange(minv, maxv)
|
|
57
|
+
s.setSingleStep(step)
|
|
58
|
+
s.setDecimals(dec)
|
|
59
|
+
s.setValue(float(val))
|
|
60
|
+
return s
|
|
61
|
+
|
|
62
|
+
self.tx = mk_spin(-100000, 100000, 1.0, 2, self._t0.tx)
|
|
63
|
+
self.ty = mk_spin(-100000, 100000, 1.0, 2, self._t0.ty)
|
|
64
|
+
self.rot = mk_spin(-3600, 3600, 0.1, 3, self._t0.rot_deg)
|
|
65
|
+
self.sx = mk_spin(0.001, 1000.0, 0.01, 4, self._t0.sx)
|
|
66
|
+
self.sy = mk_spin(0.001, 1000.0, 0.01, 4, self._t0.sy)
|
|
67
|
+
|
|
68
|
+
form.addRow("Translate X (px)", self.tx)
|
|
69
|
+
form.addRow("Translate Y (px)", self.ty)
|
|
70
|
+
form.addRow("Rotate (deg)", self.rot)
|
|
71
|
+
form.addRow("Scale X", self.sx)
|
|
72
|
+
form.addRow("Scale Y", self.sy)
|
|
73
|
+
|
|
74
|
+
# Buttons
|
|
75
|
+
btns = QDialogButtonBox(
|
|
76
|
+
QDialogButtonBox.StandardButton.Ok |
|
|
77
|
+
QDialogButtonBox.StandardButton.Cancel,
|
|
78
|
+
parent=self
|
|
79
|
+
)
|
|
80
|
+
lay.addWidget(btns)
|
|
81
|
+
|
|
82
|
+
self.btn_reset = QPushButton("Reset")
|
|
83
|
+
btns.addButton(self.btn_reset, QDialogButtonBox.ButtonRole.ResetRole)
|
|
84
|
+
|
|
85
|
+
btns.accepted.connect(self.accept)
|
|
86
|
+
btns.rejected.connect(self.reject)
|
|
87
|
+
self.btn_reset.clicked.connect(self._reset)
|
|
88
|
+
|
|
89
|
+
# Any value change triggers debounced live preview
|
|
90
|
+
for w in (self.tx, self.ty, self.rot, self.sx, self.sy):
|
|
91
|
+
w.valueChanged.connect(self._schedule_live)
|
|
92
|
+
|
|
93
|
+
# Apply initial once so opening dialog matches actual preview
|
|
94
|
+
self._schedule_live()
|
|
95
|
+
|
|
96
|
+
def _reset(self):
|
|
97
|
+
self.tx.setValue(0.0)
|
|
98
|
+
self.ty.setValue(0.0)
|
|
99
|
+
self.rot.setValue(0.0)
|
|
100
|
+
self.sx.setValue(1.0)
|
|
101
|
+
self.sy.setValue(1.0)
|
|
102
|
+
self._schedule_live()
|
|
103
|
+
|
|
104
|
+
def _schedule_live(self, *_):
|
|
105
|
+
# restart debounce
|
|
106
|
+
self._tmr.start(self._debounce_ms)
|
|
107
|
+
|
|
108
|
+
def _current_transform(self) -> LayerTransform:
|
|
109
|
+
return LayerTransform(
|
|
110
|
+
tx=float(self.tx.value()),
|
|
111
|
+
ty=float(self.ty.value()),
|
|
112
|
+
rot_deg=float(self.rot.value()),
|
|
113
|
+
sx=float(self.sx.value()),
|
|
114
|
+
sy=float(self.sy.value()),
|
|
115
|
+
pivot_x=None,
|
|
116
|
+
pivot_y=None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _apply_live(self):
|
|
120
|
+
if callable(self._apply_cb):
|
|
121
|
+
self._apply_cb(self._current_transform())
|
|
122
|
+
|
|
123
|
+
def reject(self):
|
|
124
|
+
# Cancel: restore original, then close
|
|
125
|
+
if callable(self._cancel_cb):
|
|
126
|
+
self._cancel_cb(self._orig)
|
|
127
|
+
super().reject()
|
|
128
|
+
|
|
129
|
+
def accept(self):
|
|
130
|
+
# OK: ensure latest values are applied (in case debounce pending)
|
|
131
|
+
try:
|
|
132
|
+
if self._tmr.isActive():
|
|
133
|
+
self._tmr.stop()
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
self._apply_live()
|
|
137
|
+
super().accept()
|
|
138
|
+
|
|
18
139
|
# ---------- Small row widget for a layer ----------
|
|
19
140
|
class _LayerRow(QWidget):
|
|
20
141
|
changed = pyqtSignal()
|
|
21
142
|
requestDelete = pyqtSignal()
|
|
22
143
|
moveUp = pyqtSignal()
|
|
23
144
|
moveDown = pyqtSignal()
|
|
145
|
+
requestTransform = pyqtSignal()
|
|
24
146
|
|
|
25
147
|
def __init__(self, name: str, mode: str = "Normal", opacity: float = 1.0,
|
|
26
148
|
visible: bool = True, parent=None, *, is_base: bool = False):
|
|
@@ -41,9 +163,10 @@ class _LayerRow(QWidget):
|
|
|
41
163
|
self.btn_up = QPushButton("↑"); self.btn_up.setFixedWidth(28)
|
|
42
164
|
self.btn_dn = QPushButton("↓"); self.btn_dn.setFixedWidth(28)
|
|
43
165
|
self.btn_x = QPushButton("✕"); self.btn_x.setFixedWidth(28)
|
|
44
|
-
|
|
166
|
+
self.btn_tf = QPushButton("Transform…")
|
|
167
|
+
self.btn_tf.setFixedWidth(92)
|
|
45
168
|
r1.addWidget(self.chk); r1.addWidget(self.lbl, 1)
|
|
46
|
-
r1.addWidget(self.mode); r1.addWidget(QLabel("Opacity")); r1.addWidget(self.sld, 1)
|
|
169
|
+
r1.addWidget(self.mode); r1.addWidget(QLabel("Opacity")); r1.addWidget(self.sld, 1); r1.addWidget(self.btn_tf)
|
|
47
170
|
r1.addWidget(self.btn_up); r1.addWidget(self.btn_dn); r1.addWidget(self.btn_x)
|
|
48
171
|
|
|
49
172
|
# row 2: mask controls (hidden for base)
|
|
@@ -89,7 +212,7 @@ class _LayerRow(QWidget):
|
|
|
89
212
|
|
|
90
213
|
if self._is_base:
|
|
91
214
|
# Base row is informational only
|
|
92
|
-
for w in (self.chk, self.mode, self.sld, self.btn_up, self.btn_dn, self.btn_x,
|
|
215
|
+
for w in (self.chk, self.mode, self.sld, self.btn_up, self.btn_tf, self.btn_dn, self.btn_x,
|
|
93
216
|
self.mask_combo, self.mask_invert, self.btn_clear_mask):
|
|
94
217
|
w.setEnabled(False)
|
|
95
218
|
self.lbl.setStyleSheet("color: palette(mid);")
|
|
@@ -104,6 +227,8 @@ class _LayerRow(QWidget):
|
|
|
104
227
|
self.btn_up.clicked.connect(self.moveUp.emit)
|
|
105
228
|
self.btn_dn.clicked.connect(self.moveDown.emit)
|
|
106
229
|
|
|
230
|
+
self.btn_tf.clicked.connect(self.requestTransform.emit)
|
|
231
|
+
|
|
107
232
|
# Sigmoid controls emit change + only show for Sigmoid mode
|
|
108
233
|
if self.sig_center is not None:
|
|
109
234
|
self.sig_center.valueChanged.connect(self._emit)
|
|
@@ -116,6 +241,17 @@ class _LayerRow(QWidget):
|
|
|
116
241
|
# Initial visibility
|
|
117
242
|
self._update_extra_controls(self.mode.currentText())
|
|
118
243
|
|
|
244
|
+
def setTransformDirty(self, dirty: bool):
|
|
245
|
+
if hasattr(self, "btn_tf") and self.btn_tf is not None:
|
|
246
|
+
self.btn_tf.setText("Transform… *" if dirty else "Transform…")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _on_transform_clicked(self):
|
|
250
|
+
# Let the dock handle it; row doesn't own the layer object
|
|
251
|
+
self.changed.emit() # (no-op, but keeps pattern)
|
|
252
|
+
# We'll have the dock connect this via a custom signal (see next step).
|
|
253
|
+
|
|
254
|
+
|
|
119
255
|
def _on_mode_changed(self, _idx: int):
|
|
120
256
|
# Update which extra controls are visible
|
|
121
257
|
self._update_extra_controls(self.mode.currentText())
|
|
@@ -420,7 +556,12 @@ class LayersDock(QDockWidget):
|
|
|
420
556
|
it.setSizeHint(roww.sizeHint())
|
|
421
557
|
self.list.addItem(it)
|
|
422
558
|
self.list.setItemWidget(it, roww)
|
|
423
|
-
|
|
559
|
+
t = getattr(lyr, "transform", None)
|
|
560
|
+
dirty = False
|
|
561
|
+
if t is not None:
|
|
562
|
+
dirty = (abs(t.tx) > 1e-6 or abs(t.ty) > 1e-6 or abs(t.rot_deg) > 1e-6 or
|
|
563
|
+
abs(t.sx - 1.0) > 1e-6 or abs(t.sy - 1.0) > 1e-6)
|
|
564
|
+
roww.setTransformDirty(dirty)
|
|
424
565
|
base_name = getattr(vw, "_effective_title", None)
|
|
425
566
|
base_name = base_name() if callable(base_name) else "Current View"
|
|
426
567
|
base_label = f"Base • {base_name}"
|
|
@@ -545,6 +686,58 @@ class LayersDock(QDockWidget):
|
|
|
545
686
|
roww.requestDelete.connect(lambda: self._delete_row(roww))
|
|
546
687
|
roww.moveUp.connect(lambda: self._move_row(roww, -1))
|
|
547
688
|
roww.moveDown.connect(lambda: self._move_row(roww, +1))
|
|
689
|
+
roww.requestTransform.connect(lambda rw=roww: self._edit_transform_for_row(rw))
|
|
690
|
+
|
|
691
|
+
def _edit_transform_for_row(self, roww: _LayerRow):
|
|
692
|
+
vw = self.current_view()
|
|
693
|
+
if not vw:
|
|
694
|
+
return
|
|
695
|
+
idx = self._find_row_index(roww)
|
|
696
|
+
if idx < 0 or idx >= self._layer_count():
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
lyr = vw._layers[idx]
|
|
700
|
+
|
|
701
|
+
# Ensure transform exists
|
|
702
|
+
t = getattr(lyr, "transform", None)
|
|
703
|
+
if t is None:
|
|
704
|
+
t = LayerTransform()
|
|
705
|
+
lyr.transform = t
|
|
706
|
+
|
|
707
|
+
# Helper: apply transform + refresh preview
|
|
708
|
+
def _apply_t(new_t: LayerTransform):
|
|
709
|
+
lyr.transform = new_t
|
|
710
|
+
# update the row star immediately (optional)
|
|
711
|
+
dirty = (abs(new_t.tx) > 1e-6 or abs(new_t.ty) > 1e-6 or abs(new_t.rot_deg) > 1e-6 or
|
|
712
|
+
abs(new_t.sx - 1.0) > 1e-6 or abs(new_t.sy - 1.0) > 1e-6)
|
|
713
|
+
try:
|
|
714
|
+
roww.setTransformDirty(dirty)
|
|
715
|
+
except Exception:
|
|
716
|
+
pass
|
|
717
|
+
|
|
718
|
+
# Live preview refresh
|
|
719
|
+
vw.apply_layer_stack(vw._layers)
|
|
720
|
+
|
|
721
|
+
# Helper: cancel revert
|
|
722
|
+
def _cancel_revert(orig_t: LayerTransform):
|
|
723
|
+
lyr.transform = orig_t
|
|
724
|
+
dirty = (abs(orig_t.tx) > 1e-6 or abs(orig_t.ty) > 1e-6 or abs(orig_t.rot_deg) > 1e-6 or
|
|
725
|
+
abs(orig_t.sx - 1.0) > 1e-6 or abs(orig_t.sy - 1.0) > 1e-6)
|
|
726
|
+
try:
|
|
727
|
+
roww.setTransformDirty(dirty)
|
|
728
|
+
except Exception:
|
|
729
|
+
pass
|
|
730
|
+
vw.apply_layer_stack(vw._layers)
|
|
731
|
+
|
|
732
|
+
dlg = _TransformDialog(
|
|
733
|
+
self,
|
|
734
|
+
t=lyr.transform,
|
|
735
|
+
apply_cb=_apply_t,
|
|
736
|
+
cancel_cb=_cancel_revert,
|
|
737
|
+
debounce_ms=200, # tweak 150–300
|
|
738
|
+
)
|
|
739
|
+
dlg.exec()
|
|
740
|
+
|
|
548
741
|
|
|
549
742
|
def _apply_list_to_view_debounced(self):
|
|
550
743
|
# restart the timer on every slider tick
|
|
@@ -2007,7 +2007,8 @@ def save_image(img_array,
|
|
|
2007
2007
|
is_mono=False,
|
|
2008
2008
|
image_meta=None,
|
|
2009
2009
|
file_meta=None,
|
|
2010
|
-
wcs_header=None
|
|
2010
|
+
wcs_header=None,
|
|
2011
|
+
jpeg_quality: int | None = None):
|
|
2011
2012
|
"""
|
|
2012
2013
|
Save an image array to a file in the specified format and bit depth.
|
|
2013
2014
|
- Robust to mis-ordered positional args (header/bit_depth swap).
|
|
@@ -2060,9 +2061,14 @@ def save_image(img_array,
|
|
|
2060
2061
|
|
|
2061
2062
|
if fmt == "jpg":
|
|
2062
2063
|
img = Image.fromarray((np.clip(img_array, 0, 1) * 255).astype(np.uint8))
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2064
|
+
|
|
2065
|
+
q = 95 if jpeg_quality is None else int(jpeg_quality)
|
|
2066
|
+
q = max(1, min(100, q))
|
|
2067
|
+
|
|
2068
|
+
# subsampling=0 keeps best chroma quality; optimize can reduce size a bit
|
|
2069
|
+
img.save(filename, quality=q, subsampling=0, optimize=True)
|
|
2070
|
+
|
|
2071
|
+
print(f"Saved 8-bit JPG image to: {filename} (quality={q})")
|
|
2066
2072
|
return
|
|
2067
2073
|
|
|
2068
2074
|
# ---------------------------------------------------------------------
|