Coverage for paintmaskanimator/canvas_paint_tools.py: 66%

637 statements  

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

1"""Pixel-writing tools for PaintCanvas: brush, fill, line, shape and lasso. 

2 

3Split out of ``canvas.py`` as a mixin. Everything here turns a pointer gesture 

4into pixels on the active layer -- the pressure-driven brush stroke, the flood 

5fill and its region search, the auto-select-by-region helper, the line/shape tool 

6previews and their commits, and lasso polygon filling. Undo entries are pushed 

7through ``PaintCanvas.push_undo`` as usual; they run against a live 

8``PaintCanvas``. 

9""" 

10import dataclasses 

11import math 

12import numpy as np 

13from typing import Any 

14from PySide6.QtCore import QPoint, QPointF, QRect, QRectF, Qt 

15from PySide6.QtGui import QColor, QImage, QPainter, QPainterPath, QPen, QPolygonF 

16from PySide6.QtWidgets import QApplication 

17from .i18n import tr 

18from ._canvas_members import CanvasMembers 

19from . import bucket_fill, imaging 

20from .undo_entries import LayerRegionUndo 

21from .pressure import _pressure_bezier_at 

22from .brush_raster import paint_segment 

23from .logging_setup import get_logger 

24 

25log = get_logger(__name__) 

26 

27 

28#: ドラッグ塗りで、端に接した領域を「外の背景ではない」とみなす大きさの上限 

29#: (画像全体に対する割合)。最初にクリックした部分の3倍までもよしとする。 

30BUCKET_DRAG_EDGE_AREA = 0.1 

31 

32 

33class PaintToolsMixin(CanvasMembers): 

34 # バケツのドラッグ塗りの途中経過(押している間だけ持つ)。 

35 _bucket_drag: Any = None 

36 

37 def pressure_size_scale(self, pressure): 

38 """筆圧から線幅倍率だけを返す。不透明度には使用しない。""" 

39 if not self.pressure_enabled: 

40 return 1.0 

41 pressure = max( 

42 0.0, 

43 min(1.0, float(pressure)), 

44 ) 

45 points = getattr( 

46 self, 

47 "pressure_curve_points", 

48 None, 

49 ) 

50 if points and len(points) >= 2: 50 ↛ 56line 50 didn't jump to line 56 because the condition on line 50 was always true

51 value = _pressure_bezier_at( 

52 points, 

53 pressure, 

54 ) 

55 else: 

56 value = pressure ** self.pressure_curve 

57 return ( 

58 max(self.pressure_min, value) 

59 * self.pressure_max 

60 ) 

61 

62 def pressure_value(self, pressure): 

63 """旧呼び出し互換。返す値は線幅倍率のみ。""" 

64 return self.pressure_size_scale(pressure) 

65 

66 def _update_stroke_region(self, a, b, width): 

67 """Repaint only the widget area touched by a brush segment.""" 

68 wa = self.canvas_to_widget(a) 

69 wb = self.canvas_to_widget(b) 

70 margin = max(4, int(math.ceil(float(width) * max(self.zoom, 0.01) / 2.0)) + 3) 

71 dirty = QRectF(wa, wb).normalized().adjusted( 

72 -margin, -margin, margin, margin 

73 ).toAlignedRect() 

74 self.update(dirty) 

75 

76 def draw_line(self,a,b,pressure): 

77 """完全な非AAマスクで、下地色と均一にRGB合成する。""" 

78 painter_tool = self.effective_tool() 

79 source_color = self.paint_source_color() 

80 # 筆圧はここで線幅にだけ使用する。 

81 width = max( 

82 0.5, 

83 self.pen_size 

84 * self.pressure_size_scale(pressure) 

85 ) 

86 draw_width = max(0.5, float(width)) 

87 raster_a = QPointF(float(a.x()), float(a.y())) 

88 raster_b = QPointF(float(b.x()), float(b.y())) 

89 

90 margin = int(math.ceil(draw_width / 2.0)) + 3 

91 left = max( 

92 0, 

93 int(math.floor(min(a.x(), b.x()))) - margin, 

94 ) 

95 top = max( 

96 0, 

97 int(math.floor(min(a.y(), b.y()))) - margin, 

98 ) 

99 right = min( 

100 self.active_layer.image.width(), 

101 int(math.ceil(max(a.x(), b.x()))) + margin + 1, 

102 ) 

103 bottom = min( 

104 self.active_layer.image.height(), 

105 int(math.ceil(max(a.y(), b.y()))) + margin + 1, 

106 ) 

107 if right <= left or bottom <= top: 

108 return 

109 

110 rect = QRectF( 

111 left, 

112 top, 

113 right - left, 

114 bottom - top, 

115 ).toAlignedRect() 

116 self._ensure_before_region(rect) 

117 before_region = self._stroke_before_region(rect) 

118 overlay = QImage( 

119 rect.width(), 

120 rect.height(), 

121 QImage.Format.Format_ARGB32_Premultiplied, 

122 ) 

123 overlay.fill(Qt.GlobalColor.transparent) 

124 

125 painter = QPainter(overlay) 

126 painter.setRenderHint( 

127 QPainter.RenderHint.Antialiasing, 

128 False, 

129 ) 

130 painter.setRenderHint( 

131 QPainter.RenderHint.TextAntialiasing, 

132 False, 

133 ) 

134 self.apply_selection_clip( 

135 painter, 

136 rect.x(), 

137 rect.y(), 

138 ) 

139 local_a = QPointF( 

140 raster_a.x() - rect.x(), 

141 raster_a.y() - rect.y(), 

142 ) 

143 local_b = QPointF( 

144 raster_b.x() - rect.x(), 

145 raster_b.y() - rect.y(), 

146 ) 

147 paint_segment(painter, local_a, local_b, source_color, draw_width) 

148 painter.end() 

149 

150 sub_only = bool( 

151 painter_tool == "brush" 

152 and not getattr(self, "mask_all_enabled", True) 

153 ) 

154 if sub_only: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true

155 overlay_rgba = self._qimage_rgba_array(overlay) 

156 base_region = before_region 

157 base_rgba = self._qimage_rgba_array(base_region) 

158 

159 selected_rgbs = self.selected_mask_colors() 

160 eligible = np.zeros( 

161 base_rgba.shape[:2], 

162 dtype=bool, 

163 ) 

164 if selected_rgbs: 

165 packed = ( 

166 ( 

167 base_rgba[:, :, 0].astype(np.uint32) 

168 << 16 

169 ) 

170 | ( 

171 base_rgba[:, :, 1].astype(np.uint32) 

172 << 8 

173 ) 

174 | base_rgba[:, :, 2].astype(np.uint32) 

175 ) 

176 selected_values = np.fromiter( 

177 ( 

178 (r << 16) | (g << 8) | b 

179 for r, g, b in selected_rgbs 

180 ), 

181 dtype=np.uint32, 

182 count=len(selected_rgbs), 

183 ) 

