From 823cb90d2db295dc34fca55c85aa2dfd3ef82d6a Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Tue, 17 Mar 2026 02:09:29 +0300 Subject: [PATCH] Show captured border width overlay in picture CSS test preview Backend: send border_width in WS metadata and frame_dims (width, height) as a separate JSON message on first JPEG frame. Frontend: render semi-transparent green overlay rectangles on each active edge showing the sampling region depth, plus a small px label. Overlays are proportionally sized based on border_width relative to frame dimensions. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/routes/color_strip_sources.py | 12 +++- .../src/wled_controller/static/css/modal.css | 13 ++++ .../static/js/features/color-strips.js | 61 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/server/src/wled_controller/api/routes/color_strip_sources.py b/server/src/wled_controller/api/routes/color_strip_sources.py index a617fcf..eba060e 100644 --- a/server/src/wled_controller/api/routes/color_strip_sources.py +++ b/server/src/wled_controller/api/routes/color_strip_sources.py @@ -822,6 +822,7 @@ async def test_color_strip_ws( indices = [(idx + offset) % total for idx in indices] edges.append({"edge": seg.edge, "indices": indices}) meta["edges"] = edges + meta["border_width"] = cal.border_width if is_composite and hasattr(source, "layers"): # Send layer info for composite preview enabled_layers = [l for l in source.layers if l.get("enabled", True)] @@ -850,6 +851,7 @@ async def test_color_strip_ws( _frame_live = stream.live_stream _last_aux_time = 0.0 _AUX_INTERVAL = 0.08 # send JPEG preview / brightness updates ~12 FPS + _frame_dims_sent = False # send frame dimensions once with first JPEG # Stream binary RGB frames at ~20 Hz while True: @@ -904,8 +906,16 @@ async def test_color_strip_ws( # Ensure 3-channel RGB (some engines may produce BGRA) if img.ndim == 3 and img.shape[2] == 4: img = img[:, :, :3] - # Downscale for bandwidth h, w = img.shape[:2] + # Send frame dimensions once so client can compute border overlay + if not _frame_dims_sent: + _frame_dims_sent = True + await websocket.send_text(_json.dumps({ + "type": "frame_dims", + "width": w, + "height": h, + })) + # Downscale for bandwidth scale = min(960 / w, 540 / h, 1.0) if scale < 1.0: new_w = max(1, int(w * scale)) diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index edf8881..bff7d9f 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -215,6 +215,19 @@ z-index: 10; } +.css-test-border-label { + position: absolute; + bottom: 4px; + right: 4px; + background: rgba(0, 0, 0, 0.6); + color: var(--primary-color); + font-size: 0.65rem; + font-family: monospace; + padding: 1px 5px; + border-radius: 3px; + z-index: 5; +} + .css-test-rect-label { color: rgba(255, 255, 255, 0.85); font-size: 0.8rem; diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js index d448476..a0b3d7d 100644 --- a/server/src/wled_controller/static/js/features/color-strips.js +++ b/server/src/wled_controller/static/js/features/color-strips.js @@ -2612,6 +2612,12 @@ function _cssTestConnect(sourceId, ledCount, fps) { return; } + // Handle frame dimensions — render border-width overlay + if (msg.type === 'frame_dims' && _cssTestMeta) { + _cssTestRenderBorderOverlay(msg.width, msg.height); + return; + } + // Initial metadata _cssTestMeta = msg; const isPicture = _cssTestMeta.edges && _cssTestMeta.edges.length > 0; @@ -2913,6 +2919,61 @@ function _cssTestRenderRect(rgbBytes, edges) { } } +function _cssTestRenderBorderOverlay(frameW, frameH) { + const screen = document.getElementById('css-test-rect-screen'); + if (!screen || !_cssTestMeta) return; + + // Remove any previous border overlay + screen.querySelectorAll('.css-test-border-overlay').forEach(el => el.remove()); + + const bw = _cssTestMeta.border_width; + if (!bw || bw <= 0) return; + + const edges = _cssTestMeta.edges || []; + const activeEdges = new Set(edges.map(e => e.edge)); + + // Compute border as percentage of frame dimensions + const bwPctH = (bw / frameH * 100).toFixed(2); // % for top/bottom + const bwPctW = (bw / frameW * 100).toFixed(2); // % for left/right + + const overlayStyle = 'position:absolute;pointer-events:none;background:rgba(var(--primary-color-rgb, 76,175,80),0.18);border:1px solid rgba(var(--primary-color-rgb, 76,175,80),0.4);'; + + if (activeEdges.has('top')) { + const el = document.createElement('div'); + el.className = 'css-test-border-overlay'; + el.style.cssText = `${overlayStyle}top:0;left:0;right:0;height:${bwPctH}%;`; + el.title = `${t('calibration.border_width')} ${bw}px`; + screen.appendChild(el); + } + if (activeEdges.has('bottom')) { + const el = document.createElement('div'); + el.className = 'css-test-border-overlay'; + el.style.cssText = `${overlayStyle}bottom:0;left:0;right:0;height:${bwPctH}%;`; + el.title = `${t('calibration.border_width')} ${bw}px`; + screen.appendChild(el); + } + if (activeEdges.has('left')) { + const el = document.createElement('div'); + el.className = 'css-test-border-overlay'; + el.style.cssText = `${overlayStyle}top:0;bottom:0;left:0;width:${bwPctW}%;`; + el.title = `${t('calibration.border_width')} ${bw}px`; + screen.appendChild(el); + } + if (activeEdges.has('right')) { + const el = document.createElement('div'); + el.className = 'css-test-border-overlay'; + el.style.cssText = `${overlayStyle}top:0;bottom:0;right:0;width:${bwPctW}%;`; + el.title = `${t('calibration.border_width')} ${bw}px`; + screen.appendChild(el); + } + + // Show border width label + const label = document.createElement('div'); + label.className = 'css-test-border-overlay css-test-border-label'; + label.textContent = `${bw}px`; + screen.appendChild(label); +} + function _cssTestRenderTicks(edges) { const canvas = document.getElementById('css-test-rect-ticks'); const rectEl = document.getElementById('css-test-rect');