messageflight 0.2.4__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.
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from PyQt6.QtCore import Qt
6
+ from PyQt6.QtGui import QColor, QPainter, QPainterPath
7
+
8
+ from .base import ParamDef, PlanePreset
9
+
10
+
11
+ @dataclass
12
+ class RocketParameters:
13
+ body_length: int = 60
14
+ body_width: int = 16
15
+ nose_length: int = 20
16
+ fin_size: int = 12
17
+ flame_length: int = 15
18
+ body_color: str = "#C0C0C0"
19
+ nose_color: str = "#FF4500"
20
+ fin_color: str = "#8B0000"
21
+ flame_color: str = "#FFA500"
22
+ flame_intensity: float = 1.0
23
+ banner_color: str = "#FF6347"
24
+ text_color: str = "#FFFFFF"
25
+ rotation: float = 0.0
26
+ banner_attach_x: int = -12
27
+ banner_attach_y: int = 0
28
+
29
+
30
+ class RocketPreset(PlanePreset):
31
+ name = "火箭"
32
+ icon = "🚀"
33
+
34
+ def draw(self, painter: QPainter, params: RocketParameters) -> None:
35
+ bl = params.body_length
36
+ bw = params.body_width
37
+ nl = params.nose_length
38
+ fs = params.fin_size
39
+ fl = params.flame_length
40
+ painter.save()
41
+ # Nose cone (triangle pointing right)
42
+ painter.setPen(Qt.PenStyle.NoPen)
43
+ painter.setBrush(QColor(params.nose_color))
44
+ nose = QPainterPath()
45
+ nose.moveTo(bl, -bw // 2)
46
+ nose.lineTo(bl + nl, 0)
47
+ nose.lineTo(bl, bw // 2)
48
+ nose.closeSubpath()
49
+ painter.drawPath(nose)
50
+ # Body
51
+ painter.setBrush(QColor(params.body_color))
52
+ painter.drawRect(0, -bw // 2, bl, bw)
53
+ # Fins (two triangles at left end)
54
+ painter.setBrush(QColor(params.fin_color))
55
+ fin_top = QPainterPath()
56
+ fin_top.moveTo(0, -bw // 2)
57
+ fin_top.lineTo(-fs, -bw // 2 - fs)
58
+ fin_top.lineTo(0, -bw // 2 + fs // 2)
59
+ fin_top.closeSubpath()
60
+ painter.drawPath(fin_top)
61
+ fin_bot = QPainterPath()
62
+ fin_bot.moveTo(0, bw // 2)
63
+ fin_bot.lineTo(-fs, bw // 2 + fs)
64
+ fin_bot.lineTo(0, bw // 2 - fs // 2)
65
+ fin_bot.closeSubpath()
66
+ painter.drawPath(fin_bot)
67
+ # Flame
68
+ fl_actual = int(fl * params.flame_intensity)
69
+ if fl_actual > 0:
70
+ painter.setBrush(QColor(params.flame_color))
71
+ painter.drawEllipse(-fl_actual, -bw // 4, fl_actual, bw // 2)
72
+ painter.restore()
73
+
74
+ def get_parameters(self) -> list[ParamDef]:
75
+ return [
76
+ ParamDef("body_length", "机身长度", "int", 60, 30, 100),
77
+ ParamDef("body_width", "机身宽度", "int", 16, 8, 32),
78
+ ParamDef("nose_length", "机头长度", "int", 20, 10, 50),
79
+ ParamDef("fin_size", "尾翼大小", "int", 12, 4, 24),
80
+ ParamDef("flame_length", "火焰长度", "int", 15, 5, 40),
81
+ ParamDef("body_color", "机身颜色", "color", "#C0C0C0"),
82
+ ParamDef("nose_color", "机头颜色", "color", "#FF4500"),
83
+ ParamDef("fin_color", "尾翼颜色", "color", "#8B0000"),
84
+ ParamDef("flame_color", "火焰颜色", "color", "#FFA500"),
85
+ ParamDef("flame_intensity", "火焰强度", "float", 1.0, 0.0, 2.0, 0.1),
86
+ ParamDef("banner_color", "横幅颜色", "color", "#FF6347"),
87
+ ParamDef("rotation", "旋转角度", "float", 0.0, -45.0, 45.0, 1.0),
88
+ ParamDef("text_color", "文字颜色", "color", "#FFFFFF"),
89
+ ParamDef("banner_attach_x", "横幅挂载X", "int", -12, -50, 100),
90
+ ParamDef("banner_attach_y", "横幅挂载Y", "int", 0, -50, 100),
91
+ ]
92
+
93
+ def get_default_params(self) -> RocketParameters:
94
+ return RocketParameters()
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from PyQt6.QtCore import Qt
6
+ from PyQt6.QtGui import QColor, QPainter, QPainterPath
7
+
8
+ from .base import ParamDef, PlanePreset
9
+
10
+
11
+ @dataclass
12
+ class UFOParameters:
13
+ disc_radius: int = 30
14
+ dome_radius: int = 15
15
+ beam_width: int = 20
16
+ beam_length: int = 25
17
+ disc_color: str = "#808080"
18
+ dome_color: str = "#C0C0C0"
19
+ beam_color: str = "#00FF00"
20
+ glow_intensity: float = 0.8
21
+ banner_color: str = "#9370DB"
22
+ text_color: str = "#FFFFFF"
23
+ rotation: float = 0.0
24
+ banner_attach_x: int = -30
25
+ banner_attach_y: int = 0
26
+
27
+
28
+ class UFOPreset(PlanePreset):
29
+ name = "UFO"
30
+ icon = "🛸"
31
+
32
+ def draw(self, painter: QPainter, params: UFOParameters) -> None:
33
+ dr = params.disc_radius
34
+ dor = params.dome_radius
35
+ bw = params.beam_width
36
+ bl = params.beam_length
37
+ painter.save()
38
+ beam_color = QColor(params.beam_color)
39
+ beam_color.setAlphaF(0.3 * params.glow_intensity)
40
+ painter.setPen(Qt.PenStyle.NoPen)
41
+ painter.setBrush(beam_color)
42
+ beam = QPainterPath()
43
+ beam.moveTo(-bw // 2, 0)
44
+ beam.lineTo(bw // 2, 0)
45
+ beam.lineTo(bw, bl)
46
+ beam.lineTo(-bw, bl)
47
+ beam.closeSubpath()
48
+ painter.drawPath(beam)
49
+ painter.setBrush(QColor(params.disc_color))
50
+ painter.drawEllipse(-dr, -dr // 2, dr * 2, dr)
51
+ painter.setBrush(QColor(params.dome_color))
52
+ painter.drawEllipse(-dor // 2, -dr - dor // 2, dor, dor)
53
+ painter.restore()
54
+
55
+ def get_parameters(self) -> list[ParamDef]:
56
+ return [
57
+ ParamDef("disc_radius", "圆盘半径", "int", 30, 10, 60),
58
+ ParamDef("dome_radius", "圆顶半径", "int", 15, 5, 30),
59
+ ParamDef("beam_width", "光束宽度", "int", 20, 5, 40),
60
+ ParamDef("beam_length", "光束长度", "int", 25, 5, 50),
61
+ ParamDef("disc_color", "圆盘颜色", "color", "#808080"),
62
+ ParamDef("dome_color", "圆顶颜色", "color", "#C0C0C0"),
63
+ ParamDef("beam_color", "光束颜色", "color", "#00FF00"),
64
+ ParamDef("glow_intensity", "发光强度", "float", 0.8, 0.0, 1.0, 0.1),
65
+ ParamDef("banner_color", "横幅颜色", "color", "#9370DB"),
66
+ ParamDef("rotation", "旋转角度", "float", 0.0, -45.0, 45.0, 1.0),
67
+ ParamDef("text_color", "文字颜色", "color", "#FFFFFF"),
68
+ ParamDef("banner_attach_x", "横幅挂载X", "int", -30, -50, 100),
69
+ ParamDef("banner_attach_y", "横幅挂载Y", "int", 0, -50, 100),
70
+ ]
71
+
72
+ def get_default_params(self) -> UFOParameters:
73
+ return UFOParameters()
@@ -0,0 +1,345 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ from typing import Optional, cast
6
+
7
+ from PyQt6.QtCore import Qt
8
+ from PyQt6.QtGui import QColor, QFont, QPainter, QPen
9
+ from PyQt6.QtWidgets import (
10
+ QCheckBox,
11
+ QColorDialog,
12
+ QComboBox,
13
+ QDialog,
14
+ QDialogButtonBox,
15
+ QDoubleSpinBox,
16
+ QFormLayout,
17
+ QHBoxLayout,
18
+ QLabel,
19
+ QLineEdit,
20
+ QPushButton,
21
+ QScrollArea,
22
+ QSpinBox,
23
+ QVBoxLayout,
24
+ QWidget,
25
+ )
26
+
27
+ from message_flight.config import AppConfig
28
+ from message_flight.i18n import tr
29
+ from message_flight.plane_presets import get_preset, list_presets
30
+ from message_flight.plane_presets.base import PlanePreset
31
+
32
+
33
+ class PresetPreviewWidget(QWidget):
34
+ _CENTER_X = 100
35
+ _CENTER_Y = 75
36
+ _GRAB_RADIUS = 10
37
+
38
+ def __init__(self, preset: PlanePreset, params, on_mount_changed=None, parent=None):
39
+ super().__init__(parent)
40
+ self._preset = preset
41
+ self._params = params
42
+ self._on_mount_changed = on_mount_changed
43
+ self._dragging = False
44
+ self.setFixedSize(200, 150)
45
+ self.setCursor(Qt.CursorShape.CrossCursor)
46
+
47
+ def update_preset(self, preset) -> None:
48
+ """Replace the active preset and request a repaint."""
49
+ self._preset = preset
50
+ self.update()
51
+
52
+ def update_params(self, params) -> None:
53
+ self._params = params
54
+ self.update()
55
+
56
+ def _get_mount(self) -> tuple[int, int]:
57
+ return (
58
+ getattr(self._params, "banner_attach_x", 0),
59
+ getattr(self._params, "banner_attach_y", 0),
60
+ )
61
+
62
+ def _set_mount(self, mx: int, my: int) -> None:
63
+ if hasattr(self._params, "banner_attach_x"):
64
+ self._params.banner_attach_x = int(mx)
65
+ if hasattr(self._params, "banner_attach_y"):
66
+ self._params.banner_attach_y = int(my)
67
+ if self._on_mount_changed is not None:
68
+ self._on_mount_changed(mx, my)
69
+ self.update()
70
+
71
+ # ------------------------------------------------------------------
72
+ # 鼠标事件
73
+ # ------------------------------------------------------------------
74
+ def mousePressEvent(self, event):
75
+ mx, my = self._world_to_mount(event.pos())
76
+ cur_mx, cur_my = self._get_mount()
77
+ dist = ((mx - cur_mx) ** 2 + (my - cur_my) ** 2) ** 0.5
78
+ if dist < self._GRAB_RADIUS:
79
+ self._dragging = True
80
+ self.setCursor(Qt.CursorShape.ClosedHandCursor)
81
+ else:
82
+ self._set_mount(mx, my)
83
+
84
+ def mouseMoveEvent(self, event):
85
+ if self._dragging:
86
+ mx, my = self._world_to_mount(event.pos())
87
+ self._set_mount(mx, my)
88
+
89
+ def mouseReleaseEvent(self, event):
90
+ if self._dragging:
91
+ self._dragging = False
92
+ self.setCursor(Qt.CursorShape.CrossCursor)
93
+
94
+ def _world_to_mount(self, pos) -> tuple[int, int]:
95
+ return int(pos.x() - self._CENTER_X), int(pos.y() - self._CENTER_Y)
96
+
97
+ # ------------------------------------------------------------------
98
+ # 绘制
99
+ # ------------------------------------------------------------------
100
+ def paintEvent(self, event):
101
+ painter = QPainter(self)
102
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
103
+ painter.fillRect(self.rect(), QColor("#f5f5f5"))
104
+
105
+ # 轻微网格线
106
+ pen = QPen(QColor("#e0e0e0"))
107
+ pen.setWidth(1)
108
+ painter.setPen(pen)
109
+ for x in range(0, 201, 20):
110
+ painter.drawLine(x, 0, x, 150)
111
+ for y in range(0, 151, 20):
112
+ painter.drawLine(0, y, 200, y)
113
+
114
+ # 飞船原点(中心点)
115
+ painter.setPen(QPen(QColor("#888888"), 1))
116
+ painter.drawEllipse(self._CENTER_X - 2, self._CENTER_Y - 2, 4, 4)
117
+
118
+ # 绘制飞船(translate 到中心)
119
+ painter.translate(self._CENTER_X, self._CENTER_Y)
120
+ self._preset.draw(painter, self._params)
121
+ painter.translate(-self._CENTER_X, -self._CENTER_Y)
122
+
123
+ # 挂载点
124
+ mx, my = self._get_mount()
125
+ wx = self._CENTER_X + mx
126
+ wy = self._CENTER_Y + my
127
+
128
+ # 虚线连接
129
+ dash = QPen(QColor("#FF5722"), 1, Qt.PenStyle.DotLine)
130
+ painter.setPen(dash)
131
+ painter.drawLine(self._CENTER_X, self._CENTER_Y, wx, wy)
132
+
133
+ # 挂载点手柄
134
+ painter.setPen(QPen(QColor("#D32F2F"), 2))
135
+ painter.setBrush(QColor("#FF5252"))
136
+ painter.drawEllipse(wx - 5, wy - 5, 10, 10)
137
+
138
+ # 坐标标签
139
+ painter.setPen(QColor("#333333"))
140
+ font = QFont("Microsoft YaHei", 8)
141
+ painter.setFont(font)
142
+ label = f"({mx}, {my})"
143
+ painter.drawText(wx + 8, wy + 4, label)
144
+
145
+ painter.end()
146
+
147
+
148
+ class PresetEditorWindow(QDialog):
149
+ def __init__(self, cfg: AppConfig, parent: Optional[QWidget] = None) -> None:
150
+ super().__init__(parent)
151
+ self._language = cfg.language
152
+ self.setWindowTitle(tr("preset_editor.title", self._language))
153
+ self.setModal(True)
154
+ self._cfg = cfg
155
+ self._preset_key = cfg.plane_preset_key or "airplane"
156
+ preset_obj = get_preset(self._preset_key)
157
+ if cfg.plane_preset_params_json:
158
+ try:
159
+ data = json.loads(cfg.plane_preset_params_json)
160
+ default = preset_obj.get_default_params()
161
+ self._params = dataclasses.replace(
162
+ default,
163
+ **{k: v for k, v in data.items() if hasattr(default, k)},
164
+ )
165
+ except (json.JSONDecodeError, TypeError):
166
+ self._params = preset_obj.get_default_params()
167
+ else:
168
+ self._params = preset_obj.get_default_params()
169
+ self._param_widgets: dict[str, QWidget] = {}
170
+
171
+ root = QVBoxLayout(self)
172
+ top_row = QHBoxLayout()
173
+ top_row.addWidget(QLabel(tr("preset_editor.preset", self._language)))
174
+ self._preset_combo = QComboBox()
175
+ for key, name, icon in list_presets():
176
+ self._preset_combo.addItem(f"{icon} {name}", key)
177
+ idx = next(
178
+ (i for i in range(self._preset_combo.count())
179
+ if self._preset_combo.itemData(i) == self._preset_key),
180
+ 0,
181
+ )
182
+ self._preset_combo.setCurrentIndex(idx)
183
+ self._preset_combo.currentIndexChanged.connect(self._on_preset_changed)
184
+ top_row.addWidget(self._preset_combo)
185
+ top_row.addStretch(1)
186
+ root.addLayout(top_row)
187
+
188
+ middle = QHBoxLayout()
189
+ self._param_panel = QWidget()
190
+ self._param_layout = QFormLayout(self._param_panel)
191
+ scroll = QScrollArea()
192
+ scroll.setWidget(self._param_panel)
193
+ scroll.setWidgetResizable(True)
194
+ scroll.setFixedWidth(280)
195
+ middle.addWidget(scroll)
196
+
197
+ # 预览 + 拖拽回调
198
+ self._preview = PresetPreviewWidget(
199
+ preset_obj, self._params,
200
+ on_mount_changed=self._on_preview_mount_changed,
201
+ )
202
+ middle.addWidget(self._preview)
203
+ root.addLayout(middle)
204
+
205
+ self._button_box = QDialogButtonBox(
206
+ QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
207
+ )
208
+ self._button_box.accepted.connect(self.accept)
209
+ self._button_box.rejected.connect(self.reject)
210
+ root.addWidget(self._button_box)
211
+
212
+ self._build_param_panel()
213
+
214
+ def _on_preview_mount_changed(self, mx: int, my: int) -> None:
215
+ """预览拖拽时同步更新参数面板中的 SpinBox。"""
216
+ for name in ("banner_attach_x", "banner_attach_y"):
217
+ spin = self._param_widgets.get(name)
218
+ if spin is not None:
219
+ value = mx if name == "banner_attach_x" else my
220
+ spin_box = cast(QSpinBox, spin)
221
+ spin_box.blockSignals(True)
222
+ spin_box.setValue(value)
223
+ spin_box.blockSignals(False)
224
+
225
+ def _refresh_preview(self) -> None:
226
+ self._preview.update_params(self._params)
227
+
228
+ def _on_preset_changed(self, index: int) -> None:
229
+ key = self._preset_combo.itemData(index)
230
+ self._preset_key = key
231
+ preset_obj = get_preset(key)
232
+ self._params = preset_obj.get_default_params()
233
+ self._build_param_panel()
234
+ self._preview.update_preset(preset_obj)
235
+ self._refresh_preview()
236
+
237
+ def _build_param_panel(self) -> None:
238
+ while self._param_layout.count():
239
+ item = self._param_layout.takeAt(0)
240
+ if item is None:
241
+ continue
242
+ w = item.widget()
243
+ if w is not None:
244
+ w.deleteLater()
245
+ self._param_widgets.clear()
246
+ preset_obj = get_preset(self._preset_key)
247
+ for param_def in preset_obj.get_parameters():
248
+ value = getattr(self._params, param_def.name)
249
+ if param_def.type == "color":
250
+ row_widget = QWidget()
251
+ row_layout = QHBoxLayout(row_widget)
252
+ row_layout.setContentsMargins(0, 0, 0, 0)
253
+ edit = QLineEdit(str(value))
254
+ swatch = QLabel()
255
+ swatch.setFixedSize(24, 18)
256
+ qc = QColor(str(value))
257
+ swatch.setStyleSheet(
258
+ f"background-color: {qc.name() if qc.isValid() else '#888888'};"
259
+ )
260
+
261
+ def make_picker(_edit=edit, _swatch=swatch, _n=param_def.name, _label=param_def.label):
262
+ def open_picker():
263
+ current = QColor(_edit.text())
264
+ chosen = QColorDialog.getColor(
265
+ current,
266
+ self,
267
+ tr("preset_editor.choose_color", self._language, label=_label),
268
+ )
269
+ if chosen.isValid():
270
+ _edit.setText(chosen.name())
271
+ _swatch.setStyleSheet(
272
+ f"background-color: {chosen.name()};"
273
+ )
274
+ setattr(self._params, _n, chosen.name())
275
+ self._refresh_preview()
276
+ return open_picker
277
+ picker_btn = QPushButton("…")
278
+ picker_btn.setFixedWidth(30)
279
+ picker_btn.clicked.connect(make_picker())
280
+ edit.textChanged.connect(
281
+ lambda text, _e=edit, _s=swatch, _n=param_def.name:
282
+ self._on_color_text_changed(_n, text, _e, _s)
283
+ )
284
+ row_layout.addWidget(edit)
285
+ row_layout.addWidget(swatch)
286
+ row_layout.addWidget(picker_btn)
287
+ self._param_layout.addRow(param_def.label, row_widget)
288
+ self._param_widgets[param_def.name] = edit
289
+ elif param_def.type == "int":
290
+ spin = QSpinBox()
291
+ spin.setRange(
292
+ int(param_def.min) if param_def.min is not None else 0,
293
+ int(param_def.max) if param_def.max is not None else 999,
294
+ )
295
+ spin.setValue(int(value))
296
+ spin.valueChanged.connect(
297
+ lambda v, _n=param_def.name: self._on_int_changed(_n, v)
298
+ )
299
+ self._param_layout.addRow(param_def.label, spin)
300
+ self._param_widgets[param_def.name] = spin
301
+ elif param_def.type == "float":
302
+ double_spin = QDoubleSpinBox()
303
+ double_spin.setRange(
304
+ float(param_def.min) if param_def.min is not None else 0.0,
305
+ float(param_def.max) if param_def.max is not None else 999.0,
306
+ )
307
+ if param_def.step is not None:
308
+ double_spin.setSingleStep(float(param_def.step))
309
+ double_spin.setValue(float(value))
310
+ double_spin.valueChanged.connect(
311
+ lambda v, _n=param_def.name: self._on_float_changed(_n, v)
312
+ )
313
+ self._param_layout.addRow(param_def.label, double_spin)
314
+ self._param_widgets[param_def.name] = double_spin
315
+ elif param_def.type == "bool":
316
+ cb = QCheckBox()
317
+ cb.setChecked(bool(value))
318
+ cb.toggled.connect(
319
+ lambda checked, _n=param_def.name: self._on_bool_changed(_n, checked)
320
+ )
321
+ self._param_layout.addRow(param_def.label, cb)
322
+ self._param_widgets[param_def.name] = cb
323
+
324
+ def _on_color_text_changed(self, name, text, edit, swatch) -> None:
325
+ qc = QColor(text)
326
+ if qc.isValid():
327
+ swatch.setStyleSheet(f"background-color: {qc.name()};")
328
+ setattr(self._params, name, text)
329
+ self._refresh_preview()
330
+
331
+ def _on_int_changed(self, name, value) -> None:
332
+ setattr(self._params, name, int(value))
333
+ self._refresh_preview()
334
+
335
+ def _on_float_changed(self, name, value) -> None:
336
+ setattr(self._params, name, float(value))
337
+ self._refresh_preview()
338
+
339
+ def _on_bool_changed(self, name, value) -> None:
340
+ setattr(self._params, name, bool(value))
341
+ self._refresh_preview()
342
+
343
+ def get_result(self) -> tuple[str, str]:
344
+ data = dataclasses.asdict(self._params)
345
+ return (self._preset_key, json.dumps(data, ensure_ascii=False))