184 eligible |= ( 

185 (base_rgba[:, :, 3] > 0) 

186 & np.isin(packed, selected_values) 

187 ) 

188 if self.background_mask_rgb in selected_rgbs: 

189 eligible |= ( 

190 (base_rgba[:, :, 3] == 0) 

191 | np.all( 

192 base_rgba[:, :, :3] == 255, 

193 axis=2, 

194 ) 

195 ) 

196 

197 overlay_rgba[:, :, 3][~eligible] = 0 

198 overlay = self._rgba_array_to_qimage( 

199 overlay_rgba 

200 ) 

201 

202 colors = self._blend_overlay_into_active_layer( 

203 overlay, 

204 rect.topLeft(), 

205 exact_colors=(source_color,), 

206 ) 

207 color_set = getattr( 

208 self, 

209 "_brush_blended_colors", 

210 None, 

211 ) 

212 if color_set is not None: 212 ↛ 215line 212 didn't jump to line 215 because the condition on line 212 was always true

213 color_set.update(colors) 

214 

215 self.active_layer.has_content = True 

216 self._update_stroke_region(a, b, draw_width) 

217 

218 @staticmethod 

219 def _scanline_connected_region(*args, **kwargs): 

220 return imaging.scanline_connected_region(*args, **kwargs) 

221 

222 def _bucket_tools(self): 

223 """ツールパネル。ウィンドウを持たないテスト用キャンバスでは ``None``。""" 

224 window: Any = self.window() 

225 return getattr(window, "tools", None) 

226 

227 def bucket_options(self, *, respect_modifiers=True): 

228 """現在のバケツ設定を UI から読み出す。 

229 

230 ``respect_modifiers`` が真なら、Shift を押しながらの塗りは含み塗りを 

231 行わない(Issue #43)。ペイントマンと同じく、含み塗りを ON にしたまま 

232 1回だけ外せるようにするため。自動塗りのように人の操作と切り離して 

233 呼ぶところは偽を渡す。 

234 """ 

235 # 含み塗りの対象は、ツールパネルの「含む色」に登録した色。未登録なら 

236 # サブカラー。使用色パネルでの選択に依存させると、何を含むのかを 

237 # 塗る前に別の場所で準備する必要があり分かりにくい。 

238 tools = self._bucket_tools() 

239 slots = getattr(tools, "bucket_include_colors", None) 

240 mask_colors = tuple(slots.colors()) if slots is not None else () 

241 if not mask_colors: 

242 sub = QColor(self.sub_color) 

243 mask_colors = ((sub.red(), sub.green(), sub.blue()),) 

244 options = bucket_fill.BucketOptions.from_tools( 

245 tools, 

246 mask_colors=mask_colors, 

247 background_selected=self.background_mask_rgb in mask_colors, 

248 island_replacements=self._island_replacements(), 

249 ) 

250 if respect_modifiers and options.include_masks and self._shift_held(): 

251 options = dataclasses.replace(options, include_masks=False) 

252 if self.effective_tool() == "closed_fill": 

253 # 閉領域フィル(Issue #41):線の隙間を閉じ、開いた線の先も伸ばして 

254 # から塗る。1ドットの隙間でこぼれるところを拾うための道具なので、 

255 # 隙間閉じを設定任せにしない。逆に「塗り残し拾い」は必要以上に 

256 # 広がる元になるため、こちらでは使わない。 

257 options = dataclasses.replace( 

258 options, 

259 close_gap=True, 

260 extend_lines=True, 

261 island_max_width=0, 

262 ) 

263 return options 

264 

265 @staticmethod 

266 def _shift_held(): 

267 if QApplication.instance() is None: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true

268 return False 

269 return bool( 

270 QApplication.keyboardModifiers() & Qt.KeyboardModifier.ShiftModifier 

271 ) 

272 

273 def _island_replacements(self): 

274 """使用色パネルに登録した置換色 ``((元の RGB, 置換色の RGB), ...)``。""" 

275 window: Any = self.window() 

276 palette = getattr(window, "palette", None) 

277 replacements = getattr(palette, "replacements", None) or {} 

278 return tuple( 

279 (tuple(int(value) for value in source[:3]), 

280 (color.red(), color.green(), color.blue())) 

281 for source, color in replacements.items() 

282 ) 

283 

284 def _fill_islands(self, patch, islands, bounds): 

285 """囲まれた島を、それぞれの置換色で ``patch``(外接矩形の部分)へ塗る。""" 

286 x0, y0, x1, y1 = bounds 

287 written = set() 

288 for rgb, mask in islands: 

289 local = mask[y0:y1, x0:x1] 

290 if local.any(): 290 ↛ 288line 290 didn't jump to line 288 because the condition on line 290 was always true

291 written.update(self._fill_patch(patch, local, QColor(*rgb))) 

292 return written 

293 

294 def _layer_fill_pixels(self, image): 

295 """RGBA8888 に変換した複製と、その画素ビューを返す。""" 

296 converted = image.convertToFormat(QImage.Format.Format_RGBA8888) 

297 width, height = converted.width(), converted.height() 

298 rows = np.frombuffer( 

299 imaging.qimage_buffer(converted), dtype=np.uint8 

300 ).reshape((height, converted.bytesPerLine())) 

301 return converted, rows[:, :width * 4].reshape((height, width, 4)) 

302 

303 # ------------------------------------------------------------------ 

304 # 参照レイヤー:どの絵を見て塗る範囲を決めるか。 

305 # ------------------------------------------------------------------ 

306 def bucket_reference_pixels(self, options, frame, layer_index, 

307 layer_image, layer_pixels): 

308 """判定に使う ``(保持用の QImage, RGBA8888 の画素)`` を返す。 

309 

310 塗る先はいつもアクティブレイヤーなので、ここで返すのは「どこを塗るか」を 

311 決めるための絵だけ。参照が「選択したレイヤーのみ」なら、呼び出し側が 

312 すでに持っている画素をそのまま返す(余分な変換をしない)。 

313 

314 画素は返した QImage のバッファを指すので、使い終わるまで QImage を 

315 手放さないように両方を返す。 

316 """ 

317 image = self._bucket_reference_image( 

318 options, frame, layer_index, layer_image 

319 ) 

320 if image is layer_image: 

321 return layer_image, layer_pixels 

322 drag = self._bucket_drag 

323 if drag is not None: 

324 key = image.cacheKey() 

325 cached = drag["reference_pixels"].get(key) 

326 if cached is None: 

327 cached = self._layer_fill_pixels(image) 

328 drag["reference_pixels"].clear() 

329 drag["reference_pixels"][key] = cached 

330 return cached 

331 return self._layer_fill_pixels(image) 

332 

333 def _bucket_reference_image(self, options, frame, layer_index, fallback): 

334 """参照レイヤーの設定に合う絵を返す。無ければ ``fallback`` のまま。""" 

335 reference = getattr( 

336 options, "reference", bucket_fill.REFERENCE_LAYER 

337 ) 

338 if reference == bucket_fill.REFERENCE_CANVAS: 

