imagebaker 0.0.41__py3-none-any.whl → 0.0.48__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.
- imagebaker/__init__.py +1 -1
- imagebaker/core/__init__.py +0 -0
- imagebaker/core/configs/__init__.py +1 -0
- imagebaker/core/configs/configs.py +156 -0
- imagebaker/core/defs/__init__.py +1 -0
- imagebaker/core/defs/defs.py +258 -0
- imagebaker/core/plugins/__init__.py +0 -0
- imagebaker/core/plugins/base_plugin.py +39 -0
- imagebaker/core/plugins/cosine_plugin.py +39 -0
- imagebaker/layers/__init__.py +3 -0
- imagebaker/layers/annotable_layer.py +847 -0
- imagebaker/layers/base_layer.py +724 -0
- imagebaker/layers/canvas_layer.py +1007 -0
- imagebaker/list_views/__init__.py +3 -0
- imagebaker/list_views/annotation_list.py +203 -0
- imagebaker/list_views/canvas_list.py +185 -0
- imagebaker/list_views/image_list.py +138 -0
- imagebaker/list_views/layer_list.py +390 -0
- imagebaker/list_views/layer_settings.py +219 -0
- imagebaker/models/__init__.py +0 -0
- imagebaker/models/base_model.py +150 -0
- imagebaker/tabs/__init__.py +2 -0
- imagebaker/tabs/baker_tab.py +496 -0
- imagebaker/tabs/layerify_tab.py +837 -0
- imagebaker/utils/__init__.py +0 -0
- imagebaker/utils/image.py +105 -0
- imagebaker/utils/state_utils.py +92 -0
- imagebaker/utils/transform_mask.py +107 -0
- imagebaker/window/__init__.py +1 -0
- imagebaker/window/app.py +136 -0
- imagebaker/window/main_window.py +181 -0
- imagebaker/workers/__init__.py +3 -0
- imagebaker/workers/baker_worker.py +247 -0
- imagebaker/workers/layerify_worker.py +91 -0
- imagebaker/workers/model_worker.py +54 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/METADATA +6 -6
- imagebaker-0.0.48.dist-info/RECORD +41 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/WHEEL +1 -1
- imagebaker-0.0.41.dist-info/RECORD +0 -7
- {imagebaker-0.0.41.dist-info/licenses → imagebaker-0.0.48.dist-info}/LICENSE +0 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/entry_points.txt +0 -0
- {imagebaker-0.0.41.dist-info → imagebaker-0.0.48.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,724 @@
|
|
1
|
+
from imagebaker.core.configs import LayerConfig, CanvasConfig, CursorDef
|
2
|
+
from imagebaker.core.defs import Annotation, MouseMode, LayerState, DrawingState
|
3
|
+
from imagebaker.utils.state_utils import calculate_intermediate_states
|
4
|
+
from imagebaker import logger
|
5
|
+
|
6
|
+
from PySide6.QtCore import QPointF, QPoint, Qt, Signal, QSizeF, QSize
|
7
|
+
from PySide6.QtGui import (
|
8
|
+
QColor,
|
9
|
+
QPixmap,
|
10
|
+
QPainter,
|
11
|
+
QMouseEvent,
|
12
|
+
QKeyEvent,
|
13
|
+
QImage,
|
14
|
+
QPen,
|
15
|
+
QCursor,
|
16
|
+
)
|
17
|
+
from PySide6.QtWidgets import QWidget
|
18
|
+
|
19
|
+
from typing import Optional
|
20
|
+
from pathlib import Path
|
21
|
+
|
22
|
+
|
23
|
+
class BaseLayer(QWidget):
|
24
|
+
messageSignal = Signal(str)
|
25
|
+
zoomChanged = Signal(float)
|
26
|
+
mouseMoved = Signal(QPointF)
|
27
|
+
annotationCleared = Signal()
|
28
|
+
layerRemoved = Signal(int)
|
29
|
+
layersChanged = Signal()
|
30
|
+
layerSignal = Signal(object)
|
31
|
+
|
32
|
+
def __init__(self, parent: QWidget, config: LayerConfig | CanvasConfig):
|
33
|
+
"""
|
34
|
+
BaseLayer is an abstract class that represents a single layer in the canvas.
|
35
|
+
It provides functionality for managing layer properties, handling user interactions,
|
36
|
+
and rendering the layer's content. This class is designed to be extended by
|
37
|
+
subclasses that implement specific layer behaviors.
|
38
|
+
|
39
|
+
Attributes:
|
40
|
+
id (int): Unique identifier for the layer.
|
41
|
+
layer_state (LayerState): The current state of the layer, including properties
|
42
|
+
like position, scale, rotation, and visibility.
|
43
|
+
previous_state (LayerState): The previous state of the layer, used for undo operations.
|
44
|
+
layers (list[BaseLayer]): A list of child layers associated with this layer.
|
45
|
+
annotations (list[Annotation]): A list of annotations associated with the layer.
|
46
|
+
selected_annotation (Optional[Annotation]): The currently selected annotation.
|
47
|
+
current_annotation (Optional[Annotation]): The annotation currently being created or edited.
|
48
|
+
copied_annotation (Optional[Annotation]): A copied annotation for pasting.
|
49
|
+
image (QPixmap): The image associated with the layer.
|
50
|
+
scale (float): The current scale of the layer.
|
51
|
+
pan_offset (QPointF): The current pan offset of the layer.
|
52
|
+
mouse_mode (MouseMode): The current mouse interaction mode (e.g., DRAW, PAN, IDLE).
|
53
|
+
states (dict[int, list[LayerState]]): A dictionary of saved states for the layer,
|
54
|
+
indexed by step.
|
55
|
+
current_step (int): The current step in the layer's state history.
|
56
|
+
drawing_color (QColor): The color used for drawing operations.
|
57
|
+
brush_size (int): The size of the brush used for drawing operations.
|
58
|
+
config (LayerConfig | CanvasConfig): Configuration settings for the layer.
|
59
|
+
file_path (Path): The file path associated with the layer's image.
|
60
|
+
visible (bool): Whether the layer is visible.
|
61
|
+
selected (bool): Whether the layer is selected.
|
62
|
+
opacity (float): The opacity of the layer (0.0 to 1.0).
|
63
|
+
transform_origin (QPointF): The origin point for transformations (e.g., rotation, scaling).
|
64
|
+
playing (bool): Whether the layer is currently in a "playing" state (e.g., animation).
|
65
|
+
allow_annotation_export (bool): Whether annotations can be exported for this layer.
|
66
|
+
|
67
|
+
Signals:
|
68
|
+
messageSignal (str): Emitted when a message needs to be displayed.
|
69
|
+
zoomChanged (float): Emitted when the zoom level changes.
|
70
|
+
mouseMoved (QPointF): Emitted when the mouse moves over the layer.
|
71
|
+
annotationCleared (): Emitted when annotations are cleared.
|
72
|
+
layerRemoved (int): Emitted when a layer is removed.
|
73
|
+
layersChanged (): Emitted when the layer list changes.
|
74
|
+
layerSignal (object): Emitted with a layer-related signal.
|
75
|
+
|
76
|
+
Methods:
|
77
|
+
save_current_state(steps: int = 1):
|
78
|
+
Save the current state of the layer, including intermediate states
|
79
|
+
calculated between the previous and current states.
|
80
|
+
|
81
|
+
set_image(image_path: Path | QPixmap | QImage):
|
82
|
+
Set the image for the layer from a file path, QPixmap, or QImage.
|
83
|
+
|
84
|
+
get_layer(id: str) -> "BaseLayer":
|
85
|
+
Retrieve a child layer by its ID.
|
86
|
+
|
87
|
+
reset_view():
|
88
|
+
Reset the view of the layer, including scale and offset.
|
89
|
+
|
90
|
+
clear_annotations():
|
91
|
+
Clear all annotations associated with the layer.
|
92
|
+
|
93
|
+
update_cursor():
|
94
|
+
Update the cursor based on the current mouse mode.
|
95
|
+
|
96
|
+
undo():
|
97
|
+
Undo the last change to the layer's state.
|
98
|
+
|
99
|
+
set_mode(mode: MouseMode):
|
100
|
+
Set the mouse interaction mode for the layer.
|
101
|
+
|
102
|
+
widget_to_image_pos(pos: QPointF) -> QPointF:
|
103
|
+
Convert a widget position to an image position.
|
104
|
+
|
105
|
+
get_thumbnail(annotation: Annotation = None) -> QPixmap:
|
106
|
+
Generate a thumbnail for the layer or a specific annotation.
|
107
|
+
|
108
|
+
copy() -> "BaseLayer":
|
109
|
+
Create a copy of the layer, including its properties and annotations.
|
110
|
+
|
111
|
+
paintEvent(event):
|
112
|
+
Handle the paint event for the layer.
|
113
|
+
|
114
|
+
paint_layer(painter: QPainter):
|
115
|
+
Abstract method to paint the layer's content. Must be implemented by subclasses.
|
116
|
+
|
117
|
+
handle_mouse_press(event: QMouseEvent):
|
118
|
+
Abstract method to handle mouse press events. Must be implemented by subclasses.
|
119
|
+
|
120
|
+
handle_mouse_move(event: QMouseEvent):
|
121
|
+
Abstract method to handle mouse move events. Must be implemented by subclasses.
|
122
|
+
|
123
|
+
handle_mouse_release(event: QMouseEvent):
|
124
|
+
Abstract method to handle mouse release events. Must be implemented by subclasses.
|
125
|
+
|
126
|
+
handle_wheel(event: QWheelEvent):
|
127
|
+
Abstract method to handle wheel events. Must be implemented by subclasses.
|
128
|
+
|
129
|
+
handle_key_press(event: QKeyEvent):
|
130
|
+
Abstract method to handle key press events. Must be implemented by subclasses.
|
131
|
+
|
132
|
+
handle_key_release(event: QKeyEvent):
|
133
|
+
Abstract method to handle key release events. Must be implemented by subclasses.
|
134
|
+
|
135
|
+
Notes:
|
136
|
+
- This class is designed to be extended by subclasses that implement specific
|
137
|
+
layer behaviors (e.g., drawing, annotation, image manipulation).
|
138
|
+
- The `paint_layer` method must be implemented by subclasses to define how
|
139
|
+
the layer's content is rendered.
|
140
|
+
|
141
|
+
"""
|
142
|
+
super().__init__(parent)
|
143
|
+
self.id = id(self)
|
144
|
+
self.layer_state = LayerState(layer_id=self.id)
|
145
|
+
self._previous_state = None
|
146
|
+
self.thumbnails = {}
|
147
|
+
self.label_rects = []
|
148
|
+
self._last_state = None
|
149
|
+
self.config = config
|
150
|
+
self.parent_obj = parent
|
151
|
+
self.mouse_mode = MouseMode.IDLE
|
152
|
+
self.file_path: Path = Path("Runtime")
|
153
|
+
self.layersChanged.connect(self.update)
|
154
|
+
|
155
|
+
self.drag_start: QPointF = None
|
156
|
+
self.drag_offset: QPointF = None
|
157
|
+
self.offset: QPointF = QPointF(0, 0)
|
158
|
+
self.pan_start: QPointF = None
|
159
|
+
self.pan_offset: QPointF = None
|
160
|
+
self.image = QPixmap()
|
161
|
+
self.annotations: list[Annotation] = []
|
162
|
+
self.current_annotation: Optional[Annotation] = None
|
163
|
+
self.copied_annotation: Optional[Annotation] = None
|
164
|
+
self.selected_annotation: Optional[Annotation] = None
|
165
|
+
|
166
|
+
self.layers: list[BaseLayer] = []
|
167
|
+
self.layer_masks = []
|
168
|
+
self._back_buffer = QPixmap()
|
169
|
+
self.current_label: str = None
|
170
|
+
self.current_color: QColor = QColor(255, 255, 255)
|
171
|
+
|
172
|
+
self.scale = 1.0
|
173
|
+
self.pan_offset = QPointF(0, 0)
|
174
|
+
self.last_pan_point = None
|
175
|
+
self._dragging_layer = None
|
176
|
+
self._drag_offset = QPointF(0, 0)
|
177
|
+
self._current_hover = None
|
178
|
+
self._active_handle = None
|
179
|
+
self._transform_start = None
|
180
|
+
self._is_panning = False
|
181
|
+
self.offset = QPointF(0, 0)
|
182
|
+
self.copied_layer: BaseLayer = None
|
183
|
+
self.selected_layer: BaseLayer = None
|
184
|
+
self.mouse_mode = MouseMode.IDLE
|
185
|
+
self.prev_mouse_mode = MouseMode.IDLE
|
186
|
+
self.states: dict[str, list[LayerState]] = dict()
|
187
|
+
|
188
|
+
self.states: dict[int, list[LayerState]] = dict()
|
189
|
+
self.previous_state = None
|
190
|
+
self.current_step = 0
|
191
|
+
self.drawing_color = QColor(Qt.red) # Default drawing color
|
192
|
+
self.brush_size = 5 # Default brush size
|
193
|
+
|
194
|
+
if isinstance(config, LayerConfig):
|
195
|
+
self.current_label = self.config.predefined_labels[0].name
|
196
|
+
self.current_color = self.config.predefined_labels[0].color
|
197
|
+
|
198
|
+
self.setMouseTracking(True)
|
199
|
+
self.setFocusPolicy(Qt.StrongFocus)
|
200
|
+
|
201
|
+
def get_layer(self, id: str) -> "BaseLayer":
|
202
|
+
"""
|
203
|
+
Get a child layer by its ID.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
id (str): The ID of the layer to retrieve.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Child of BaseLayer: The child layer with the specified ID, or None if not found
|
210
|
+
"""
|
211
|
+
for layer in self.layers:
|
212
|
+
if layer.layer_id == id:
|
213
|
+
return layer
|
214
|
+
return None
|
215
|
+
|
216
|
+
def save_current_state(self, steps: int = 1):
|
217
|
+
"""
|
218
|
+
Save the current state of the layer, including intermediate states
|
219
|
+
calculated between the previous and current states.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
steps (int): The number of intermediate steps to calculate between
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
None
|
226
|
+
"""
|
227
|
+
curr_states = {}
|
228
|
+
mode = self.mouse_mode
|
229
|
+
|
230
|
+
for layer in self.layers:
|
231
|
+
# Calculate intermediate states between previous_state and current_state
|
232
|
+
intermediate_states = calculate_intermediate_states(
|
233
|
+
layer.previous_state, layer.layer_state.copy(), steps
|
234
|
+
)
|
235
|
+
is_selected = layer.selected
|
236
|
+
|
237
|
+
for step, state in enumerate(intermediate_states):
|
238
|
+
step += self.current_step
|
239
|
+
|
240
|
+
logger.info(f"Saving state {step} for layer {layer.layer_id}")
|
241
|
+
state.selected = False
|
242
|
+
if step not in curr_states:
|
243
|
+
curr_states[step] = []
|
244
|
+
|
245
|
+
# Deep copy the drawing states to avoid unintended modifications
|
246
|
+
state.drawing_states = [
|
247
|
+
DrawingState(
|
248
|
+
position=d.position,
|
249
|
+
color=d.color,
|
250
|
+
size=d.size,
|
251
|
+
)
|
252
|
+
for d in layer.layer_state.drawing_states
|
253
|
+
]
|
254
|
+
curr_states[step].append(state)
|
255
|
+
|
256
|
+
# Update the layer's previous_state to the current state
|
257
|
+
layer.previous_state = layer.layer_state.copy()
|
258
|
+
layer.selected = is_selected
|
259
|
+
|
260
|
+
# Save the calculated states in self.states
|
261
|
+
for step, states in curr_states.items():
|
262
|
+
self.states[step] = states
|
263
|
+
self.current_step = step
|
264
|
+
|
265
|
+
# Save the current layer's state
|
266
|
+
self.previous_state = self.layer_state.copy()
|
267
|
+
self.layer_state.drawing_states = [
|
268
|
+
DrawingState(
|
269
|
+
position=d.position,
|
270
|
+
color=d.color,
|
271
|
+
size=d.size,
|
272
|
+
)
|
273
|
+
for d in self.layer_state.drawing_states
|
274
|
+
]
|
275
|
+
|
276
|
+
# Emit a message signal indicating the state has been saved
|
277
|
+
self.messageSignal.emit(f"Saved state {self.current_step}")
|
278
|
+
self.mouse_mode = mode
|
279
|
+
|
280
|
+
self.update()
|
281
|
+
|
282
|
+
def minimumSizeHint(self):
|
283
|
+
"""Return the minimum size hint for the widget."""
|
284
|
+
return QSize(100, 100)
|
285
|
+
|
286
|
+
def widget_to_image_pos(self, pos: QPointF) -> QPointF:
|
287
|
+
"""
|
288
|
+
Convert a widget position to an image position.
|
289
|
+
"""
|
290
|
+
return QPointF(
|
291
|
+
(pos.x() - self.offset.x()) / self.scale,
|
292
|
+
(pos.y() - self.offset.y()) / self.scale,
|
293
|
+
)
|
294
|
+
|
295
|
+
def update_cursor(self):
|
296
|
+
"""
|
297
|
+
Update the cursor based on the current mouse mode.
|
298
|
+
"""
|
299
|
+
if MouseMode.POINT == self.mouse_mode:
|
300
|
+
self.setCursor(CursorDef.POINT_CURSOR)
|
301
|
+
elif MouseMode.RECTANGLE == self.mouse_mode:
|
302
|
+
self.setCursor(CursorDef.RECTANGLE_CURSOR)
|
303
|
+
elif MouseMode.POLYGON == self.mouse_mode:
|
304
|
+
self.setCursor(CursorDef.POLYGON_CURSOR)
|
305
|
+
elif MouseMode.PAN == self.mouse_mode:
|
306
|
+
self.setCursor(CursorDef.PAN_CURSOR)
|
307
|
+
elif MouseMode.IDLE == self.mouse_mode:
|
308
|
+
self.setCursor(CursorDef.IDLE_CURSOR)
|
309
|
+
elif MouseMode.RESIZE == self.mouse_mode:
|
310
|
+
self.setCursor(CursorDef.RECTANGLE_CURSOR)
|
311
|
+
elif MouseMode.RESIZE_HEIGHT == self.mouse_mode:
|
312
|
+
self.setCursor(CursorDef.TRANSFORM_UPDOWN)
|
313
|
+
elif MouseMode.RESIZE_WIDTH == self.mouse_mode:
|
314
|
+
self.setCursor(CursorDef.TRANSFORM_LEFTRIGHT)
|
315
|
+
elif MouseMode.GRAB == self.mouse_mode:
|
316
|
+
self.setCursor(CursorDef.GRAB_CURSOR)
|
317
|
+
elif self.mouse_mode == MouseMode.DRAW:
|
318
|
+
# Create a custom cursor for drawing (circle representing brush size)
|
319
|
+
self.setCursor(self._create_custom_cursor(self.drawing_color, "circle"))
|
320
|
+
|
321
|
+
elif self.mouse_mode == MouseMode.ERASE:
|
322
|
+
# Create a custom cursor for erasing (square representing eraser size)
|
323
|
+
self.setCursor(self._create_custom_cursor(Qt.white, "square"))
|
324
|
+
|
325
|
+
else:
|
326
|
+
# Reset to default cursor
|
327
|
+
self.setCursor(Qt.ArrowCursor)
|
328
|
+
|
329
|
+
def _create_custom_cursor(self, color: QColor, shape: str) -> QCursor:
|
330
|
+
"""Create a custom cursor with the given color and shape."""
|
331
|
+
pixmap = QPixmap(self.brush_size * 2, self.brush_size * 2)
|
332
|
+
pixmap.fill(Qt.transparent)
|
333
|
+
painter = QPainter(pixmap)
|
334
|
+
painter.setRenderHints(QPainter.Antialiasing)
|
335
|
+
painter.setPen(QPen(Qt.black, 1)) # Border color for the cursor
|
336
|
+
painter.setBrush(color)
|
337
|
+
|
338
|
+
if shape == "circle":
|
339
|
+
painter.drawEllipse(
|
340
|
+
pixmap.rect().center(), self.brush_size, self.brush_size
|
341
|
+
)
|
342
|
+
elif shape == "square":
|
343
|
+
painter.drawRect(pixmap.rect().adjusted(1, 1, -1, -1))
|
344
|
+
|
345
|
+
painter.end()
|
346
|
+
return QCursor(pixmap)
|
347
|
+
|
348
|
+
def set_image(self, image_path: Path | QPixmap | QImage):
|
349
|
+
"""
|
350
|
+
Set the image for the layer from a file path, QPixmap, or QImage.
|
351
|
+
"""
|
352
|
+
if isinstance(image_path, Path):
|
353
|
+
self.file_path = image_path
|
354
|
+
|
355
|
+
if image_path.exists():
|
356
|
+
self.image.load(str(image_path))
|
357
|
+
self.reset_view()
|
358
|
+
self.update()
|
359
|
+
elif isinstance(image_path, QPixmap):
|
360
|
+
self.image = image_path
|
361
|
+
self.reset_view()
|
362
|
+
self.update()
|
363
|
+
elif isinstance(image_path, QImage):
|
364
|
+
self.image = QPixmap.fromImage(image_path)
|
365
|
+
self.reset_view()
|
366
|
+
self.update()
|
367
|
+
|
368
|
+
self.original_size = QSizeF(self.image.size()) # Store original size
|
369
|
+
|
370
|
+
def get_thumbnail(self, annotation: Annotation = None):
|
371
|
+
"""
|
372
|
+
Generate a thumbnail for the layer or a specific annotation.
|
373
|
+
"""
|
374
|
+
image = QPixmap(*self.config.normal_draw_config.thumbnail_size)
|
375
|
+
image.fill(Qt.transparent)
|
376
|
+
|
377
|
+
if annotation:
|
378
|
+
if annotation.rectangle:
|
379
|
+
image = self.image.copy(annotation.rectangle.toRect())
|
380
|
+
elif annotation.polygon:
|
381
|
+
image = self.image.copy(annotation.polygon.boundingRect().toRect())
|
382
|
+
elif annotation.points:
|
383
|
+
# Create a small thumbnail around the point
|
384
|
+
thumbnail_size = 100
|
385
|
+
thumbnail = QPixmap(thumbnail_size, thumbnail_size)
|
386
|
+
thumbnail.fill(Qt.transparent)
|
387
|
+
painter = QPainter(thumbnail)
|
388
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
389
|
+
painter.setBrush(annotation.color)
|
390
|
+
painter.setPen(Qt.NoPen)
|
391
|
+
painter.drawEllipse(thumbnail.rect().center() + QPoint(-5, -5), 10, 10)
|
392
|
+
painter.end()
|
393
|
+
image = thumbnail
|
394
|
+
else:
|
395
|
+
if self.image:
|
396
|
+
image = self.image.copy(
|
397
|
+
0, 0, *self.config.normal_draw_config.thumbnail_size
|
398
|
+
)
|
399
|
+
elif len(self.layers) > 0:
|
400
|
+
image = self.layers[0].get_thumbnail()
|
401
|
+
else:
|
402
|
+
image = QPixmap(*self.config.normal_draw_config.thumbnail_size)
|
403
|
+
image.fill(Qt.transparent)
|
404
|
+
|
405
|
+
return image.scaled(*self.config.normal_draw_config.thumbnail_size)
|
406
|
+
|
407
|
+
def copy(self):
|
408
|
+
"""
|
409
|
+
Create a copy of the layer, including its properties and annotations.
|
410
|
+
Should be overridden by subclasses to copy additional properties.
|
411
|
+
"""
|
412
|
+
layer = self.__class__(self.parent_obj, self.config)
|
413
|
+
layer.set_image(self.image)
|
414
|
+
layer.annotations = [ann.copy() for ann in self.annotations]
|
415
|
+
layer.layers = [layer.copy() for layer in self.layers]
|
416
|
+
layer.layer_name = self.layer_name
|
417
|
+
layer.position = self.position
|
418
|
+
layer.rotation = self.rotation
|
419
|
+
layer.scale = self.scale
|
420
|
+
layer.scale_x = self.scale_x
|
421
|
+
layer.scale_y = self.scale_y
|
422
|
+
layer.opacity = self.opacity
|
423
|
+
layer.visible = self.visible
|
424
|
+
layer.selected = False
|
425
|
+
layer.is_annotable = self.is_annotable
|
426
|
+
return layer
|
427
|
+
|
428
|
+
def set_mode(self, mode: MouseMode):
|
429
|
+
"""
|
430
|
+
Set the mouse interaction mode for the layer.
|
431
|
+
"""
|
432
|
+
# Preserve current annotation when changing modes
|
433
|
+
if mode == self.mouse_mode:
|
434
|
+
return
|
435
|
+
|
436
|
+
# Only reset if switching to a different annotation mode
|
437
|
+
if mode not in [MouseMode.POLYGON, MouseMode.RECTANGLE, MouseMode.POINT]:
|
438
|
+
self.current_annotation = None
|
439
|
+
|
440
|
+
self.mouse_mode = mode
|
441
|
+
logger.debug(f"Layer {self.layer_id}: Mode set to {mode}")
|
442
|
+
self.update()
|
443
|
+
|
444
|
+
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
445
|
+
# return super().mouseDoubleClickEvent(event)
|
446
|
+
pos = event.pos()
|
447
|
+
self.handle_mouse_double_click(event, pos)
|
448
|
+
self.update()
|
449
|
+
super().mouseDoubleClickEvent(event)
|
450
|
+
|
451
|
+
def mousePressEvent(self, event):
|
452
|
+
self.handle_mouse_press(event)
|
453
|
+
|
454
|
+
self.update()
|
455
|
+
super().mousePressEvent(event)
|
456
|
+
|
457
|
+
def mouseMoveEvent(self, event: QMouseEvent):
|
458
|
+
|
459
|
+
self.handle_mouse_move(event)
|
460
|
+
self.update()
|
461
|
+
super().mouseMoveEvent(event)
|
462
|
+
|
463
|
+
def mouseReleaseEvent(self, event: QMouseEvent):
|
464
|
+
|
465
|
+
self.handle_mouse_release(event)
|
466
|
+
self.update()
|
467
|
+
super().mouseReleaseEvent(event)
|
468
|
+
|
469
|
+
def wheelEvent(self, event):
|
470
|
+
self.handle_wheel(event)
|
471
|
+
self.update()
|
472
|
+
super().wheelEvent(event)
|
473
|
+
|
474
|
+
def keyPressEvent(self, event: QKeyEvent):
|
475
|
+
self.handle_key_press(event)
|
476
|
+
self.update()
|
477
|
+
super().keyPressEvent(event)
|
478
|
+
|
479
|
+
def keyReleaseEvent(self, event):
|
480
|
+
if self.is_annotable:
|
481
|
+
self.handle_key_release(event)
|
482
|
+
else:
|
483
|
+
self.handle_key_release(event)
|
484
|
+
self.update()
|
485
|
+
|
486
|
+
def handle_mouse_double_click(self, event, pos):
|
487
|
+
raise NotImplementedError
|
488
|
+
|
489
|
+
def handle_mouse_press(self, event):
|
490
|
+
"""
|
491
|
+
Handle mouse press events for selecting layers, initiating transformations,
|
492
|
+
or starting drawing/erasing operations.
|
493
|
+
|
494
|
+
Args:
|
495
|
+
event (QMouseEvent): The mouse press event.
|
496
|
+
"""
|
497
|
+
raise NotImplementedError
|
498
|
+
|
499
|
+
def handle_mouse_move(self, event):
|
500
|
+
"""
|
501
|
+
Handle mouse move events for panning, drawing, erasing, or transforming layers.
|
502
|
+
|
503
|
+
Args:
|
504
|
+
event (QMouseEvent): The mouse move event.
|
505
|
+
"""
|
506
|
+
raise NotImplementedError
|
507
|
+
|
508
|
+
def handle_mouse_release(self, event):
|
509
|
+
"""
|
510
|
+
Handle mouse release events, such as resetting the active handle or stopping
|
511
|
+
drawing/erasing operations.
|
512
|
+
|
513
|
+
Args:
|
514
|
+
event (QMouseEvent): The mouse release event.
|
515
|
+
"""
|
516
|
+
raise NotImplementedError
|
517
|
+
|
518
|
+
def handle_wheel(self, event):
|
519
|
+
"""
|
520
|
+
Handle mouse wheel events for adjusting the brush size or zooming the canvas.
|
521
|
+
|
522
|
+
Args:
|
523
|
+
event (QWheelEvent): The wheel event.
|
524
|
+
"""
|
525
|
+
raise NotImplementedError
|
526
|
+
|
527
|
+
def handle_key_press(self, event):
|
528
|
+
raise NotImplementedError
|
529
|
+
|
530
|
+
def handle_key_release(self, event):
|
531
|
+
raise NotImplementedError
|
532
|
+
|
533
|
+
def reset_view(self):
|
534
|
+
self.scale = 1.0
|
535
|
+
self.offset = QPointF(0, 0)
|
536
|
+
|
537
|
+
def clear_annotations(self):
|
538
|
+
self.annotations.clear()
|
539
|
+
self.selected_annotation = None
|
540
|
+
self.current_annotation = None
|
541
|
+
self.annotationCleared.emit()
|
542
|
+
self.update()
|
543
|
+
|
544
|
+
def paintEvent(self, event):
|
545
|
+
self.paint_event()
|
546
|
+
|
547
|
+
def paint_event(self):
|
548
|
+
painter = QPainter(self)
|
549
|
+
|
550
|
+
painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
|
551
|
+
|
552
|
+
painter.fillRect(
|
553
|
+
self.rect(),
|
554
|
+
QColor(
|
555
|
+
self.config.normal_draw_config.background_color.red(),
|
556
|
+
self.config.normal_draw_config.background_color.green(),
|
557
|
+
self.config.normal_draw_config.background_color.blue(),
|
558
|
+
),
|
559
|
+
)
|
560
|
+
self.paint_layer(painter)
|
561
|
+
|
562
|
+
def paint_layer(self, painter: QPainter):
|
563
|
+
raise NotImplementedError
|
564
|
+
|
565
|
+
def __del__(self):
|
566
|
+
logger.debug(f"Layer {self.layer_id}: {self.layer_name} deleted.")
|
567
|
+
|
568
|
+
# override update to store last state
|
569
|
+
def update(self):
|
570
|
+
self._last_state = self.layer_state
|
571
|
+
self.update_cursor()
|
572
|
+
super().update()
|
573
|
+
|
574
|
+
def undo(self):
|
575
|
+
if self._last_state is not None:
|
576
|
+
self.layer_state = self._last_state
|
577
|
+
self.update()
|
578
|
+
|
579
|
+
# Layer ID Property
|
580
|
+
@property
|
581
|
+
def layer_id(self) -> int:
|
582
|
+
return self.layer_state.layer_id
|
583
|
+
|
584
|
+
@layer_id.setter
|
585
|
+
def layer_id(self, value: int):
|
586
|
+
self.layer_state.layer_id = value
|
587
|
+
|
588
|
+
@property
|
589
|
+
def is_annotable(self) -> bool:
|
590
|
+
return self.layer_state.is_annotable
|
591
|
+
|
592
|
+
@is_annotable.setter
|
593
|
+
def is_annotable(self, value: bool):
|
594
|
+
self.layer_state.is_annotable = value
|
595
|
+
|
596
|
+
# Layer Name Property
|
597
|
+
@property
|
598
|
+
def layer_name(self) -> str:
|
599
|
+
return self.layer_state.layer_name
|
600
|
+
|
601
|
+
@layer_name.setter
|
602
|
+
def layer_name(self, value: str):
|
603
|
+
self.layer_state.layer_name = value
|
604
|
+
|
605
|
+
# Position Property
|
606
|
+
@property
|
607
|
+
def position(self) -> QPointF:
|
608
|
+
return self.layer_state.position
|
609
|
+
|
610
|
+
@position.setter
|
611
|
+
def position(self, value: QPointF):
|
612
|
+
self.layer_state.position = value
|
613
|
+
|
614
|
+
# Rotation Property
|
615
|
+
@property
|
616
|
+
def rotation(self) -> float:
|
617
|
+
return self.layer_state.rotation
|
618
|
+
|
619
|
+
@rotation.setter
|
620
|
+
def rotation(self, value: float):
|
621
|
+
self.layer_state.rotation = value
|
622
|
+
|
623
|
+
# Scale Property
|
624
|
+
@property
|
625
|
+
def scale(self) -> float:
|
626
|
+
return self.layer_state.scale
|
627
|
+
|
628
|
+
@scale.setter
|
629
|
+
def scale(self, value: float):
|
630
|
+
self.layer_state.scale = value
|
631
|
+
|
632
|
+
# Scale X Property
|
633
|
+
@property
|
634
|
+
def scale_x(self) -> float:
|
635
|
+
return self.layer_state.scale_x
|
636
|
+
|
637
|
+
@scale_x.setter
|
638
|
+
def scale_x(self, value: float):
|
639
|
+
self.layer_state.scale_x = value
|
640
|
+
|
641
|
+
# Scale Y Property
|
642
|
+
@property
|
643
|
+
def scale_y(self) -> float:
|
644
|
+
return self.layer_state.scale_y
|
645
|
+
|
646
|
+
@scale_y.setter
|
647
|
+
def scale_y(self, value: float):
|
648
|
+
self.layer_state.scale_y = value
|
649
|
+
|
650
|
+
# Transform Origin Property
|
651
|
+
@property
|
652
|
+
def transform_origin(self) -> QPointF:
|
653
|
+
return self.layer_state.transform_origin
|
654
|
+
|
655
|
+
@transform_origin.setter
|
656
|
+
def transform_origin(self, value: QPointF):
|
657
|
+
self.layer_state.transform_origin = value
|
658
|
+
|
659
|
+
# Order Property
|
660
|
+
@property
|
661
|
+
def order(self) -> int:
|
662
|
+
return self.layer_state.order
|
663
|
+
|
664
|
+
@order.setter
|
665
|
+
def order(self, value: int):
|
666
|
+
self.layer_state.order = value
|
667
|
+
|
668
|
+
# Visibility Property
|
669
|
+
@property
|
670
|
+
def visible(self) -> bool:
|
671
|
+
return self.layer_state.visible
|
672
|
+
|
673
|
+
@visible.setter
|
674
|
+
def visible(self, value: bool):
|
675
|
+
self.layer_state.visible = value
|
676
|
+
|
677
|
+
# Annotation Export Property
|
678
|
+
@property
|
679
|
+
def allow_annotation_export(self) -> bool:
|
680
|
+
return self.layer_state.allow_annotation_export
|
681
|
+
|
682
|
+
@allow_annotation_export.setter
|
683
|
+
def allow_annotation_export(self, value: bool):
|
684
|
+
self.layer_state.allow_annotation_export = value
|
685
|
+
|
686
|
+
@property
|
687
|
+
def playing(self) -> bool:
|
688
|
+
return self.layer_state.playing
|
689
|
+
|
690
|
+
@playing.setter
|
691
|
+
def playing(self, value: bool):
|
692
|
+
self.layer_state.playing = value
|
693
|
+
|
694
|
+
@property
|
695
|
+
def selected(self) -> bool:
|
696
|
+
return self.layer_state.selected
|
697
|
+
|
698
|
+
@selected.setter
|
699
|
+
def selected(self, value: bool):
|
700
|
+
self.layer_state.selected = value
|
701
|
+
|
702
|
+
@property
|
703
|
+
def opacity(self) -> float:
|
704
|
+
return self.layer_state.opacity
|
705
|
+
|
706
|
+
@opacity.setter
|
707
|
+
def opacity(self, value: float):
|
708
|
+
self.layer_state.opacity = value
|
709
|
+
|
710
|
+
@property
|
711
|
+
def status(self) -> str:
|
712
|
+
return self.layer_state.status
|
713
|
+
|
714
|
+
@status.setter
|
715
|
+
def status(self, value: str):
|
716
|
+
self.layer_state.status = value
|
717
|
+
|
718
|
+
@property
|
719
|
+
def drawing_states(self) -> list[DrawingState]:
|
720
|
+
return self.layer_state.drawing_states
|
721
|
+
|
722
|
+
@drawing_states.setter
|
723
|
+
def drawing_states(self, value: list[DrawingState]):
|
724
|
+
self.layer_state.drawing_states = value
|