setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__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/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +114 -37
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +548 -275
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +134 -8
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +246 -16
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +519 -289
- setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +67 -47
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +748 -255
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +97 -11
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +83 -21
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +253 -49
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1610 -574
- setiastro/saspro/star_alignment.py +522 -453
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14733 -135
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +133 -57
- setiastro/saspro/widgets/spinboxes.py +28 -13
- setiastro/saspro/wimi.py +92 -721
- setiastro/saspro/wims.py +46 -36
- setiastro/saspro/window_shelf.py +2 -2
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/whitebalance.py
CHANGED
|
@@ -88,7 +88,7 @@ def plot_star_color_ratios_comparison(raw_pixels: np.ndarray, after_pixels: np.n
|
|
|
88
88
|
|
|
89
89
|
plt.suptitle("Star Color Ratios with RGB Mapping", fontsize=14)
|
|
90
90
|
plt.tight_layout()
|
|
91
|
-
plt.show()
|
|
91
|
+
plt.show(block=False)
|
|
92
92
|
|
|
93
93
|
def apply_manual_white_balance(img: np.ndarray, r_gain: float, g_gain: float, b_gain: float) -> np.ndarray:
|
|
94
94
|
"""Simple per-channel gain, clipped to [0,1]."""
|
|
@@ -184,6 +184,30 @@ def apply_white_balance_to_doc(doc, preset: Optional[Dict] = None):
|
|
|
184
184
|
step_name="White Balance",
|
|
185
185
|
)
|
|
186
186
|
|
|
187
|
+
def apply_pivot_gain(img: np.ndarray, med: np.ndarray, gains: np.ndarray) -> np.ndarray:
|
|
188
|
+
# img: HxWx3 float32 in [0,1]
|
|
189
|
+
med3 = med.reshape(1, 1, 3).astype(np.float32)
|
|
190
|
+
g3 = gains.reshape(1, 1, 3).astype(np.float32)
|
|
191
|
+
|
|
192
|
+
# pivot around median; do not scale negative deltas
|
|
193
|
+
d = img - med3
|
|
194
|
+
d = np.maximum(d, 0.0)
|
|
195
|
+
out = d * g3 + med3
|
|
196
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
197
|
+
|
|
198
|
+
def smoothstep(edge0, edge1, x):
|
|
199
|
+
t = np.clip((x - edge0) / (edge1 - edge0 + 1e-12), 0.0, 1.0)
|
|
200
|
+
return t * t * (3.0 - 2.0 * t)
|
|
201
|
+
|
|
202
|
+
def apply_soft_protect(img: np.ndarray, out_pivot: np.ndarray, k: float = 0.02) -> np.ndarray:
|
|
203
|
+
# luminance-based fade-in above median luminance
|
|
204
|
+
L = 0.2126*img[...,0] + 0.7152*img[...,1] + 0.0722*img[...,2]
|
|
205
|
+
Lm = float(np.median(L))
|
|
206
|
+
w = smoothstep(Lm, Lm + k, L).astype(np.float32)
|
|
207
|
+
w3 = w[..., None]
|
|
208
|
+
out = img * (1.0 - w3) + out_pivot * w3
|
|
209
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
210
|
+
|
|
187
211
|
|
|
188
212
|
# -------------------------
|
|
189
213
|
# Interactive dialog (UI)
|
|
@@ -193,16 +217,23 @@ class WhiteBalanceDialog(QDialog):
|
|
|
193
217
|
super().__init__(parent)
|
|
194
218
|
self._main = parent
|
|
195
219
|
self.doc = doc
|
|
196
|
-
|
|
197
|
-
# Connect to active document change signal
|
|
220
|
+
self._active_doc_conn = False
|
|
198
221
|
if hasattr(self._main, "currentDocumentChanged"):
|
|
199
|
-
|
|
222
|
+
try:
|
|
223
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
224
|
+
self._active_doc_conn = True
|
|
225
|
+
except Exception:
|
|
226
|
+
self._active_doc_conn = False
|
|
200
227
|
if icon:
|
|
201
228
|
self.setWindowIcon(icon)
|
|
202
229
|
self.setWindowTitle(self.tr("White Balance"))
|
|
203
230
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
204
231
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
205
232
|
self.setModal(False)
|
|
233
|
+
try:
|
|
234
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass # older PyQt6 versions
|
|
206
237
|
self.resize(900, 600)
|
|
207
238
|
|
|
208
239
|
self._build_ui()
|
|
@@ -213,6 +244,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
213
244
|
self._update_mode_widgets()
|
|
214
245
|
# kick off a first detection preview
|
|
215
246
|
QTimer.singleShot(200, self._update_star_preview)
|
|
247
|
+
self.finished.connect(lambda *_: self._cleanup())
|
|
248
|
+
|
|
216
249
|
|
|
217
250
|
# ---- UI construction ------------------------------------------------
|
|
218
251
|
def _build_ui(self):
|
|
@@ -327,11 +360,16 @@ class WhiteBalanceDialog(QDialog):
|
|
|
327
360
|
)
|
|
328
361
|
self.star_count.setText(self.tr("Detected {0} stars.").format(count))
|
|
329
362
|
# to pixmap
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
363
|
+
overlay8 = np.ascontiguousarray(np.clip(overlay * 255.0, 0, 255).astype(np.uint8))
|
|
364
|
+
h, w, _ = overlay8.shape
|
|
365
|
+
qimg = QImage(overlay8.data, w, h, 3 * w, QImage.Format.Format_RGB888)
|
|
366
|
+
|
|
367
|
+
# Make sure the numpy buffer stays alive until QPixmap is created
|
|
368
|
+
pm = QPixmap.fromImage(qimg.copy()).scaled(
|
|
369
|
+
self.preview.width(), self.preview.height(),
|
|
370
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
371
|
+
Qt.TransformationMode.SmoothTransformation
|
|
372
|
+
)
|
|
335
373
|
self.preview.setPixmap(pm)
|
|
336
374
|
except Exception as e:
|
|
337
375
|
self.star_count.setText(self.tr("Detection failed."))
|
|
@@ -394,7 +432,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
394
432
|
# Use the headless helper so doc metadata is consistent
|
|
395
433
|
apply_white_balance_to_doc(self.doc, preset)
|
|
396
434
|
# Dialog stays open - refresh document for next operation
|
|
397
|
-
self.
|
|
435
|
+
self._finish_and_close()
|
|
436
|
+
return
|
|
398
437
|
|
|
399
438
|
elif mode == "Auto":
|
|
400
439
|
preset = {"mode": "auto"}
|
|
@@ -404,7 +443,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
404
443
|
|
|
405
444
|
apply_white_balance_to_doc(self.doc, preset)
|
|
406
445
|
# Dialog stays open - refresh document for next operation
|
|
407
|
-
self.
|
|
446
|
+
self._finish_and_close()
|
|
447
|
+
return
|
|
408
448
|
|
|
409
449
|
else: # --- Star-Based: compute here so we can plot like SASv2 ---
|
|
410
450
|
thr = float(self.thr_slider.value())
|
|
@@ -472,7 +512,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
472
512
|
self.tr("Star-Based WB applied.\nDetected {0} stars.").format(int(star_count)),
|
|
473
513
|
)
|
|
474
514
|
# Dialog stays open - refresh document for next operation
|
|
475
|
-
self.
|
|
515
|
+
self._finish_and_close()
|
|
516
|
+
return
|
|
476
517
|
|
|
477
518
|
except Exception as e:
|
|
478
519
|
QMessageBox.critical(self, self.tr("White Balance"), self.tr("Failed to apply White Balance:\n{0}").format(e))
|
|
@@ -490,3 +531,34 @@ class WhiteBalanceDialog(QDialog):
|
|
|
490
531
|
self.doc = new_doc
|
|
491
532
|
except Exception:
|
|
492
533
|
pass
|
|
534
|
+
|
|
535
|
+
def _finish_and_close(self):
|
|
536
|
+
"""
|
|
537
|
+
Close this dialog after a successful apply.
|
|
538
|
+
Use accept() so it behaves like a successful completion.
|
|
539
|
+
"""
|
|
540
|
+
try:
|
|
541
|
+
self.accept()
|
|
542
|
+
except Exception:
|
|
543
|
+
self.close()
|
|
544
|
+
|
|
545
|
+
def _cleanup(self):
|
|
546
|
+
# Disconnect active-doc signal so the main window doesn't keep us alive
|
|
547
|
+
try:
|
|
548
|
+
if self._active_doc_conn and hasattr(self._main, "currentDocumentChanged"):
|
|
549
|
+
self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
550
|
+
except Exception:
|
|
551
|
+
pass
|
|
552
|
+
self._active_doc_conn = False
|
|
553
|
+
|
|
554
|
+
# Stop debounce timer
|
|
555
|
+
try:
|
|
556
|
+
if getattr(self, "_debounce", None) is not None:
|
|
557
|
+
self._debounce.stop()
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def closeEvent(self, ev):
|
|
563
|
+
self._cleanup()
|
|
564
|
+
super().closeEvent(ev)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#src/setiastro/saspro/widgets/common_utilities.py
|
|
1
2
|
"""
|
|
2
3
|
Common UI utilities and shared components.
|
|
3
4
|
|
|
@@ -220,32 +221,38 @@ _strip_ui_decorations = strip_ui_decorations
|
|
|
220
221
|
# ---------------------------------------------------------------------------
|
|
221
222
|
|
|
222
223
|
def install_crash_handlers(app: 'QApplication') -> None:
|
|
223
|
-
"""
|
|
224
|
-
Install global crash and exception handlers for the application.
|
|
225
|
-
|
|
226
|
-
Sets up:
|
|
227
|
-
1. faulthandler for hard crashes (segfaults) → saspro_crash.log
|
|
228
|
-
2. sys.excepthook for uncaught main thread exceptions
|
|
229
|
-
3. threading.excepthook for uncaught background thread exceptions
|
|
230
|
-
|
|
231
|
-
All exceptions are logged and displayed in a dialog to the user.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
app: The QApplication instance
|
|
235
|
-
|
|
236
|
-
Example:
|
|
237
|
-
app = QApplication(sys.argv)
|
|
238
|
-
install_crash_handlers(app)
|
|
239
|
-
"""
|
|
240
224
|
import faulthandler
|
|
241
|
-
|
|
242
|
-
|
|
225
|
+
import tempfile
|
|
226
|
+
from pathlib import Path
|
|
227
|
+
|
|
228
|
+
def _get_crash_log_path() -> str:
|
|
229
|
+
try:
|
|
230
|
+
if hasattr(sys, "_MEIPASS"):
|
|
231
|
+
if sys.platform.startswith("win"):
|
|
232
|
+
log_dir = Path(os.path.expandvars("%APPDATA%")) / "SetiAstroSuitePro" / "logs"
|
|
233
|
+
elif sys.platform.startswith("darwin"):
|
|
234
|
+
log_dir = Path.home() / "Library" / "Logs" / "SetiAstroSuitePro"
|
|
235
|
+
else:
|
|
236
|
+
log_dir = Path.home() / ".local" / "share" / "SetiAstroSuitePro" / "logs"
|
|
237
|
+
else:
|
|
238
|
+
# dev fallback
|
|
239
|
+
log_dir = Path("logs")
|
|
240
|
+
|
|
241
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
return str(log_dir / "saspro_crash.log")
|
|
243
|
+
except Exception:
|
|
244
|
+
return str(Path(tempfile.gettempdir()) / "saspro_crash.log")
|
|
245
|
+
|
|
246
|
+
# 1) Hard crashes → saspro_crash.log
|
|
243
247
|
try:
|
|
244
|
-
|
|
248
|
+
crash_path = _get_crash_log_path()
|
|
249
|
+
_crash_log = open(crash_path, "a", encoding="utf-8", errors="replace")
|
|
245
250
|
faulthandler.enable(file=_crash_log, all_threads=True)
|
|
246
251
|
atexit.register(_crash_log.close)
|
|
252
|
+
logging.info("Faulthandler crash log: %s", crash_path)
|
|
247
253
|
except Exception:
|
|
248
254
|
logging.exception("Failed to enable faulthandler")
|
|
255
|
+
|
|
249
256
|
|
|
250
257
|
def _show_dialog(title: str, head: str, details: str) -> None:
|
|
251
258
|
"""Show error dialog marshaled to main thread."""
|
|
@@ -261,7 +268,7 @@ def install_crash_handlers(app: 'QApplication') -> None:
|
|
|
261
268
|
m.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
262
269
|
m.exec()
|
|
263
270
|
QTimer.singleShot(0, _ui)
|
|
264
|
-
|
|
271
|
+
|
|
265
272
|
# 2) Any uncaught exception on the main thread
|
|
266
273
|
def _excepthook(exc_type, exc_value, exc_tb):
|
|
267
274
|
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
@@ -9,7 +9,7 @@ const ctx = canvas.getContext('2d');
|
|
|
9
9
|
// --- CONSTANTS ---
|
|
10
10
|
const GAME_WIDTH = 800;
|
|
11
11
|
const GAME_HEIGHT = 600;
|
|
12
|
-
const FPS = 60
|
|
12
|
+
const FPS = 48; // Reduced to 80% speed (Original 60)
|
|
13
13
|
const DT = 1 / FPS;
|
|
14
14
|
|
|
15
15
|
const COLORS = {
|
|
@@ -971,14 +971,19 @@ function updateHUD() {
|
|
|
971
971
|
|
|
972
972
|
// Game Loop
|
|
973
973
|
let lastTimeMs = 0;
|
|
974
|
+
const FRAME_INTERVAL = 1000 / FPS;
|
|
975
|
+
|
|
974
976
|
function loop(timestamp) {
|
|
975
|
-
|
|
976
|
-
lastTimeMs = timestamp;
|
|
977
|
+
requestAnimationFrame(loop);
|
|
977
978
|
|
|
978
|
-
|
|
979
|
-
|
|
979
|
+
if (!lastTimeMs) lastTimeMs = timestamp;
|
|
980
|
+
const elapsed = timestamp - lastTimeMs;
|
|
980
981
|
|
|
981
|
-
|
|
982
|
+
if (elapsed > FRAME_INTERVAL) {
|
|
983
|
+
lastTimeMs = timestamp - (elapsed % FRAME_INTERVAL);
|
|
984
|
+
Game.update();
|
|
985
|
+
Game.draw();
|
|
986
|
+
}
|
|
982
987
|
}
|
|
983
988
|
|
|
984
989
|
// Start
|
|
@@ -5,93 +5,122 @@ import psutil
|
|
|
5
5
|
from PyQt6.QtCore import Qt, QUrl, QTimer, QObject, pyqtProperty, pyqtSignal, QThread
|
|
6
6
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QFrame
|
|
7
7
|
from PyQt6.QtQuickWidgets import QQuickWidget
|
|
8
|
-
|
|
8
|
+
import time
|
|
9
|
+
import subprocess
|
|
9
10
|
from setiastro.saspro.memory_utils import get_memory_usage_mb
|
|
10
11
|
from setiastro.saspro.resources import _get_base_path
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
class GPUWorker(QThread):
|
|
13
|
-
"""Background worker to monitor GPU without blocking the UI."""
|
|
14
15
|
resultReady = pyqtSignal(float)
|
|
15
16
|
|
|
16
17
|
def __init__(self, has_nvidia: bool, parent=None):
|
|
17
18
|
super().__init__(parent)
|
|
18
19
|
self._has_nvidia = has_nvidia
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
# cache + throttle (Windows PowerShell is expensive)
|
|
22
|
+
self._last_win_poll = 0.0
|
|
23
|
+
self._cached_win_val = 0.0
|
|
24
|
+
|
|
25
|
+
self._last_emit = 0.0
|
|
26
|
+
self._last_emitted_val = None
|
|
27
|
+
|
|
28
|
+
def _startupinfo_hidden(self):
|
|
29
|
+
if os.name != "nt":
|
|
30
|
+
return None
|
|
31
|
+
si = subprocess.STARTUPINFO()
|
|
32
|
+
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
33
|
+
si.wShowWindow = 0
|
|
34
|
+
return si
|
|
20
35
|
|
|
21
36
|
def _get_windows_gpu_load(self) -> float:
|
|
22
|
-
if os.name !=
|
|
37
|
+
if os.name != "nt":
|
|
23
38
|
return 0.0
|
|
39
|
+
|
|
40
|
+
now = time.monotonic()
|
|
41
|
+
|
|
42
|
+
# THROTTLE: run this at most once every 1.5 seconds
|
|
43
|
+
if (now - self._last_win_poll) < 1.5:
|
|
44
|
+
return self._cached_win_val
|
|
45
|
+
|
|
46
|
+
self._last_win_poll = now
|
|
47
|
+
|
|
24
48
|
try:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
# Use explicit powershell.exe and make it non-interactive + hidden
|
|
50
|
+
cmd = [
|
|
51
|
+
"powershell.exe",
|
|
52
|
+
"-NoProfile",
|
|
53
|
+
"-NonInteractive",
|
|
54
|
+
"-ExecutionPolicy", "Bypass",
|
|
55
|
+
"-Command",
|
|
56
|
+
(
|
|
57
|
+
"$x = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine "
|
|
58
|
+
"-ErrorAction SilentlyContinue; "
|
|
59
|
+
"if (-not $x) { 0 } else { "
|
|
60
|
+
" $m = ($x | Measure-Object -Property UtilizationPercentage -Maximum).Maximum; "
|
|
61
|
+
" if ($m) { [math]::Round([double]$m, 1) } else { 0 } "
|
|
62
|
+
"}"
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
out = subprocess.check_output(
|
|
67
|
+
cmd,
|
|
68
|
+
startupinfo=self._startupinfo_hidden(),
|
|
69
|
+
timeout=2.0, # IMPORTANT: don’t allow 5s hangs
|
|
70
|
+
stderr=subprocess.DEVNULL, # keep it quiet
|
|
41
71
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
val_str = out.decode("utf-8").strip()
|
|
48
|
-
|
|
49
|
-
if not val_str: return 0.0
|
|
50
|
-
return float(val_str.replace(",", "."))
|
|
72
|
+
val_str = out.decode("utf-8", errors="ignore").strip()
|
|
73
|
+
|
|
74
|
+
val = float(val_str.replace(",", ".")) if val_str else 0.0
|
|
75
|
+
self._cached_win_val = val
|
|
76
|
+
return val
|
|
51
77
|
except Exception:
|
|
52
|
-
|
|
78
|
+
# keep last known value instead of spamming 0.0
|
|
79
|
+
return self._cached_win_val
|
|
53
80
|
|
|
54
81
|
def _get_gpu_load(self) -> float:
|
|
55
82
|
nv_val = 0.0
|
|
56
83
|
win_val = 0.0
|
|
57
|
-
|
|
58
|
-
#
|
|
84
|
+
|
|
85
|
+
# NVIDIA (fast, keep it)
|
|
59
86
|
if self._has_nvidia:
|
|
60
87
|
try:
|
|
61
|
-
import subprocess
|
|
62
|
-
startupinfo = None
|
|
63
|
-
if os.name == 'nt':
|
|
64
|
-
startupinfo = subprocess.STARTUPINFO()
|
|
65
|
-
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
66
|
-
startupinfo.wShowWindow = 0
|
|
67
|
-
|
|
68
88
|
out = subprocess.check_output(
|
|
69
89
|
["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"],
|
|
70
|
-
startupinfo=
|
|
71
|
-
timeout=0.6
|
|
90
|
+
startupinfo=self._startupinfo_hidden(),
|
|
91
|
+
timeout=0.6,
|
|
92
|
+
stderr=subprocess.DEVNULL,
|
|
72
93
|
)
|
|
73
|
-
line = out.decode("utf-8").strip().split(
|
|
94
|
+
line = out.decode("utf-8", errors="ignore").strip().split("\n")[0]
|
|
74
95
|
nv_val = float(line)
|
|
75
96
|
except Exception:
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
if os.name ==
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Windows integrated (slow, throttled)
|
|
100
|
+
if os.name == "nt":
|
|
80
101
|
win_val = self._get_windows_gpu_load()
|
|
81
|
-
|
|
102
|
+
|
|
82
103
|
return max(nv_val, win_val)
|
|
83
104
|
|
|
84
105
|
def run(self):
|
|
85
106
|
while not self.isInterruptionRequested():
|
|
86
107
|
try:
|
|
87
108
|
val = self._get_gpu_load()
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
|
|
110
|
+
# Optional: emit only if value changed a bit, or once per 250ms max
|
|
111
|
+
now = time.monotonic()
|
|
112
|
+
if (
|
|
113
|
+
self._last_emitted_val is None
|
|
114
|
+
or abs(val - self._last_emitted_val) >= 1.0
|
|
115
|
+
or (now - self._last_emit) >= 0.5
|
|
116
|
+
):
|
|
117
|
+
self._last_emit = now
|
|
118
|
+
self._last_emitted_val = val
|
|
119
|
+
self.resultReady.emit(val)
|
|
120
|
+
|
|
92
121
|
self.msleep(250)
|
|
93
122
|
except Exception:
|
|
94
|
-
self.msleep(1000)
|
|
123
|
+
self.msleep(1000)
|
|
95
124
|
|
|
96
125
|
class ResourceBackend(QObject):
|
|
97
126
|
"""Backend logic for the QML Resource Monitor."""
|
|
@@ -197,6 +226,7 @@ class SystemMonitorWidget(QQuickWidget):
|
|
|
197
226
|
self.setAttribute(Qt.WidgetAttribute.WA_AlwaysStackOnTop, False)
|
|
198
227
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
199
228
|
self.setClearColor(Qt.GlobalColor.transparent)
|
|
229
|
+
self._qml_push_pending = False
|
|
200
230
|
|
|
201
231
|
# Connect Backend
|
|
202
232
|
self.backend = ResourceBackend(self)
|
|
@@ -218,16 +248,23 @@ class SystemMonitorWidget(QQuickWidget):
|
|
|
218
248
|
# Let's re-write the QML loading part to use a safer 'initialProperties' approach or just signal/slots.
|
|
219
249
|
#
|
|
220
250
|
# EASIEST: QML binds to `root.cpuUsage`. Python sets `root.cpuUsage`.
|
|
221
|
-
|
|
222
|
-
self.backend.cpuChanged.connect(self._push_data_to_qml)
|
|
223
|
-
self.backend.ramChanged.connect(self._push_data_to_qml)
|
|
224
|
-
self.backend.gpuChanged.connect(self._push_data_to_qml)
|
|
225
|
-
self.backend.appRamChanged.connect(self._push_data_to_qml)
|
|
251
|
+
|
|
226
252
|
|
|
227
253
|
# Load QML
|
|
228
254
|
qml_path = os.path.join(_get_base_path(), "qml", "ResourceMonitor.qml")
|
|
229
255
|
self.setSource(QUrl.fromLocalFile(qml_path))
|
|
230
256
|
|
|
257
|
+
def _schedule_qml_push(self):
|
|
258
|
+
if self._qml_push_pending:
|
|
259
|
+
return
|
|
260
|
+
self._qml_push_pending = True
|
|
261
|
+
QTimer.singleShot(0, self._push_data_to_qml_coalesced)
|
|
262
|
+
|
|
263
|
+
def _push_data_to_qml_coalesced(self):
|
|
264
|
+
self._qml_push_pending = False
|
|
265
|
+
self._push_data_to_qml()
|
|
266
|
+
|
|
267
|
+
|
|
231
268
|
def _push_data_to_qml(self):
|
|
232
269
|
root = self.rootObject()
|
|
233
270
|
if root:
|
|
@@ -235,3 +272,42 @@ class SystemMonitorWidget(QQuickWidget):
|
|
|
235
272
|
root.setProperty("ramUsage", self.backend.ramUsage)
|
|
236
273
|
root.setProperty("gpuUsage", self.backend.gpuUsage)
|
|
237
274
|
root.setProperty("appRamString", self.backend.appRamString)
|
|
275
|
+
|
|
276
|
+
# --- Drag & Drop Support ---
|
|
277
|
+
def mousePressEvent(self, event):
|
|
278
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
279
|
+
# Wayland-friendly: ask compositor to move the window
|
|
280
|
+
wh = self.windowHandle()
|
|
281
|
+
if wh is not None:
|
|
282
|
+
try:
|
|
283
|
+
# Works best for frameless overlays on Wayland
|
|
284
|
+
wh.startSystemMove()
|
|
285
|
+
event.accept()
|
|
286
|
+
return
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
# Fallback (Windows/X11): manual move tracking
|
|
291
|
+
self._drag_start_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
|
292
|
+
event.accept()
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
super().mousePressEvent(event)
|
|
296
|
+
|
|
297
|
+
def mouseMoveEvent(self, event):
|
|
298
|
+
if event.buttons() & Qt.MouseButton.LeftButton:
|
|
299
|
+
if hasattr(self, "_drag_start_pos"):
|
|
300
|
+
self.move(event.globalPosition().toPoint() - self._drag_start_pos)
|
|
301
|
+
event.accept()
|
|
302
|
+
else:
|
|
303
|
+
super().mouseMoveEvent(event)
|
|
304
|
+
|
|
305
|
+
def mouseReleaseEvent(self, event):
|
|
306
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
307
|
+
from PyQt6.QtCore import QSettings
|
|
308
|
+
settings = QSettings("SetiAstro", "SetiAstroSuitePro")
|
|
309
|
+
pos = self.pos()
|
|
310
|
+
settings.setValue("ui/resource_monitor_pos_x", pos.x())
|
|
311
|
+
settings.setValue("ui/resource_monitor_pos_y", pos.y())
|
|
312
|
+
event.accept()
|
|
313
|
+
super().mouseReleaseEvent(event)
|
|
@@ -37,7 +37,7 @@ class CustomSpinBox(QWidget):
|
|
|
37
37
|
self.minimum = minimum
|
|
38
38
|
self.maximum = maximum
|
|
39
39
|
self.step = step
|
|
40
|
-
self._value = initial
|
|
40
|
+
self._value = int(initial)
|
|
41
41
|
|
|
42
42
|
# Line edit for value display/entry
|
|
43
43
|
self.lineEdit = QLineEdit(str(initial))
|
|
@@ -77,14 +77,13 @@ class CustomSpinBox(QWidget):
|
|
|
77
77
|
|
|
78
78
|
self._update_button_states()
|
|
79
79
|
|
|
80
|
-
@property
|
|
81
80
|
def value(self) -> int:
|
|
82
|
-
"""
|
|
83
|
-
return self._value
|
|
81
|
+
"""Qt-style getter."""
|
|
82
|
+
return int(self._value)
|
|
84
83
|
|
|
85
84
|
def setValue(self, val: int) -> None:
|
|
86
|
-
|
|
87
|
-
val = max(self.minimum, min(self.maximum, val))
|
|
85
|
+
val = int(val)
|
|
86
|
+
val = max(int(self.minimum), min(int(self.maximum), val))
|
|
88
87
|
if val != self._value:
|
|
89
88
|
self._value = val
|
|
90
89
|
self.lineEdit.setText(str(val))
|
|
@@ -134,8 +133,16 @@ class CustomSpinBox(QWidget):
|
|
|
134
133
|
"""Decrease value by step."""
|
|
135
134
|
self.setValue(self._value - self.step)
|
|
136
135
|
|
|
136
|
+
def value(self) -> int:
|
|
137
|
+
"""
|
|
138
|
+
Qt-compatible getter (QSpinBox uses value()).
|
|
139
|
+
|
|
140
|
+
Note: we also have @property value for convenience,
|
|
141
|
+
but code that expects QSpinBox calls value().
|
|
142
|
+
"""
|
|
143
|
+
return self._value
|
|
144
|
+
|
|
137
145
|
def _update_button_states(self) -> None:
|
|
138
|
-
"""Enable/disable buttons at limits."""
|
|
139
146
|
self.upButton.setEnabled(self._value < self.maximum)
|
|
140
147
|
self.downButton.setEnabled(self._value > self.minimum)
|
|
141
148
|
|
|
@@ -166,7 +173,7 @@ class CustomDoubleSpinBox(QWidget):
|
|
|
166
173
|
self.maximum = maximum
|
|
167
174
|
self.step = step
|
|
168
175
|
self.decimals = decimals
|
|
169
|
-
self._value = initial
|
|
176
|
+
self._value = float(initial)
|
|
170
177
|
|
|
171
178
|
# Line edit for value display/entry
|
|
172
179
|
self.lineEdit = QLineEdit(f"{initial:.{decimals}f}")
|
|
@@ -206,14 +213,13 @@ class CustomDoubleSpinBox(QWidget):
|
|
|
206
213
|
|
|
207
214
|
self._update_button_states()
|
|
208
215
|
|
|
209
|
-
@property
|
|
210
216
|
def value(self) -> float:
|
|
211
|
-
"""
|
|
212
|
-
return self._value
|
|
217
|
+
"""Qt-style getter."""
|
|
218
|
+
return float(self._value)
|
|
213
219
|
|
|
214
220
|
def setValue(self, val: float) -> None:
|
|
215
|
-
|
|
216
|
-
val = max(self.minimum, min(self.maximum, val))
|
|
221
|
+
val = float(val)
|
|
222
|
+
val = max(float(self.minimum), min(float(self.maximum), val))
|
|
217
223
|
if abs(val - self._value) > 1e-10:
|
|
218
224
|
self._value = val
|
|
219
225
|
self.lineEdit.setText(f"{val:.{self.decimals}f}")
|
|
@@ -228,6 +234,15 @@ class CustomDoubleSpinBox(QWidget):
|
|
|
228
234
|
self.setValue(minimum)
|
|
229
235
|
self._update_button_states()
|
|
230
236
|
|
|
237
|
+
def value(self) -> float:
|
|
238
|
+
"""
|
|
239
|
+
Qt-compatible getter (QDoubleSpinBox uses value()).
|
|
240
|
+
|
|
241
|
+
Note: we also have @property value for convenience,
|
|
242
|
+
but code that expects QDoubleSpinBox calls value().
|
|
243
|
+
"""
|
|
244
|
+
return self._value
|
|
245
|
+
|
|
231
246
|
def setMaximum(self, maximum: float) -> None:
|
|
232
247
|
"""Set the maximum value."""
|
|
233
248
|
self.maximum = maximum
|