339 return self._bucket_canvas_reference(frame, fallback) 

340 if reference == bucket_fill.REFERENCE_INITIAL: 

341 key_frame = self.resolve_key_frame(frame, layer_index) 

342 if key_frame is None: 342 ↛ 343line 342 didn't jump to line 343 because the condition on line 342 was never true

343 return fallback 

344 initial = self.frames[key_frame].layers[layer_index].initial_image 

345 # 初期状態を保持していないセルは、今の絵で判定するしかない。 

346 if initial is None or initial.size() != fallback.size(): 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true

347 return fallback 

348 return initial 

349 return fallback 

350 

351 def _bucket_canvas_reference(self, frame, fallback): 

352 """表示しているレイヤーを重ねた絵。別のレイヤーの線でも区切れるように。 

353 

354 用紙レイヤーは外す。キャンバス一面の不透明な色で覆われると、透明を 

355 たどる対象色が働かなくなるため。表示プレビューを通さない生の絵を重ねる。 

356 """ 

357 if not (0 <= frame < len(self.frames)): 357 ↛ 358line 357 didn't jump to line 358 because the condition on line 357 was never true

358 return fallback 

359 result = QImage( 

360 fallback.size(), QImage.Format.Format_ARGB32_Premultiplied 

361 ) 

362 result.fill(Qt.GlobalColor.transparent) 

363 painter = QPainter(result) 

364 for layer_index in range(len(self.frames[frame].layers)): 

365 key_frame = self.resolve_key_frame(frame, layer_index) 

366 if key_frame is None: 366 ↛ 367line 366 didn't jump to line 367 because the condition on line 366 was never true

367 continue 

368 layer = self.frames[key_frame].layers[layer_index] 

369 if not layer.visible or layer.is_paper: 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true

370 continue 

371 if layer.image is None or layer.image.isNull(): 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true

372 continue 

373 painter.setOpacity(layer.opacity) 

374 painter.drawImage(0, 0, layer.image) 

375 painter.end() 

376 return result 

377 

378 def _fill_patch(self, patch, patch_region, replacement): 

379 """``patch`` の ``patch_region`` を塗る。書き込んだ RGB の組を返す。 

380 

381 白は消しゴムとして alpha を抜き、空の組を返す(使用色に登録しない)。 

382 """ 

383 source_rgb = np.asarray( 

384 [replacement.red(), replacement.green(), replacement.blue()], 

385 dtype=np.uint8, 

386 ) 

387 if bool(np.all(source_rgb == 255)): 

388 # 白バケツは本物の消しゴム:該当領域を alpha=0 へ抜く。 

389 patch[patch_region] = 0 

390 return () 

391 

392 opacity = max(0.0, min(1.0, float(getattr(self, "fill_opacity", 1.0)))) 

393 if opacity >= 0.999999: 

394 # 100%は正規RGBをそのまま書き込む(近似色を生成しない)。 

395 patch[patch_region, :3] = source_rgb 

396 written_colors = ( 

397 (int(source_rgb[0]), int(source_rgb[1]), int(source_rgb[2])), 

398 ) 

399 else: 

400 # 100%未満だけ下地RGB(透明は白とみなす)と混色する。 

401 base_rgb = patch[patch_region, :3].astype(np.float32) 

402 base_rgb[patch[patch_region, 3] == 0] = 255.0 

403 mixed = np.clip( 

404 np.rint( 

405 base_rgb * (1.0 - opacity) 

406 + source_rgb.astype(np.float32)[None, :] * opacity 

407 ), 

408 0, 

409 255, 

410 ).astype(np.uint8) 

411 # 混色の結果が白になった画素を消しゴム扱いにしない。 

412 mixed[np.all(mixed == 255, axis=1)] = 254 

413 patch[patch_region, :3] = mixed 

414 written_colors = tuple( 

415 tuple(int(channel) for channel in row) 

416 for row in np.unique(mixed, axis=0) 

417 ) 

418 patch[patch_region, 3] = 255 

419 return written_colors 

420 

421 def flood_fill(self, p): 

422 if self._bucket_all_frames_enabled(): 

423 self.flood_fill_over_frames(p) 

424 return 

425 # 串刺し塗りの指定待ち中にチェックを外したら、途中の串刺し塗りは捨てる。 

426 self.abort_skewer_fill() 

427 self._bucket_fill_at(p, push_undo=True) 

428 

429 def _bucket_fill_at(self, p, push_undo=True, quiet=False, accept=None, line_cache=None): 

430 """``p`` を種に1か所塗る。``(塗ったマスク, 塗らずに済ませた領域)`` を返す。 

431 

432 塗れなければ塗ったマスクは None。``push_undo`` が偽なら Undo を積まない 

433 (ドラッグ塗りが最後にまとめて積む)。``accept`` を渡すと、塗る前に 

434 ``accept(結果)`` で塗ってよいかを決め、偽ならその領域を2つ目に返す。 

435 """ 

436 drag = self._bucket_drag 

437 active = self.active_layer.image 

438 if (drag is not None and drag["frame"] == self.current_frame 

439 and drag["layer"] == self.active_layer_index 

440 and drag["pixels_key"] == active.cacheKey()): 

441 image, pixels = drag["pixels"] 

442 else: 

443 image, pixels = self._layer_fill_pixels(active) 

444 start_x, start_y = int(p.x()), int(p.y()) 

445 selection_mask = self.selection_mask_bool(image.width(), image.height()) 

446 options = self.bucket_options() 

447 # 判定は参照レイヤーの絵で行い、書き込みはアクティブレイヤーへ。 

448 _reference_image, reference_pixels = self.bucket_reference_pixels( 

449 options, self.current_frame, self.active_layer_index, image, pixels 

450 ) 

451 result = bucket_fill.compute_fill_region( 

452 reference_pixels, start_x, start_y, options, selection_mask, line_cache=line_cache, 

453 ) 

454 if not result.ok: 454 ↛ 455line 454 didn't jump to line 455 because the condition on line 454 was never true

455 message = None if quiet else self._bucket_failure_message(result.reason) 

456 if message: 

457 self.status_message.emit(message) 

458 return None, result.region 

459 if accept is not None and not accept(result): 

460 return None, result.mask 

461 

462 # Undo は塗る範囲が決まってから、その外接矩形だけを保存する。 

463 # 大きなキャンバスでレイヤー全体をコピーしないため。 

464 assert result.bounds is not None # ok なら必ず外接矩形がある 

465 x0, y0, x1, y1 = result.bounds 

466 fill_rect = QRect(x0, y0, x1 - x0, y1 - y0) 

467 if push_undo and not self.push_layer_region_undo(fill_rect): 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true

468 return None, None 

469 # 以降の書き込みは外接矩形の中だけで行い、その部分だけをレイヤーへ戻す。 

470 patch = pixels[y0:y1, x0:x1] 

471 written_colors = set(self._fill_patch( 

472 patch, result.mask[y0:y1, x0:x1], self.paint_source_color() 

473 )) 

