From 75a7e18913d70c1f0685bf7d52c6a6576dc80f1a Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Wed, 28 Jan 2026 16:52:05 +0100 Subject: [PATCH] Fixes --- main/widgets/PowerLinkWidget.cpp | 237 ++++++++++++++++-- main/widgets/PowerLinkWidget.hpp | 20 +- web-interface/src/components/SidebarRight.vue | 44 +++- .../src/components/WidgetElement.vue | 62 +++-- web-interface/src/constants.js | 5 +- 5 files changed, 322 insertions(+), 46 deletions(-) diff --git a/main/widgets/PowerLinkWidget.cpp b/main/widgets/PowerLinkWidget.cpp index 95cb978..6beb15a 100644 --- a/main/widgets/PowerLinkWidget.cpp +++ b/main/widgets/PowerLinkWidget.cpp @@ -1,13 +1,33 @@ #include "PowerLinkWidget.hpp" #include "../WidgetManager.hpp" +#include +#include PowerLinkWidget::PowerLinkWidget(const WidgetConfig& config) : Widget(config) { - points_[0].x = 0; - points_[0].y = 0; - points_[1].x = 1; - points_[1].y = 1; + for (uint8_t i = 0; i < POINT_COUNT; i++) { + points_[i].x = 0; + points_[i].y = 0; + } +} + +PowerLinkWidget::~PowerLinkWidget() { + lv_anim_del(this, nullptr); + if (dot_ != nullptr) { + if (lv_obj_is_valid(dot_)) { + lv_obj_delete(dot_); + } + dot_ = nullptr; + } +} + +static float parseFloatOr(const char* text, float fallback) { + if (text == nullptr || text[0] == '\0') return fallback; + char* end = nullptr; + float value = std::strtof(text, &end); + if (end == text || !std::isfinite(value)) return fallback; + return value; } void PowerLinkWidget::buildLinePoints() { @@ -22,24 +42,101 @@ void PowerLinkWidget::buildLinePoints() { if (fromNode == nullptr || toNode == nullptr) return; if (fromNode->parentId != config_.parentId || toNode->parentId != config_.parentId) return; - int16_t x1 = fromNode->x + fromNode->width / 2; - int16_t y1 = fromNode->y + fromNode->height / 2; - int16_t x2 = toNode->x + toNode->width / 2; - int16_t y2 = toNode->y + toNode->height / 2; + float x1 = static_cast(fromNode->x) + static_cast(fromNode->width) * 0.5f; + float y1 = static_cast(fromNode->y) + static_cast(fromNode->height) * 0.5f; + float x2 = static_cast(toNode->x) + static_cast(toNode->width) * 0.5f; + float y2 = static_cast(toNode->y) + static_cast(toNode->height) * 0.5f; - int16_t minX = x1 < x2 ? x1 : x2; - int16_t minY = y1 < y2 ? y1 : y2; - int16_t maxX = x1 > x2 ? x1 : x2; - int16_t maxY = y1 > y2 ? y1 : y2; + float dx = x2 - x1; + float dy = y2 - y1; + float len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) return; - points_[0].x = x1 - minX; - points_[0].y = y1 - minY; - points_[1].x = x2 - minX; - points_[1].y = y2 - minY; + float lineWidth = config_.width > 0 ? static_cast(config_.width) : 3.0f; + if (lineWidth < 3.0f) lineWidth = 3.0f; + int16_t minSideA = fromNode->width < fromNode->height ? fromNode->width : fromNode->height; + int16_t minSideB = toNode->width < toNode->height ? toNode->width : toNode->height; + float radiusA = static_cast(minSideA) * 0.5f - lineWidth * 0.5f; + float radiusB = static_cast(minSideB) * 0.5f - lineWidth * 0.5f; + if (radiusA < 0.0f) radiusA = 0.0f; + if (radiusB < 0.0f) radiusB = 0.0f; + + float ux = dx / len; + float uy = dy / len; + float startX = x1 + ux * radiusA; + float startY = y1 + uy * radiusA; + float endX = x2 - ux * radiusB; + float endY = y2 - uy * radiusB; + + if (len <= radiusA + radiusB + 1.0f) { + startX = x1; + startY = y1; + endX = x2; + endY = y2; + } + + float dxTrim = endX - startX; + float dyTrim = endY - startY; + float lenTrim = std::sqrt(dxTrim * dxTrim + dyTrim * dyTrim); + if (lenTrim < 0.001f) lenTrim = len; + + float nx = -dyTrim / lenTrim; + float ny = dxTrim / lenTrim; + float midX = (startX + endX) * 0.5f; + float midY = (startY + endY) * 0.5f; + float curve = std::fmin(40.0f, lenTrim * 0.25f); + float curveSign = (config_.iconPosition == 2) ? -1.0f : 1.0f; + if (config_.iconPosition == 0) { + curveSign = (config_.id % 2 == 0) ? 1.0f : -1.0f; + } else if (config_.iconPosition == 1) { + curveSign = 1.0f; + } else if (config_.iconPosition == 3) { + curveSign = -1.0f; + } + + p0x_ = startX; + p0y_ = startY; + p1x_ = midX + nx * curve * curveSign; + p1y_ = midY + ny * curve * curveSign; + p2x_ = endX; + p2y_ = endY; + pathLen_ = lenTrim; + + float minXf = p0x_; + float minYf = p0y_; + float maxXf = p0x_; + float maxYf = p0y_; + + for (uint8_t i = 0; i < POINT_COUNT; i++) { + float t = static_cast(i) / static_cast(POINT_COUNT - 1); + float inv = 1.0f - t; + float x = inv * inv * p0x_ + 2.0f * inv * t * p1x_ + t * t * p2x_; + float y = inv * inv * p0y_ + 2.0f * inv * t * p1y_ + t * t * p2y_; + if (x < minXf) minXf = x; + if (y < minYf) minYf = y; + if (x > maxXf) maxXf = x; + if (y > maxYf) maxYf = y; + } + + int32_t minX = static_cast(std::floor(minXf)); + int32_t minY = static_cast(std::floor(minYf)); + int32_t maxX = static_cast(std::ceil(maxXf)); + int32_t maxY = static_cast(std::ceil(maxYf)); + + for (uint8_t i = 0; i < POINT_COUNT; i++) { + float t = static_cast(i) / static_cast(POINT_COUNT - 1); + float inv = 1.0f - t; + float x = inv * inv * p0x_ + 2.0f * inv * t * p1x_ + t * t * p2x_; + float y = inv * inv * p0y_ + 2.0f * inv * t * p1y_ + t * t * p2y_; + points_[i].x = x - minX; + points_[i].y = y - minY; + } lv_obj_set_pos(obj_, minX, minY); lv_obj_set_size(obj_, (maxX - minX) + 1, (maxY - minY) + 1); - lv_line_set_points(obj_, points_, 2); + lv_line_set_points(obj_, points_, POINT_COUNT); + + dotAnimExec(this, 0); } lv_obj_t* PowerLinkWidget::create(lv_obj_t* parent) { @@ -49,19 +146,117 @@ lv_obj_t* PowerLinkWidget::create(lv_obj_t* parent) { } lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE); + lv_obj_move_to_index(obj_, 0); buildLinePoints(); + + dot_ = lv_obj_create(parent); + if (dot_ != nullptr) { + lv_obj_remove_style_all(dot_); + lv_obj_clear_flag(dot_, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(dot_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_to_index(dot_, 1); + } + startDotAnimation(); return obj_; } void PowerLinkWidget::applyStyle() { if (obj_ == nullptr) return; + uint16_t lineWidth = config_.width > 0 ? config_.width : 3; + if (lineWidth < 3) lineWidth = 3; + lv_obj_set_style_line_color(obj_, lv_color_make( config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); - lv_obj_set_style_line_width(obj_, config_.width > 0 ? config_.width : 2, 0); + lv_obj_set_style_line_width(obj_, lineWidth, 0); lv_obj_set_style_line_opa(obj_, config_.bgOpacity, 0); + lv_obj_set_style_line_dash_width(obj_, 0, 0); + lv_obj_set_style_line_dash_gap(obj_, 0, 0); - int32_t dash = config_.iconGap > 0 ? config_.iconGap : 6; - lv_obj_set_style_line_dash_width(obj_, dash, 0); - lv_obj_set_style_line_dash_gap(obj_, dash + 4, 0); + if (dot_ != nullptr) { + uint16_t dotSize = static_cast(lineWidth * 3); + if (dotSize < 6) dotSize = 6; + if (dotSize > 16) dotSize = 16; + dotRadius_ = dotSize * 0.5f; + + lv_obj_set_size(dot_, dotSize, dotSize); + lv_obj_set_style_radius(dot_, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(dot_, lv_color_make( + config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); + lv_obj_set_style_bg_opa(dot_, config_.bgOpacity, 0); + dotAnimExec(this, 0); + } +} + +void PowerLinkWidget::startDotAnimation() { + if (dot_ == nullptr || obj_ == nullptr) return; + + if (config_.textSource == TextSource::STATIC) { + float speed = parseFloatOr(config_.text, 60.0f); + updateAnimation(speed, false); + } else { + updateAnimation(0.0f, false); + } +} + +void PowerLinkWidget::dotAnimExec(void* var, int32_t value) { + auto* self = static_cast(var); + if (self == nullptr || self->dot_ == nullptr) return; + float t = static_cast(value) / 1000.0f; + if (self->reverse_) { + t = 1.0f - t; + } + float inv = 1.0f - t; + float x = inv * inv * self->p0x_ + 2.0f * inv * t * self->p1x_ + t * t * self->p2x_; + float y = inv * inv * self->p0y_ + 2.0f * inv * t * self->p1y_ + t * t * self->p2y_; + lv_obj_set_pos(self->dot_, + static_cast(x - self->dotRadius_), + static_cast(y - self->dotRadius_)); +} + +void PowerLinkWidget::updateAnimation(float speed, bool reverse) { + if (dot_ == nullptr || obj_ == nullptr) return; + speed_ = speed; + reverse_ = reverse; + + lv_anim_del(this, nullptr); + + if (speed_ <= 0.01f || pathLen_ <= 0.5f) { + lv_obj_add_flag(dot_, LV_OBJ_FLAG_HIDDEN); + return; + } + + lv_obj_clear_flag(dot_, LV_OBJ_FLAG_HIDDEN); + + int32_t duration = static_cast((pathLen_ / speed_) * 1000.0f); + if (duration < 800) duration = 800; + if (duration > 12000) duration = 12000; + + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_var(&anim, this); + lv_anim_set_exec_cb(&anim, PowerLinkWidget::dotAnimExec); + lv_anim_set_values(&anim, 0, 1000); + lv_anim_set_repeat_count(&anim, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&anim, lv_anim_path_linear); + lv_anim_set_time(&anim, duration); + lv_anim_start(&anim); + + dotAnimExec(this, 0); +} + +void PowerLinkWidget::onKnxValue(float value) { + if (config_.textSource != TextSource::KNX_DPT_TEMP && + config_.textSource != TextSource::KNX_DPT_PERCENT) return; + + float factor = parseFloatOr(config_.text, 1.0f); + float speed = std::fabs(value) * factor; + updateAnimation(speed, value < 0.0f); +} + +void PowerLinkWidget::onKnxSwitch(bool value) { + if (config_.textSource != TextSource::KNX_DPT_SWITCH) return; + float factor = parseFloatOr(config_.text, 1.0f); + float speed = value ? factor : 0.0f; + updateAnimation(speed, false); } diff --git a/main/widgets/PowerLinkWidget.hpp b/main/widgets/PowerLinkWidget.hpp index 167fca0..19cf21c 100644 --- a/main/widgets/PowerLinkWidget.hpp +++ b/main/widgets/PowerLinkWidget.hpp @@ -5,10 +5,28 @@ class PowerLinkWidget : public Widget { public: explicit PowerLinkWidget(const WidgetConfig& config); + ~PowerLinkWidget() override; lv_obj_t* create(lv_obj_t* parent) override; void applyStyle() override; + void onKnxValue(float value) override; + void onKnxSwitch(bool value) override; private: - lv_point_precise_t points_[2] = {}; + static constexpr uint8_t POINT_COUNT = 16; + lv_point_precise_t points_[POINT_COUNT] = {}; + lv_obj_t* dot_ = nullptr; + float p0x_ = 0.0f; + float p0y_ = 0.0f; + float p1x_ = 0.0f; + float p1y_ = 0.0f; + float p2x_ = 0.0f; + float p2y_ = 0.0f; + float pathLen_ = 0.0f; + float dotRadius_ = 3.0f; + float speed_ = 0.0f; + bool reverse_ = false; void buildLinePoints(); + void startDotAnimation(); + void updateAnimation(float speed, bool reverse); + static void dotAnimExec(void* var, int32_t value); }; diff --git a/web-interface/src/components/SidebarRight.vue b/web-interface/src/components/SidebarRight.vue index f1d8ae7..f865059 100644 --- a/web-interface/src/components/SidebarRight.vue +++ b/web-interface/src/components/SidebarRight.vue @@ -131,12 +131,44 @@
{{ linkModeHint }}
-
- {{ link.fromLabel }} - -> - {{ link.toLabel }} - - +
+
+ {{ link.fromLabel }} + -> + {{ link.toLabel }} + +
+
+ + + + px +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
Keine Verbindungen.
diff --git a/web-interface/src/components/WidgetElement.vue b/web-interface/src/components/WidgetElement.vue index f056504..5c8a779 100644 --- a/web-interface/src/components/WidgetElement.vue +++ b/web-interface/src/components/WidgetElement.vue @@ -53,19 +53,22 @@ :stroke="link.color" :stroke-width="link.width" stroke-linecap="round" - :stroke-dasharray="`${link.dash} ${link.dash + 4}`" fill="none" :opacity="link.opacity" /> + > + +
Power Nodes hinzufuegen @@ -345,6 +348,7 @@ const powerFlowLinks = computed(() => { const toNode = nodeMap.get(link.y); if (!fromNode || !toNode) return []; + const lineWidth = Math.max(3, link.w || 3); const fromCenter = { x: (fromNode.x + fromNode.w / 2) * s, y: (fromNode.y + fromNode.h / 2) * s @@ -357,25 +361,51 @@ const powerFlowLinks = computed(() => { const dx = toCenter.x - fromCenter.x; const dy = toCenter.y - fromCenter.y; const len = Math.hypot(dx, dy) || 1; - const nx = -dy / len; - const ny = dx / len; - const midX = (fromCenter.x + toCenter.x) / 2; - const midY = (fromCenter.y + toCenter.y) / 2; + const ux = dx / len; + const uy = dy / len; + const fromRadius = Math.max(0, (Math.min(fromNode.w, fromNode.h) * 0.5 - lineWidth * 0.5) * s); + const toRadius = Math.max(0, (Math.min(toNode.w, toNode.h) * 0.5 - lineWidth * 0.5) * s); + + let startX = fromCenter.x + ux * fromRadius; + let startY = fromCenter.y + uy * fromRadius; + let endX = toCenter.x - ux * toRadius; + let endY = toCenter.y - uy * toRadius; + + if (len <= fromRadius + toRadius + 1) { + startX = fromCenter.x; + startY = fromCenter.y; + endX = toCenter.x; + endY = toCenter.y; + } + + const dxTrim = endX - startX; + const dyTrim = endY - startY; + const lenTrim = Math.hypot(dxTrim, dyTrim) || 1; + const nx = -dyTrim / lenTrim; + const ny = dxTrim / lenTrim; + const midX = (startX + endX) / 2; + const midY = (startY + endY) / 2; const curveSign = idx % 2 === 0 ? 1 : -1; - const curve = Math.min(42 * s, len * 0.3) * curveSign; + const curve = Math.min(42 * s, lenTrim * 0.3) * curveSign; const cpx = midX + nx * curve; const cpy = midY + ny * curve; - const dash = Math.max(2, link.iconGap || 6); + const dotRadius = Math.min(8, Math.max(4, lineWidth * 1.6)); + const rawValue = parseFloat(link.text); + const hasRaw = Number.isFinite(rawValue); + const isStatic = (link.textSrc ?? 0) === 0; + const factor = hasRaw ? rawValue : (isStatic ? 60 : 1); + const previewValue = 50; + const speed = Math.max(5, isStatic ? factor : Math.abs(previewValue) * factor); + const duration = Math.max(2, Math.min(10, lenTrim / speed)); return [{ id: link.id, - path: `M ${fromCenter.x} ${fromCenter.y} Q ${cpx} ${cpy} ${toCenter.x} ${toCenter.y}`, + path: `M ${startX} ${startY} Q ${cpx} ${cpy} ${endX} ${endY}`, color: link.bgColor || '#6fa7d8', opacity: clamp((link.bgOpacity ?? 255) / 255, 0.1, 1), - width: Math.max(1, link.w || 2), - dash, - dotX: midX + nx * curve * 0.5, - dotY: midY + ny * curve * 0.5 + width: lineWidth, + dotRadius, + duration }]; }); }); diff --git a/web-interface/src/constants.js b/web-interface/src/constants.js index 3959d2e..848de3c 100644 --- a/web-interface/src/constants.js +++ b/web-interface/src/constants.js @@ -71,7 +71,8 @@ export const sourceOptions = { button: [0], led: [0, 2], icon: [0, 2], - powernode: [0, 1, 2, 3, 4] + powernode: [0, 1, 2, 3, 4], + powerlink: [0, 1, 3] }; export const ICON_DEFAULTS = { @@ -269,7 +270,7 @@ export const WIDGET_DEFAULTS = { iconGap: 8 }, powerlink: { - w: 2, + w: 3, h: 0, text: '', textSrc: 0,