Add composite color strip source type with layer blending

Composite sources stack multiple existing color strip sources as layers
with configurable blend modes (Normal, Add, Multiply, Screen) and per-layer
opacity. Includes full CRUD, hot-reload, delete protection for referenced
layers, and pre-allocated integer blend math at 30 FPS.

Also eliminates per-frame numpy allocations in color_strip_stream,
effect_stream, and wled_target_processor (buffer pre-allocation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 11:01:44 +03:00
parent e5a6eafd09
commit 2657f46e5d
15 changed files with 1042 additions and 144 deletions

View File

@@ -79,6 +79,7 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
description=source.description,
frame_interpolation=getattr(source, "frame_interpolation", None),
animation=getattr(source, "animation", None),
layers=getattr(source, "layers", None),
overlay_active=overlay_active,
created_at=source.created_at,
updated_at=source.updated_at,
@@ -128,6 +129,8 @@ async def create_color_strip_source(
stops = [s.model_dump() for s in data.stops] if data.stops is not None else None
layers = [l.model_dump() for l in data.layers] if data.layers is not None else None
source = store.create_source(
name=data.name,
source_type=data.source_type,
@@ -152,6 +155,7 @@ async def create_color_strip_source(
intensity=data.intensity,
scale=data.scale,
mirror=data.mirror,
layers=layers,
)
return _css_to_response(source)
@@ -193,6 +197,8 @@ async def update_color_strip_source(
stops = [s.model_dump() for s in data.stops] if data.stops is not None else None
layers = [l.model_dump() for l in data.layers] if data.layers is not None else None
source = store.update_source(
source_id=source_id,
name=data.name,
@@ -217,6 +223,7 @@ async def update_color_strip_source(
intensity=data.intensity,
scale=data.scale,
mirror=data.mirror,
layers=layers,
)
# Hot-reload running stream (no restart needed for in-place param changes)
@@ -249,6 +256,12 @@ async def delete_color_strip_source(
detail="Color strip source is referenced by one or more LED targets. "
"Delete or reassign the targets first.",
)
if store.is_referenced_by_composite(source_id):
raise HTTPException(
status_code=409,
detail="Color strip source is used as a layer in a composite source. "
"Remove it from the composite first.",
)
store.delete_source(source_id)
except HTTPException:
raise