474 written_colors.update(self._fill_islands(patch, result.islands, result.bounds)) 

475 written_colors = tuple(sorted(written_colors)) 

476 self._write_rgba_patch(patch, fill_rect) 

477 if drag is not None: 

478 drag["pixels"] = (image, pixels) 

479 drag["pixels_key"] = self.active_layer.image.cacheKey() 

480 self.active_layer.has_content = True 

481 if written_colors: 

482 self._emit_actual_paint_colors(written_colors) 

483 self.cellChanged.emit(self.current_frame, self.active_layer_index) 

484 self.update() 

485 return result.mask, None 

486 

487 # ------------------------------------------------------------------ 

488 # ドラッグ塗り:なぞった軌跡が通った囲まれた部分を、それぞれ塗る。 

489 # ------------------------------------------------------------------ 

490 def begin_bucket_drag(self, p): 

491 """バケツを押した。最初の1か所を塗り、なぞりの準備をする。""" 

492 self.finish_bucket_drag() 

493 layer = self.active_layer 

494 # 画像は暗黙共有なので、ここでは複製しない(塗った時点で分かれる)。 

495 before = QImage(layer.image) 

496 converted, layer_pixels = self._layer_fill_pixels(before) 

497 width, height = before.width(), before.height() 

498 x, y = int(p.x()), int(p.y()) 

499 if not (0 <= x < width and 0 <= y < height): 499 ↛ 500line 499 didn't jump to line 500 because the condition on line 499 was never true

500 self._bucket_drag = None 

501 return 

502 options = self.bucket_options() 

503 # なぞる先の判定も、1か所塗るときと同じ絵を見る。 

504 _reference_image, original = self.bucket_reference_pixels( 

505 options, self.current_frame, self.active_layer_index, 

506 converted, layer_pixels, 

507 ) 

508 category = bucket_fill.seed_category(original, x, y) 

509 # なぞった先で塗り始めてよい画素。線の上を通っても線を塗らないよう、 

510 # 「すべての色」では最初にクリックした色の種類、それ以外は対象色の画素に限る。 

511 eligible = bucket_fill.fill_target_mask( 

512 original, options.target_color, category 

513 ) 

514 self._bucket_drag = { 

515 "frame": int(self.current_frame), 

516 "layer": int(self.active_layer_index), 

517 "before": before, 

518 "has_content": bool(layer.has_content), 

519 "eligible": eligible, 

520 # 試した場所(塗れなかった点も含む)と、実際に塗った範囲。 

521 "tried": np.zeros((height, width), dtype=bool), 

522 "painted": np.zeros((height, width), dtype=bool), 

523 "last": QPointF(p), 

524 "line_cache": {}, 

525 "pixels": (converted, layer_pixels), 

526 "pixels_key": layer.image.cacheKey(), 

527 "reference_pixels": {}, 

528 } 

529 # 最初の1か所はクリックと同じ(開いた領域も設定どおりに塗る)。 

530 self._bucket_drag_fill(p, first=True) 

531 

532 def continue_bucket_drag(self, p): 

533 """なぞった区間上の、まだ塗っていない囲まれた部分を塗る。""" 

534 drag = self._bucket_drag 

535 if drag is None: 535 ↛ 536line 535 didn't jump to line 536 because the condition on line 535 was never true

536 return 

537 start = drag["last"] 

538 distance = math.hypot(p.x() - start.x(), p.y() - start.y()) 

539 steps = max(1, int(math.ceil(distance))) 

540 for step in range(1, steps + 1): 

541 t = step / steps 

542 self._bucket_drag_fill( 

543 QPointF( 

544 start.x() + (p.x() - start.x()) * t, 

545 start.y() + (p.y() - start.y()) * t, 

546 ), 

547 first=False, 

548 ) 

549 drag["last"] = QPointF(p) 

550 

551 def _bucket_drag_fill(self, p, first): 

552 drag = self._bucket_drag 

553 x, y = int(p.x()), int(p.y()) 

554 tried = drag["tried"] 

555 height, width = tried.shape 

556 if not (0 <= x < width and 0 <= y < height): 556 ↛ 557line 556 didn't jump to line 557 because the condition on line 556 was never true

557 return 

558 if tried[y, x] or not drag["eligible"][y, x]: 

559 return 

560 mask, skipped = self._bucket_fill_at( 

561 QPointF(x, y), push_undo=False, quiet=not first, 

562 accept=None if first else self._bucket_drag_accepts, 

563 line_cache=drag["line_cache"], 

564 ) 

565 # 塗れなかった点・塗らなかった領域も、同じ場所で何度も判定しない。 

566 tried[y, x] = True 

567 if skipped is not None: 

568 tried |= skipped 

569 if mask is not None: 

570 tried |= mask 

571 drag["painted"] |= mask 

572 if first: 

573 drag["first_area"] = int(np.count_nonzero(mask)) 

574 

575 def _bucket_drag_accepts(self, result): 

576 """なぞった先の領域を塗るか。外の背景だけは塗らない。 

577 

578 余白の無いキャンバスでは、フレームの端で閉じた部分(端まで線を引いた所)も 

579 外の背景も「端に接した領域」になる。外の背景はふつう画面の大半を占めるので、 

580 端に接していて大きすぎる領域だけを外の背景とみなして塗らない。 

581 """ 

582 region = result.mask 

583 touches_edge = bool( 

584 region[0].any() or region[-1].any() 

585 or region[:, 0].any() or region[:, -1].any() 

586 ) 

587 if not touches_edge: 

588 return True 

589 area = int(np.count_nonzero(region)) 

590 first_area = int(self._bucket_drag.get("first_area", 0)) if self._bucket_drag else 0 

591 limit = max(3 * first_area, int(region.size * BUCKET_DRAG_EDGE_AREA)) 

592 return area <= limit 

593 

594 def finish_bucket_drag(self): 

595 """ドラッグで塗った分を、Undo 1回で戻せるようにまとめて積む。""" 

596 drag = self._bucket_drag 

597 self._bucket_drag = None 

598 if drag is None: 

599 return 

600 if not self._bucket_drag_target_exists(drag): 600 ↛ 601line 600 didn't jump to line 601 because the condition on line 600 was never true

601 return 

602 ys, xs = np.nonzero(drag["painted"]) 

603 if not xs.size: 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true

604 return 

605 rect = QRect( 

606 int(xs.min()), int(ys.min()), 

607 int(xs.max() - xs.min() + 1), int(ys.max() - ys.min() + 1), 

608 ) 

609 self.push_undo(LayerRegionUndo( 

610 drag["frame"], drag["layer"], rect, 

611 drag["before"].copy(rect), drag["has_content"], 

612 )) 

613 

614 def _bucket_drag_target_exists(self, drag): 

615 return ( 

616 0 <= drag["frame"] < len(self.frames) 

617 and 0 <= drag["layer"] < len(self.frames[drag["frame"]].layers) 

618 ) 

619 

620 @staticmethod 

621 def _bucket_failure_message(reason): 

