Add Pattern Templates for Key Colors targets with visual canvas editor

Introduce Pattern Template entity as a reusable rectangle layout that
Key Colors targets reference via pattern_template_id. This replaces
inline rectangle storage with a shared template system.

Backend:
- New PatternTemplate data model, store (JSON persistence), CRUD API
- KC targets now reference pattern_template_id instead of inline rectangles
- ProcessorManager resolves pattern template at KC processing start
- Picture source test endpoint supports capture_duration=0 for single frame
- Delete protection: 409 when template is referenced by a KC target

Frontend:
- Pattern Templates section in Key Colors sub-tab with card UI
- Visual canvas editor with drag-to-move, 8-point resize handles
- Background capture from any picture source for visual alignment
- Precise coordinate list synced bidirectionally with canvas
- Resizable editor container, viewport-constrained modal
- KC target editor uses pattern template dropdown instead of inline rects
- Localization (en/ru) for all new UI elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 18:07:40 +03:00
parent 5f9bc9a37e
commit 87e7eee743
21 changed files with 1423 additions and 150 deletions

View File

@@ -155,7 +155,7 @@ class ProcessorManager:
Targets are registered for processing (streaming sources to devices).
"""
def __init__(self, picture_source_store=None, capture_template_store=None, pp_template_store=None):
def __init__(self, picture_source_store=None, capture_template_store=None, pp_template_store=None, pattern_template_store=None):
"""Initialize processor manager."""
self._devices: Dict[str, DeviceState] = {}
self._targets: Dict[str, TargetState] = {}
@@ -165,6 +165,7 @@ class ProcessorManager:
self._picture_source_store = picture_source_store
self._capture_template_store = capture_template_store
self._pp_template_store = pp_template_store
self._pattern_template_store = pattern_template_store
self._live_stream_manager = LiveStreamManager(
picture_source_store, capture_template_store, pp_template_store
)
@@ -1054,8 +1055,19 @@ class ProcessorManager:
if not state.picture_source_id:
raise ValueError(f"KC target {target_id} has no picture source assigned")
if not state.settings.rectangles:
raise ValueError(f"KC target {target_id} has no rectangles defined")
if not state.settings.pattern_template_id:
raise ValueError(f"KC target {target_id} has no pattern template assigned")
# Resolve pattern template to get rectangles
try:
pattern_template = self._pattern_template_store.get_template(state.settings.pattern_template_id)
except (ValueError, AttributeError):
raise ValueError(f"Pattern template {state.settings.pattern_template_id} not found")
if not pattern_template.rectangles:
raise ValueError(f"Pattern template {state.settings.pattern_template_id} has no rectangles")
state._resolved_rectangles = pattern_template.rectangles
# Acquire live stream
try:
@@ -1133,9 +1145,11 @@ class ProcessorManager:
frame_time = 1.0 / target_fps
fps_samples: List[float] = []
rectangles = state._resolved_rectangles
logger.info(
f"KC processing loop started for target {target_id} "
f"(fps={target_fps}, rects={len(settings.rectangles)})"
f"(fps={target_fps}, rects={len(rectangles)})"
)
try:
@@ -1152,7 +1166,7 @@ class ProcessorManager:
h, w = img.shape[:2]
colors: Dict[str, Tuple[int, int, int]] = {}
for rect in settings.rectangles:
for rect in rectangles:
# Convert relative coords to pixel coords
px_x = max(0, int(rect.x * w))
px_y = max(0, int(rect.y * h))