Coverage for paintmaskanimator/main_window_onion.py: 30%

143 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""Onion skin; owned by ``MainWindow`` as ``window.onion``. 

2 

3Toggles onion-skin display, owns the floating settings window, and implements 

4the "center canvas between onion shifts" transform. 

5 

6A collaborator rather than a mixin -- see ``main_window_export.py`` for why. 

7""" 

8from typing import TYPE_CHECKING 

9 

10import math 

11from PySide6.QtCore import QPointF, Qt 

12from PySide6.QtGui import QColor 

13 

14from .i18n import tr 

15from .onion import OnionSkinSettingsBrowser 

16from .tool_window import ToolWindow 

17 

18if TYPE_CHECKING: 

19 from .main_window import MainWindow 

20 

21 

22class OnionSkinController: 

23 """Owned by ``MainWindow`` as ``window.onion``. 

24 

25 A collaborator rather than a mixin -- see main_window_export.py for why. 

26 """ 

27 

28 def __init__(self, window: "MainWindow"): 

29 self.window = window 

30 

31 """Onion-skin toggles and the onion settings window.""" 

32 

33 def set_all_layers(self, enabled): 

34 self.window.canvas.onion_all_layers = bool(enabled) 

35 self.window.canvas._onion_cache.clear() 

36 self.window.canvas.update() 

37 

38 def set_enabled(self, enabled): 

39 self.window.canvas.onion_skin = bool(enabled) 

40 self.window.canvas._onion_cache.clear() 

41 self.window.canvas.update() 

42 

43 def toggle_settings_popup(self, checked): 

44 checked = bool(checked) 

45 if checked: 45 ↛ 48line 45 didn't jump to line 48 because the condition on line 45 was always true

46 self.show_settings() 

47 else: 

48 settings_window = self.window._onion_settings_window 

49 if settings_window is not None: 

50 settings_window.close() 

51 self.window.canvas.update() 

52 

53 def show_settings(self): 

54 browser = self.window._onion_settings_browser 

55 if browser is not None: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

56 if self.window._onion_settings_window is not None: 

57 self.window._onion_settings_window.show_and_raise() 

58 return 

59 

60 browser = OnionSkinSettingsBrowser( 

61 self.window.canvas.onion_previous_count, 

62 self.window.canvas.onion_next_count, 

63 self.window.canvas.onion_previous_opacity, 

64 self.window.canvas.onion_next_opacity, 

65 self.window.canvas.onion_previous_color, 

66 self.window.canvas.onion_next_color, 

67 self.window.canvas.onion_previous_color_enabled, 

68 self.window.canvas.onion_next_color_enabled, 

69 self.window.canvas.onion_selected_colors_only, 

70 self.window.canvas.onion_previous_shift_x, 

71 self.window.canvas.onion_previous_shift_y, 

72 self.window.canvas.onion_previous_rotation, 

73 self.window.canvas.onion_next_shift_x, 

74 self.window.canvas.onion_next_shift_y, 

75 self.window.canvas.onion_next_rotation, 

76 self.window.canvas.onion_previous_scale, 

77 self.window.canvas.onion_next_scale, 

78 self.window.canvas.onion_previous_levels, 

79 self.window.canvas.onion_next_levels, 

80 self.window.canvas.onion_center_percent, 

81 self.window.canvas.rotation, 

82 self.window, 

83 ) 

84 self.window._onion_settings_browser = browser 

85 browser.settingsChanged.connect( 

86 self.apply_browser_settings 

87 ) 

88 browser.shiftEditRequested.connect( 

89 self.window.canvas.begin_onion_shift_interaction 

90 ) 

91 browser.canvasPositionEditRequested.connect( 

92 self.window.canvas.begin_onion_canvas_position_interaction 

93 ) 

94 browser.canvasRotationRequested.connect( 

95 self.window.canvas.set_onion_canvas_view_rotation 

96 ) 

97 browser.centerCanvasRequested.connect( 

98 self.center_canvas_between_shifts 

99 ) 

100 browser.destroyed.connect( 

101 self._settings_browser_destroyed 

102 ) 

103 

104 # パネルの列に差し込むのではなく、キャンバスの上に浮かぶ別ウィンドウで 

105 # 開く。閉じると中身ごと破棄し、次に開くときは現在の設定から作り直す。 

106 settings_window = ToolWindow( 

107 self.window, tr("オニオンスキン設定"), "onion_settings", browser, 

108 ) 

109 settings_window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) 

110 self.window._onion_settings_window = settings_window 

111 settings_window.show_and_raise() 

112 

113 def _settings_browser_destroyed(self, *_args): 

114 self.window._onion_settings_browser = None 

115 self.window._onion_settings_window = None 

116 self.window.canvas.cancel_onion_interaction(restore=False) 

117 if self.window.timeline.onion_settings.isChecked(): 117 ↛ exitline 117 didn't return from function '_settings_browser_destroyed' because the condition on line 117 was always true

118 self.window.timeline.onion_settings.blockSignals(True) 

119 self.window.timeline.onion_settings.setChecked(False) 

120 self.window.timeline.onion_settings.blockSignals(False) 

121 

122 def apply_browser_settings(self): 

123 browser = self.window._onion_settings_browser 

124 if browser is None: 

125 return 

126 

127 self.window.canvas.onion_previous_count = ( 

128 browser.previous_count.value() 

129 ) 

130 self.window.canvas.onion_next_count = ( 

131 browser.next_count.value() 

132 ) 

133 self.window.canvas.onion_previous_opacity = ( 

134 browser.previous_opacity.value() / 100.0 

135 ) 

136 self.window.canvas.onion_next_opacity = ( 

137 browser.next_opacity.value() / 100.0 

138 ) 

139 self.window.canvas.onion_previous_levels = ( 

140 browser.previous_levels() 

141 ) 

142 self.window.canvas.onion_next_levels = ( 

143 browser.next_levels() 

144 ) 

145 self.window.canvas.onion_previous_color = QColor( 

146 browser.previous_color 

147 ) 

148 self.window.canvas.onion_next_color = QColor( 

149 browser.next_color 

150 ) 

151 self.window.canvas.onion_previous_color_enabled = ( 

152 browser.previous_color_enabled.isChecked() 

153 ) 

154 self.window.canvas.onion_next_color_enabled = ( 

155 browser.next_color_enabled.isChecked() 

156 ) 

157 self.window.canvas.onion_selected_colors_only = ( 

158 browser.selected_colors_only.isChecked() 

159 ) 

160 self.window.canvas.onion_previous_shift_x = float( 

161 browser.previous_shift_x.value() 

162 ) 

163 self.window.canvas.onion_previous_shift_y = float( 

164 browser.previous_shift_y.value() 

165 ) 

166 self.window.canvas.onion_previous_rotation = float( 

167 browser.previous_rotation.value() 

168 ) 

169 self.window.canvas.onion_next_shift_x = float( 

170 browser.next_shift_x.value() 

171 ) 

172 self.window.canvas.onion_next_shift_y = float( 

173 browser.next_shift_y.value() 

174 ) 

175 self.window.canvas.onion_next_rotation = float( 

176 browser.next_rotation.value() 

177 ) 

178 self.window.canvas.onion_previous_scale = float( 

179 browser.previous_scale.value() 

180 ) 

181 self.window.canvas.onion_next_scale = float( 

182 browser.next_scale.value() 

183 ) 

184 self.window.canvas.onion_center_percent = float( 

185 browser.center_percent_value.value() 

186 ) 

187 

188 self.window.canvas._onion_cache.clear() 

189 self.window.canvas.update() 

190 

191 def sync_browser_from_canvas(self): 

192 browser = self.window._onion_settings_browser 

193 if browser is None: 

194 return 

195 browser.set_transform_values( 

196 self.window.canvas.onion_previous_shift_x, 

197 self.window.canvas.onion_previous_shift_y, 

198 self.window.canvas.onion_previous_rotation, 

199 self.window.canvas.onion_previous_scale, 

200 self.window.canvas.onion_next_shift_x, 

201 self.window.canvas.onion_next_shift_y, 

202 self.window.canvas.onion_next_rotation, 

203 self.window.canvas.onion_next_scale, 

204 ) 

205 browser.set_canvas_rotation_value( 

206 self.window.canvas.rotation 

207 ) 

208 

209 def finish_browser_interaction(self): 

210 browser = self.window._onion_settings_browser 

211 if browser is not None: 

212 browser.clear_interaction_buttons() 

213 

214 def center_canvas_between_shifts(self, percent): 

215 """前後間のTU/TB変形を現在キャンバスの基準へ取り込む。""" 

216 self.apply_browser_settings() 

217 percent = max(0.0, min(100.0, float(percent))) 

218 self.window.canvas.onion_center_percent = percent 

219 ratio = percent / 100.0 

220 

221 previous_x = float(self.window.canvas.onion_previous_shift_x) 

222 previous_y = float(self.window.canvas.onion_previous_shift_y) 

223 next_x = float(self.window.canvas.onion_next_shift_x) 

224 next_y = float(self.window.canvas.onion_next_shift_y) 

225 previous_rotation = float( 

226 self.window.canvas.onion_previous_rotation 

227 ) 

228 next_rotation = float( 

229 self.window.canvas.onion_next_rotation 

230 ) 

231 previous_scale = max( 

232 0.01, 

233 float(self.window.canvas.onion_previous_scale) / 100.0, 

234 ) 

235 next_scale = max( 

236 0.01, 

237 float(self.window.canvas.onion_next_scale) / 100.0, 

238 ) 

239 

240 target_x = previous_x + (next_x - previous_x) * ratio 

241 target_y = previous_y + (next_y - previous_y) * ratio 

242 

243 # 回転は最短方向、TU/TB拡大率は撮影倍率として線形補間する。 

244 rotation_delta = ( 

245 (next_rotation - previous_rotation + 180.0) 

246 % 360.0 

247 ) - 180.0 

248 target_rotation = self.window.canvas._normalized_angle( 

249 previous_rotation + rotation_delta * ratio 

250 ) 

251 target_scale = max( 

252 0.01, 

253 previous_scale + (next_scale - previous_scale) * ratio, 

254 ) 

255 

256 # 表示用デジタルズームself.canvas.zoomは変更しない。 

257 digital_zoom = max(0.01, float(self.window.canvas.zoom)) 

258 current_tu_tb = max( 

259 0.0001, 

260 float(self.window.canvas.onion_tu_tb_scale), 

261 ) 

262 old_view_rotation = float(self.window.canvas.rotation) 

263 view_angle = math.radians(old_view_rotation) 

264 local_display_x = ( 

265 target_x * digital_zoom * current_tu_tb 

266 ) 

267 local_display_y = ( 

268 target_y * digital_zoom * current_tu_tb 

269 ) 

270 display_x = ( 

271 local_display_x * math.cos(view_angle) 

272 - local_display_y * math.sin(view_angle) 

273 ) 

274 display_y = ( 

275 local_display_x * math.sin(view_angle) 

276 + local_display_y * math.cos(view_angle) 

277 ) 

278 

279 self.window.canvas.pan += QPointF(display_x, display_y) 

280 self.window.canvas.rotation = self.window.canvas._normalized_angle( 

281 old_view_rotation + target_rotation 

282 ) 

283 self.window.canvas.onion_tu_tb_scale = max( 

284 0.05, 

285 min(20.0, current_tu_tb * target_scale), 

286 ) 

287 

288 # H_target^-1 × H_eachで相対位置・回転・倍率を再取得。 

289 angle = math.radians(-target_rotation) 

290 cos_angle = math.cos(angle) 

291 sin_angle = math.sin(angle) 

292 

293 def relative_shift(source_x, source_y): 

294 dx = float(source_x) - target_x 

295 dy = float(source_y) - target_y 

296 return QPointF( 

297 (dx * cos_angle - dy * sin_angle) 

298 / target_scale, 

299 (dx * sin_angle + dy * cos_angle) 

300 / target_scale, 

301 ) 

302 

303 previous_relative = relative_shift( 

304 previous_x, 

305 previous_y, 

306 ) 

307 next_relative = relative_shift(next_x, next_y) 

308 

309 # 選択した中央%の回転を新しい0°基準として取得する。 

310 # キャンバス表示角度は0°へ戻し、前後の角度は 

311 # 取得した中央回転との差分として再設定する。 

312 absorbed_view_rotation = float(self.window.canvas.rotation) 

313 previous_screen_relative = self.window.canvas._rotated_vector( 

314 previous_relative.x(), 

315 previous_relative.y(), 

316 absorbed_view_rotation, 

317 ) 

318 next_screen_relative = self.window.canvas._rotated_vector( 

319 next_relative.x(), 

320 next_relative.y(), 

321 absorbed_view_rotation, 

322 ) 

323 

324 self.window.canvas.onion_previous_shift_x = ( 

325 previous_screen_relative.x() 

326 ) 

327 self.window.canvas.onion_previous_shift_y = ( 

328 previous_screen_relative.y() 

329 ) 

330 self.window.canvas.onion_next_shift_x = ( 

331 next_screen_relative.x() 

332 ) 

333 self.window.canvas.onion_next_shift_y = ( 

334 next_screen_relative.y() 

335 ) 

336 self.window.canvas.onion_previous_rotation = ( 

337 self.window.canvas._normalized_angle( 

338 previous_rotation - target_rotation 

339 ) 

340 ) 

341 self.window.canvas.onion_next_rotation = ( 

342 self.window.canvas._normalized_angle( 

343 next_rotation - target_rotation 

344 ) 

345 ) 

346 self.window.canvas.rotation = 0.0 

347 self.window.canvas.onion_previous_scale = max( 

348 1.0, 

349 min(199.0, previous_scale / target_scale * 100.0), 

350 ) 

351 self.window.canvas.onion_next_scale = max( 

352 1.0, 

353 min(199.0, next_scale / target_scale * 100.0), 

354 ) 

355 

356 self.sync_browser_from_canvas() 

357 self.window.canvas.viewChanged.emit( 

358 float(self.window.canvas.zoom), 

359 float(self.window.canvas.rotation), 

360 ) 

361 self.window.canvas.update() 

362 self.window.statusBar().showMessage( 

363 tr("前後の位置・回転・TU/TB拡大率間の{percent:g}%をキャンバス基準へ移しました。(中央回転 {rotation:.1f}°を基準として取得/撮影倍率 {value:.1f}%/キャンバス回転 0°/デジタルズーム変更なし)").format(percent=percent, rotation=target_rotation, value=target_scale * 100.0), 

364 4200, 

365 )