622 """塗れなかった理由のうち、ユーザーに伝える価値があるものだけ文言にする。""" 

623 if reason == bucket_fill.REASON_OUTSIDE_SELECTION: 623 ↛ 624line 623 didn't jump to line 624 because the condition on line 623 was never true

624 return tr("選択範囲の外側なので塗りを開始しませんでした。") 

625 if reason == bucket_fill.REASON_OPEN_REGION: 625 ↛ 627line 625 didn't jump to line 627 because the condition on line 625 was always true

626 return tr("領域がキャンバス端まで開いているため、塗りを開始しませんでした。") 

627 if reason == bucket_fill.REASON_NOT_TARGET: 

628 return tr("クリックした場所は「対象色」で塗る対象にしていない色です。") 

629 return None 

630 

631 def _bucket_all_frames_enabled(self): 

632 tools = self._bucket_tools() 

633 checkbox = getattr(tools, "bucket_all_frames", None) 

634 return bool(checkbox is not None and checkbox.isChecked()) 

635 

636 def _seed_in_bounds(self, start_x, start_y, image=None): 

637 image = image if image is not None else self.active_layer.image 

638 return 0 <= start_x < image.width() and 0 <= start_y < image.height() 

639 

640 def _write_rgba_patch(self, patch, rect): 

641 """RGBA8888 の部分配列を、アクティブレイヤーの rect へそのまま書き戻す。""" 

642 patch_image = imaging.rgba_array_to_qimage(patch) 

643 painter = QPainter(self.active_layer.image) 

644 painter.setCompositionMode( 

645 QPainter.CompositionMode.CompositionMode_Source 

646 ) 

647 painter.drawImage(rect.topLeft(), patch_image) 

648 painter.end() 

649 

650 def auto_select_region(self, point, modifiers=Qt.KeyboardModifier.NoModifier): 

651 """バケツと同じ連続領域判定で選択マスクを作成・加減算する。""" 

652 image = self.active_layer.image.convertToFormat( 

653 QImage.Format.Format_RGBA8888 

654 ) 

655 width, height = image.width(), image.height() 

656 start_x, start_y = int(point.x()), int(point.y()) 

657 if not (0 <= start_x < width and 0 <= start_y < height): 657 ↛ 658line 657 didn't jump to line 658 because the condition on line 657 was never true

658 return False 

659 

660 ptr = imaging.qimage_buffer(image) 

661 rows = np.frombuffer(ptr, dtype=np.uint8).reshape( 

662 (height, image.bytesPerLine()) 

663 ) 

664 pixels = rows[:, :width * 4].reshape((height, width, 4)) 

665 target = pixels[start_y, start_x] 

666 pseudo_background = ( 

667 (pixels[:, :, 3] == 0) 

668 | np.all(pixels[:, :, :3] == 255, axis=2) 

669 ) 

670 target_is_background = bool( 

671 int(target[3]) == 0 

672 or np.all(target[:3] == 255) 

673 ) 

674 target_mask = ( 

675 pseudo_background.copy() 

676 if target_is_background 

677 else ( 

678 (pixels[:, :, 3] > 0) 

679 & np.all(pixels[:, :, :3] == target[:3], axis=2) 

680 ) 

681 ) 

682 

683 window = self.window() 

684 tools = getattr(window, "tools", None) 

685 adjacent = bool( 

686 tools is None or tools.bucket_adjacent.isChecked() 

687 ) 

688 close_gap = bool( 

689 tools is not None 

690 and tools.bucket_close_gap.isChecked() 

691 and adjacent 

692 ) 

693 

694 if adjacent: 694 ↛ 749line 694 didn't jump to line 749 because the condition on line 694 was always true

695 passable = target_mask 

696 if close_gap and tools is not None: 696 ↛ 697line 696 didn't jump to line 697 because the condition on line 696 was never true

697 radius = max(1, int(tools.bucket_gap_width.value())) 

698 boundary = ~target_mask 

699 expanded = boundary.copy() 

700 for _ in range(radius): 

701 source = expanded.copy() 

702 expanded[1:, :] |= source[:-1, :] 

703 expanded[:-1, :] |= source[1:, :] 

704 expanded[:, 1:] |= source[:, :-1] 

705 expanded[:, :-1] |= source[:, 1:] 

706 expanded[1:, 1:] |= source[:-1, :-1] 

707 expanded[:-1, :-1] |= source[1:, 1:] 

708 expanded[1:, :-1] |= source[:-1, 1:] 

709 expanded[:-1, 1:] |= source[1:, :-1] 

710 virtual_boundary = expanded 

711 for _ in range(radius): 

712 padded = np.pad( 

713 virtual_boundary, 

714 ((1, 1), (1, 1)), 

715 mode="constant", 

716 constant_values=False, 

717 ) 

718 virtual_boundary = ( 

719 padded[0:-2, 0:-2] 

720 & padded[0:-2, 1:-1] 

721 & padded[0:-2, 2:] 

722 & padded[1:-1, 0:-2] 

723 & padded[1:-1, 1:-1] 

724 & padded[1:-1, 2:] 

725 & padded[2:, 0:-2] 

726 & padded[2:, 1:-1] 

727 & padded[2:, 2:] 

728 ) 

729 passable = target_mask & ~virtual_boundary 

730 passable[start_y, start_x] = bool( 

731 target_mask[start_y, start_x] 

732 ) 

733 region = self._scanline_connected_region( 

734 passable, (start_x, start_y) 

735 ) 

736 require_closed = bool( 

737 tools is not None 

738 and tools.bucket_require_closed.isChecked() 

739 ) 

740 if require_closed and ( 740 ↛ 744line 740 didn't jump to line 744 because the condition on line 740 was never true

741 np.any(region[0, :]) or np.any(region[-1, :]) 

742 or np.any(region[:, 0]) or np.any(region[:, -1]) 

743 ): 

744 self.status_message.emit( 

745 tr("領域がキャンバス端まで開いているため選択しませんでした。") 

746 ) 

747 return False 

748 else: 

749 region = target_mask.copy() 

750 

751 current = self.selection_mask_bool(width, height) 

752 if current is None: 752 ↛ 754line 752 didn't jump to line 754 because the condition on line 752 was always true

753 current = np.zeros((height, width), dtype=bool) 

754 if modifiers & Qt.KeyboardModifier.AltModifier: 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true

755 combined = current & ~region 

756 elif modifiers & Qt.KeyboardModifier.ShiftModifier: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true

757 combined = current | region 

758 else: 

759 combined = region 

760 

761 return self.set_selection_mask(combined) 

762 

763 

764 def _line_preview_path(self): 

765 if self.line_start is None or self.line_end is None: 765 ↛ 766line 765 didn't jump to line 766 because the condition on line 765 was never true

766 return None 

767 path = QPainterPath(QPointF(self.line_start)) 

768 if self.line_curve_stage == 2 and self.line_control is not None: 

769 path.quadTo(QPointF(self.line_control), QPointF(self.line_end)) 

