CSS: add GradientColorStripSource with visual editor
- Backend: GradientColorStripSource storage model, GradientColorStripStream with numpy interpolation (bidirectional stops, auto-size from device LED count), ColorStop Pydantic schema, API create/update/guard routes - Frontend: gradient editor modal (canvas preview, draggable markers, stop rows), CSS hard-edge card swatch, locale keys (en + ru) - Fixes: stop row mousedown no longer rebuilds DOM (buttons now clickable), position input max-width, bidir/remove button static width Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ from wled_controller.core.capture.calibration import (
|
||||
)
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource, StaticColorStripSource
|
||||
from wled_controller.storage.color_strip_source import GradientColorStripSource, PictureColorStripSource, StaticColorStripSource
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
@@ -44,6 +44,16 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
|
||||
if isinstance(source, PictureColorStripSource) and source.calibration:
|
||||
calibration = CalibrationSchema(**calibration_to_dict(source.calibration))
|
||||
|
||||
# Convert raw stop dicts to ColorStop schema objects for gradient sources
|
||||
from wled_controller.api.schemas.color_strip_sources import ColorStop as ColorStopSchema
|
||||
raw_stops = getattr(source, "stops", None)
|
||||
stops = None
|
||||
if raw_stops is not None:
|
||||
try:
|
||||
stops = [ColorStopSchema(**s) for s in raw_stops]
|
||||
except Exception:
|
||||
stops = None
|
||||
|
||||
return ColorStripSourceResponse(
|
||||
id=source.id,
|
||||
name=source.name,
|
||||
@@ -58,6 +68,7 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
|
||||
led_count=getattr(source, "led_count", 0),
|
||||
calibration=calibration,
|
||||
color=getattr(source, "color", None),
|
||||
stops=stops,
|
||||
description=source.description,
|
||||
overlay_active=overlay_active,
|
||||
created_at=source.created_at,
|
||||
@@ -106,6 +117,8 @@ async def create_color_strip_source(
|
||||
if data.calibration is not None:
|
||||
calibration = calibration_from_dict(data.calibration.model_dump())
|
||||
|
||||
stops = [s.model_dump() for s in data.stops] if data.stops is not None else None
|
||||
|
||||
source = store.create_source(
|
||||
name=data.name,
|
||||
source_type=data.source_type,
|
||||
@@ -119,6 +132,7 @@ async def create_color_strip_source(
|
||||
led_count=data.led_count,
|
||||
calibration=calibration,
|
||||
color=data.color,
|
||||
stops=stops,
|
||||
description=data.description,
|
||||
)
|
||||
return _css_to_response(source)
|
||||
@@ -159,6 +173,8 @@ async def update_color_strip_source(
|
||||
if data.calibration is not None:
|
||||
calibration = calibration_from_dict(data.calibration.model_dump())
|
||||
|
||||
stops = [s.model_dump() for s in data.stops] if data.stops is not None else None
|
||||
|
||||
source = store.update_source(
|
||||
source_id=source_id,
|
||||
name=data.name,
|
||||
@@ -172,6 +188,7 @@ async def update_color_strip_source(
|
||||
led_count=data.led_count,
|
||||
calibration=calibration,
|
||||
color=data.color,
|
||||
stops=stops,
|
||||
description=data.description,
|
||||
)
|
||||
|
||||
@@ -258,10 +275,10 @@ async def test_css_calibration(
|
||||
if body.edges:
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if isinstance(source, StaticColorStripSource):
|
||||
if isinstance(source, (StaticColorStripSource, GradientColorStripSource)):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Calibration test is not applicable for static color strip sources",
|
||||
detail="Calibration test is not applicable for this color strip source type",
|
||||
)
|
||||
if isinstance(source, PictureColorStripSource) and source.calibration:
|
||||
calibration = source.calibration
|
||||
@@ -304,8 +321,8 @@ async def start_css_overlay(
|
||||
"""Start screen overlay visualization for a color strip source."""
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if isinstance(source, StaticColorStripSource):
|
||||
raise HTTPException(status_code=400, detail="Overlay is not supported for static color strip sources")
|
||||
if isinstance(source, (StaticColorStripSource, GradientColorStripSource)):
|
||||
raise HTTPException(status_code=400, detail="Overlay is not supported for this color strip source type")
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
raise HTTPException(status_code=400, detail="Overlay only supported for picture color strip sources")
|
||||
if not source.calibration:
|
||||
|
||||
Reference in New Issue
Block a user