770 else: 

771 path.lineTo(QPointF(self.line_end)) 

772 return path 

773 

774 def _draw_tapered_path( 

775 self, 

776 painter, 

777 path, 

778 color, 

779 base_width, 

780 start_width=None, 

781 end_width=None, 

782 start_curve=1.0, 

783 end_curve=1.0, 

784 ): 

785 """入り抜き幅を変化させながら、非AA線分として描画する。""" 

786 try: 

787 length = max(1.0, float(path.length())) 

788 except (AttributeError, RuntimeError, TypeError, ValueError) as exc: 

789 log.debug("path.length() failed, using endpoint distance: %s", exc) 

790 length = max( 

791 1.0, 

792 math.hypot( 

793 self.line_end.x() - self.line_start.x(), 

794 self.line_end.y() - self.line_start.y(), 

795 ), 

796 ) 

797 steps = max(2, min(4096, int(math.ceil(length * 1.5)))) 

798 previous = path.pointAtPercent(0.0) 

799 for index in range(1, steps + 1): 

800 ratio = index / steps 

801 point = path.pointAtPercent(ratio) 

802 width = float(base_width) 

803 if start_width is not None and ratio <= 0.5: 

804 local = max(0.0, min(1.0, ratio * 2.0)) 

805 eased = local ** max(0.05, float(start_curve)) 

806 width = float(start_width) + ( 

807 float(base_width) - float(start_width) 

808 ) * eased 

809 elif end_width is not None and ratio >= 0.5: 

810 local = max(0.0, min(1.0, (ratio - 0.5) * 2.0)) 

811 eased = local ** max(0.05, float(end_curve)) 

812 width = float(base_width) + ( 

813 float(end_width) - float(base_width) 

814 ) * eased 

815 width = max(0.5, width) 

816 pen = QPen( 

817 color, 

818 width, 

819 Qt.PenStyle.SolidLine, 

820 Qt.PenCapStyle.RoundCap, 

821 Qt.PenJoinStyle.RoundJoin, 

822 ) 

823 painter.setPen(pen) 

824 painter.drawLine(previous, point) 

825 previous = point 

826 

827 def _begin_brush_straight_line(self, point): 

828 anchor = getattr(self, "_brush_line_anchor", None) 

829 key = (self.current_frame, self.active_layer_index) 

830 self._brush_shift_line = True 

831 self.line_start = QPointF(anchor[1] if anchor is not None and anchor[0] == key else point) 

832 self.line_end = QPointF(point) 

833 self.line_control = None 

834 self.line_curve_stage = 1 

835 self.drawing = True 

836 self.update() 

837 

838 def commit_line_tool(self): 

839 path = self._line_preview_path() 

840 if path is None: 840 ↛ 841line 840 didn't jump to line 841 because the condition on line 840 was never true

841 return 

842 self.ensure_editable_key() 

843 panel = self._active_tool_panel() 

844 base_width = max(0.5, float(self.pen_size)) 

845 start_width = None 

846 end_width = None 

847 start_curve = 1.0 

848 end_curve = 1.0 

849 if panel is not None and not getattr(self, "_brush_shift_line", False): 

850 if panel.line_taper_in.isChecked(): 850 ↛ 851line 850 didn't jump to line 851 because the condition on line 850 was never true

851 start_width = ( 

852 panel.line_taper_in_size.value() / 2.0 

853 ) 

854 start_curve = ( 

855 panel.line_taper_in_curve.value() / 100.0 

856 ) 

857 if panel.line_taper_out.isChecked(): 857 ↛ 858line 857 didn't jump to line 858 because the condition on line 857 was never true

858 end_width = ( 

859 panel.line_taper_out_size.value() / 2.0 

860 ) 

861 end_curve = ( 

862 1.0 

863 / max( 

864 0.05, 

865 panel.line_taper_out_curve.value() / 100.0, 

866 ) 

867 ) 

868 

869 source_color = self.paint_source_color() 

870 maximum_width = max( 

871 base_width, 

872 float(start_width or 0.0), 

873 float(end_width or 0.0), 

874 ) 

875 undo_margin = int(math.ceil(maximum_width / 2.0)) + 3 

876 undo_rect = path.boundingRect().adjusted( 

877 -undo_margin, -undo_margin, undo_margin, undo_margin 

878 ).toAlignedRect() 

879 if not self.push_layer_region_undo(undo_rect): 879 ↛ 880line 879 didn't jump to line 880 because the condition on line 879 was never true

880 return 

881 overlay = QImage( 

882 self.active_layer.image.width(), 

883 self.active_layer.image.height(), 

884 QImage.Format.Format_ARGB32_Premultiplied, 

885 ) 

886 overlay.fill(Qt.GlobalColor.transparent) 

887 

888 painter = QPainter(overlay) 

889 painter.setRenderHint( 

890 QPainter.RenderHint.Antialiasing, 

891 False, 

892 ) 

893 painter.setRenderHint( 

894 QPainter.RenderHint.TextAntialiasing, 

895 False, 

896 ) 

897 painter.setRenderHint( 

898 QPainter.RenderHint.SmoothPixmapTransform, 

899 False, 

900 ) 

901 self.apply_selection_clip(painter) 

902 if start_width is None and end_width is None: 902 ↛ 914line 902 didn't jump to line 914 because the condition on line 902 was always true

903 painter.setPen( 

904 QPen( 

905 source_color, 

906 base_width, 

907 Qt.PenStyle.SolidLine, 

908 Qt.PenCapStyle.RoundCap, 

909 Qt.PenJoinStyle.RoundJoin, 

910 ) 

911 ) 

912 painter.drawPath(path) 

913 else: 

914 self._draw_tapered_path( 

915 painter, 

916 path, 

917 source_color, 

918 base_width, 

919 start_width, 

920 end_width, 

921 start_curve, 

922 end_curve, 

923 ) 

924 painter.end() 

925 

926 colors = self._blend_overlay_into_active_layer( 

927 overlay, 

928 QPoint(0, 0), 

929 exact_colors=(source_color,), 

930 ) 

931 self._emit_actual_paint_colors(colors) 

932 

933 self.active_layer.has_content = True 

934 self.cellChanged.emit( 

935 self.current_frame, 

936 self.active_layer_index, 

937 ) 

938 if getattr(self, "_brush_shift_line", False): 

939 self._brush_line_anchor = ( 

940 (self.current_frame, self.active_layer_index), QPointF(self.line_end), 

941 ) 

942 self._brush_shift_line = False 

943 self.line_start = None 

944 self.line_end = None 

945 self.line_control = None 

946 self.line_curve_stage = 0 

947 self.drawing = False 

948 self.update() 

949 

950 def _shape_rect(self): 

951 if self.shape_start is None or self.shape_end is None: 

952 return QRectF() 

953 start = QPointF(self.shape_start) 

954 end = QPointF(self.shape_end) 

955 panel = self._active_tool_panel() 

956 if panel is not None and panel.shape_lock_ratio.isChecked(): 

957 dx = end.x() - start.x() 

958 dy = end.y() - start.y() 

959 size = max(abs(dx), abs(dy)) 

960 end = QPointF( 

961 start.x() + (size if dx >= 0 else -size), 

962 start.y() + (size if dy >= 0 else -size), 

963 ) 

964 return QRectF(start, end).normalized() 

965 

966 def _shape_path(self): 

967 rect = self._shape_rect() 

968 if rect.isEmpty(): 

969 return None 

970 panel = self._active_tool_panel() 

971 shape_type = panel.shape_type.currentData() if panel is not None else "polygon" 

972 path = QPainterPath() 

973 if shape_type == "ellipse": 

974 path.addEllipse(rect) 

975 return path 

976 

977 corners = max(3, int(panel.shape_corners.value()) if panel is not None else 4) 

978 center = rect.center() 

979 radius_x = rect.width() / 2.0 

980 radius_y = rect.height() / 2.0 

981 if corners == 4: 

982 # 4角はひし形ではなく、ドラッグ範囲に沿う□(長方形)にする。 

983 points = [ 

984 QPointF(rect.left(), rect.top()), 

985 QPointF(rect.right(), rect.top()), 

986 QPointF(rect.right(), rect.bottom()), 

987 QPointF(rect.left(), rect.bottom()), 

988 ] 

989 else: 

990 points = [] 

991 for index in range(corners): 

992 angle = -math.pi / 2.0 + (2.0 * math.pi * index / corners) 

993 points.append( 

994 QPointF( 

995 center.x() + math.cos(angle) * radius_x, 

996 center.y() + math.sin(angle) * radius_y, 

997 ) 

998 ) 

999 if points: 

1000 path.moveTo(points[0]) 

1001 for point in points[1:]: 

1002 path.lineTo(point) 

1003 path.closeSubpath() 

1004 return path 

1005 

1006 def commit_shape_tool(self): 

1007 path = self._shape_path() 

1008 if path is None: 

1009 self.shape_start = None 

1010 self.shape_end = None 

1011 self.drawing = False 

1012 self.update() 

1013 return 

1014 

1015 self.ensure_editable_key() 

1016 panel = self._active_tool_panel() 

1017 use_split_colors = bool( 

1018 panel is not None 

1019 and panel.shape_sub_outline_main_fill.isChecked() 

1020 ) 

1021 fill_inside = bool( 

1022 panel is not None 

1023 and panel.shape_fill_inside.isChecked() 

1024 ) 

1025 outline_width = ( 

1026 panel.shape_outline_width.value() / 2.0 

1027 if panel is not None 

1028 else 1.0 

1029 ) 

1030 undo_margin = int(math.ceil(float(outline_width) / 2.0)) + 3 

1031 undo_rect = path.boundingRect().adjusted( 

1032 -undo_margin, -undo_margin, undo_margin, undo_margin 

1033 ).toAlignedRect() 

1034 if not self.push_layer_region_undo(undo_rect): 

1035 return 

1036 

1037 if use_split_colors: 

1038 outline_color = self.paint_source_color( 

1039 self.sub_color 

1040 ) 

1041 fill_color = self.paint_source_color( 

1042 self.main_color 

1043 ) 

1044 else: 

1045 outline_color = self.paint_source_color() 

1046 fill_color = QColor(outline_color) 

1047 

1048 overlay = QImage( 

1049 self.active_layer.image.width(), 

1050 self.active_layer.image.height(), 

1051 QImage.Format.Format_ARGB32_Premultiplied, 

1052 ) 

1053 overlay.fill(Qt.GlobalColor.transparent) 

1054 

1055 painter = QPainter(overlay) 

1056 painter.setRenderHint( 

1057 QPainter.RenderHint.Antialiasing, 

1058 False, 

1059 ) 

1060 painter.setRenderHint( 

1061 QPainter.RenderHint.TextAntialiasing, 

1062 False, 

1063 ) 

1064 painter.setRenderHint( 

1065 QPainter.RenderHint.SmoothPixmapTransform, 

1066 False, 

1067 ) 

1068 self.apply_selection_clip(painter) 

1069 if fill_inside: 

1070 painter.setPen(Qt.PenStyle.NoPen) 

1071 painter.setBrush(fill_color) 

1072 painter.drawPath(path) 

1073 painter.setBrush(Qt.BrushStyle.NoBrush) 

1074 painter.setPen( 

1075 QPen( 

1076 outline_color, 

1077 max(0.5, float(outline_width)), 

1078 Qt.PenStyle.SolidLine, 

1079 Qt.PenCapStyle.RoundCap, 

1080 Qt.PenJoinStyle.RoundJoin, 

1081 ) 

1082 ) 

1083 painter.drawPath(path) 

1084 painter.end() 

1085 

1086 exact_shape_colors = ( 

1087 (outline_color, fill_color) 

1088 if fill_inside 

1089 else (outline_color,) 

1090 ) 

1091 colors = self._blend_overlay_into_active_layer( 

1092 overlay, 

1093 QPoint(0, 0), 

1094 exact_colors=exact_shape_colors, 

1095 ) 

1096 self._emit_actual_paint_colors(colors) 

1097 

1098 self.active_layer.has_content = True 

1099 self.cellChanged.emit( 

1100 self.current_frame, 

1101 self.active_layer_index, 

1102 ) 

1103 self.shape_start = None 

1104 self.shape_end = None 

1105 self.drawing = False 

1106 self.update() 

1107 

1108 def fill_lasso_polygon(self, points): 

1109 if len(points) < 3: 1109 ↛ 1110line 1109 didn't jump to line 1110 because the condition on line 1109 was never true

1110 return 

1111 

1112 self.ensure_editable_key() 

1113 window: Any = self.window() 

1114 outline_and_fill = bool( 

1115 hasattr(window, "tools") 

1116 and window.tools.lasso_main_outline_sub_fill.isChecked() 

1117 ) 

1118 mask_only = not getattr(self, "mask_all_enabled", True) 

1119 inside_boundary = bool( 

1120 hasattr(window, "tools") 

1121 and window.tools.lasso_inside_boundary.isChecked() 

1122 ) 

1123 outline_width = ( 

1124 window.tools.lasso_outline_width.value() / 2.0 

1125 if outline_and_fill and hasattr(window, "tools") 

1126 else 0.0 

1127 ) 

1128 

1129 polygon = QPolygonF(points) 

1130 margin = int(math.ceil(outline_width / 2.0)) + 2 

1131 rect = polygon.boundingRect().adjusted( 

1132 -margin, -margin, margin, margin 

1133 ).toAlignedRect().intersected(self.active_layer.image.rect()) 

1134 if rect.isEmpty(): 1134 ↛ 1135line 1134 didn't jump to line 1135 because the condition on line 1134 was never true

1135 return 

1136 local_polygon = QPolygonF([ 

1137 QPointF(point.x() - rect.x(), point.y() - rect.y()) 

1138 for point in points 

1139 ]) 

1140 overlay = QImage( 

1141 rect.width(), 

1142 rect.height(), 

1143 QImage.Format.Format_ARGB32_Premultiplied, 

1144 ) 

1145 overlay.fill(Qt.GlobalColor.transparent) 

1146 

1147 fill_color = ( 

1148 self.paint_source_color(self.main_color) 

1149 if outline_and_fill 

1150 else self.paint_source_color() 

1151 ) 

1152 

1153 painter = QPainter(overlay) 

1154 painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) 

1155 painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False) 

1156 self.apply_selection_clip(painter, rect.x(), rect.y()) 

1157 painter.setPen(Qt.PenStyle.NoPen) 

1158 painter.setBrush(fill_color) 

1159 painter.drawPolygon(local_polygon) 

1160 

1161 line_color = None 

1162 if outline_and_fill: 1162 ↛ 1163line 1162 didn't jump to line 1163 because the condition on line 1162 was never true

1163 line_color = self.paint_source_color(self.sub_color) 

1164 line_pen = QPen(line_color) 

1165 line_pen.setWidthF(max(0.5, float(outline_width))) 

1166 line_pen.setStyle(Qt.PenStyle.SolidLine) 

1167 line_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) 

1168 painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) 

1169 painter.setPen(line_pen) 

1170 painter.setBrush(Qt.BrushStyle.NoBrush) 

1171 painter.drawPolygon(local_polygon) 

1172 painter.end() 

1173 

1174 width, height = rect.width(), rect.height() 

1175 mask_image = overlay.convertToFormat( 

1176 QImage.Format.Format_RGBA8888 

1177 ) 

1178 

1179 mask_ptr = imaging.qimage_buffer(mask_image) 

1180 mask_rows = np.frombuffer( 

1181 mask_ptr, dtype=np.uint8 

1182 ).reshape((height, mask_image.bytesPerLine())) 

1183 mask_pixels = mask_rows[:, :width * 4].reshape( 

1184 (height, width, 4) 

1185 ) 

1186 

1187 options = bucket_fill.BucketOptions.from_tools( 

1188 window.tools if hasattr(window, "tools") else None, 

1189 ) 

1190 reference = self._bucket_reference_image( 

1191 options, self.current_frame, self.active_layer_index, self.active_layer.image, 

1192 ) 

1193 reference_owner, reference_pixels = self._layer_fill_pixels(reference.copy(rect)) 

1194 if options.target_color != bucket_fill.TARGET_ALL: 

1195 if options.target_color in ( 

1196 bucket_fill.TARGET_OPAQUE_AND_INNER_TRANSPARENT, 

1197 bucket_fill.TARGET_TRANSPARENT_AND_CLOSED_OPAQUE, 

1198 ): 

1199 # 内部の穴は投げ縄の外側まで含む元の参照画像で判断する。 

1200 full_owner, full_pixels = self._layer_fill_pixels(reference) 

1201 eligible = bucket_fill.fill_target_mask(full_pixels, options.target_color, None) 

1202 eligible |= bucket_fill._enclosed_by(eligible) 

1203 eligible = eligible[rect.y():rect.y() + height, rect.x():rect.x() + width] 

1204 else: 

1205 eligible = bucket_fill.fill_target_mask(reference_pixels, options.target_color, None) 

1206 mask_pixels[:, :, 3][~eligible] = 0 

1207 

1208 if mask_only: 1208 ↛ 1209line 1208 didn't jump to line 1209 because the condition on line 1208 was never true

1209 selected_rgbs = self.selected_mask_colors() 

1210 if not selected_rgbs: 

1211 target_mask = np.zeros((height, width), dtype=bool) 

1212 else: 

1213 packed_source = ( 

1214 (reference_pixels[:, :, 0].astype(np.uint32) << 16) 

1215 | (reference_pixels[:, :, 1].astype(np.uint32) << 8) 

1216 | reference_pixels[:, :, 2].astype(np.uint32) 

1217 ) 

1218 selected_values = np.fromiter( 

1219 ((r << 16) | (g << 8) | b for r, g, b in selected_rgbs), 

1220 dtype=np.uint32, 

1221 count=len(selected_rgbs), 

1222 ) 

1223 target_mask = ( 

1224 (reference_pixels[:, :, 3] > 0) 

1225 & np.isin(packed_source, selected_values) 

1226 ) 

1227 if self.background_mask_rgb in selected_rgbs: 

1228 target_mask |= ( 

1229 (reference_pixels[:, :, 3] == 0) 

1230 | np.all( 

1231 reference_pixels[:, :, :3] == 255, 

1232 axis=2, 

1233 ) 

1234 ) 

1235 mask_pixels[:, :, 3][~target_mask] = 0 

1236 

1237 if inside_boundary: 

1238 polygon_area = mask_pixels[:, :, 3] > 0 

1239 passable = polygon_area.copy() 

1240 if options.target_color == bucket_fill.TARGET_ALL: 1240 ↛ 1241line 1240 didn't jump to line 1241 because the condition on line 1240 was never true

1241 passable &= ( 

1242 (reference_pixels[:, :, 3] == 0) 

1243 | np.all(reference_pixels[:, :, :3] == 255, axis=2) 

1244 ) 

1245 candidates = np.argwhere(passable) 

1246 if candidates.size: 1246 ↛ 1262line 1246 didn't jump to line 1262 because the condition on line 1246 was always true

1247 center = np.mean( 

1248 np.array([[point.x() - rect.x(), point.y() - rect.y()] 

1249 for point in points]), 

1250 axis=0, 

1251 ) 

1252 distances = ( 

1253 (candidates[:, 1] - center[0]) ** 2 

1254 + (candidates[:, 0] - center[1]) ** 2 

1255 ) 

1256 seed_y, seed_x = candidates[int(np.argmin(distances))] 

1257 region = self._scanline_connected_region( 

1258 passable, (int(seed_x), int(seed_y)) 

1259 ) 

1260 mask_pixels[:, :, 3][~region] = 0 

1261 else: 

1262 mask_pixels[:, :, 3] = 0 

1263 

1264 if not mask_pixels[:, :, 3].any(): 

1265 return 

1266 self.push_layer_region_undo(rect) 

1267 overlay = mask_image.convertToFormat( 

1268 QImage.Format.Format_ARGB32_Premultiplied 

1269 ) 

1270 exact_lasso_colors = ( 

1271 (fill_color, line_color) 

1272 if line_color is not None 

1273 else (fill_color,) 

1274 ) 

1275 colors = self._blend_overlay_into_active_layer( 

1276 overlay, 

1277 rect.topLeft(), 

1278 exact_colors=exact_lasso_colors, 

1279 opacity=getattr(self, "fill_opacity", 1.0), 

1280 ) 

1281 

1282 self.active_layer.has_content = True 

1283 self._emit_actual_paint_colors(colors) 

1284 self.cellChanged.emit( 

1285 self.current_frame, 

1286 self.active_layer_index, 

1287 ) 

1288 self.update()