knxdisplay/web-interface/src/components/widgets/elements/PowerFlowElement.vue
2026-02-04 21:11:04 +01:00

206 lines
7.0 KiB
Vue

<template>
<div
class="z-[1] select-none touch-none"
:class="selected ? 'outline outline-2 outline-accent outline-offset-2' : ''"
:style="computedStyle"
@mousedown.stop="$emit('drag-start', { id: widget.id, event: $event })"
@touchstart.stop="$emit('drag-start', { id: widget.id, event: $event })"
@click.stop="$emit('select')"
>
<div class="absolute inset-0" :style="powerFlowBgStyle"></div>
<div v-if="widget.text" class="absolute left-4 top-3 text-[13px] uppercase tracking-[0.08em]" :style="{ color: widget.textColor }">
{{ widget.text }}
</div>
<svg class="absolute inset-0 pointer-events-none" :width="widget.w * scale" :height="widget.h * scale">
<path
v-for="link in powerFlowLinks"
:key="`link-${link.id}`"
:d="link.path"
:stroke="link.color"
:stroke-width="link.width"
stroke-linecap="round"
fill="none"
:opacity="link.opacity"
/>
<circle
v-for="link in powerFlowLinks"
:key="`dot-${link.id}`"
:r="link.dotRadius"
:fill="link.color"
:opacity="link.opacity"
>
<animateMotion
:dur="`${link.duration}s`"
repeatCount="indefinite"
:path="link.path"
/>
</circle>
</svg>
<div v-if="!powerNodes.length" class="absolute inset-0 grid place-items-center text-[12px] text-muted">
Power Nodes hinzufuegen
</div>
<WidgetElement
v-for="child in powerFlowChildren"
:key="child.id"
:widget="child"
:scale="scale"
:selected="store.selectedWidgetId === child.id"
@select="store.selectedWidgetId = child.id"
@drag-start="$emit('drag-start', $event)"
@resize-start="$emit('resize-start', $event)"
/>
<!-- Resize Handle -->
<div
v-if="selected"
class="absolute -right-1.5 -bottom-1.5 w-3.5 h-3.5 rounded-[4px] bg-accent border-[2px] border-[#1b1308] shadow-[0_4px_12px_rgba(0,0,0,0.35)] cursor-se-resize z-10"
data-resize-handle
@mousedown.stop="$emit('resize-start', { id: widget.id, event: $event })"
@touchstart.stop="$emit('resize-start', { id: widget.id, event: $event })"
>
<span class="absolute right-[2px] bottom-[2px] w-[6px] h-[6px] border-r-2 border-b-2 border-[#1b1308a6] rounded-[2px]"></span>
</div>
</div>
</template>
<script setup>
import { computed, defineAsyncComponent } from 'vue';
import { useEditorStore } from '../../../stores/editor';
import { WIDGET_TYPES } from '../../../constants';
import { getBaseStyle, getShadowStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
const WidgetElement = defineAsyncComponent(() => import('../../WidgetElement.vue'));
const props = defineProps({
widget: { type: Object, required: true },
scale: { type: Number, default: 1 },
selected: { type: Boolean, default: false }
});
defineEmits(['select', 'drag-start', 'resize-start']);
const store = useEditorStore();
const children = computed(() => {
if (!store.activeScreen) return [];
return store.activeScreen.widgets.filter(w => w.parentId === props.widget.id);
});
const powerNodes = computed(() => {
return children.value.filter((child) => child.type === WIDGET_TYPES.POWERNODE && child.visible !== false);
});
const powerLinkWidgets = computed(() => {
return children.value.filter((child) => child.type === WIDGET_TYPES.POWERLINK && child.visible !== false);
});
const powerFlowChildren = computed(() => {
return children.value.filter((child) => child.type !== WIDGET_TYPES.POWERLINK);
});
const powerFlowLinks = computed(() => {
if (powerNodes.value.length < 2 || !powerLinkWidgets.value.length) return [];
const s = props.scale;
const nodeMap = new Map(powerNodes.value.map((node) => [node.id, node]));
return powerLinkWidgets.value.flatMap((link, idx) => {
const fromNode = nodeMap.get(link.x);
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
};
const toCenter = {
x: (toNode.x + toNode.w / 2) * s,
y: (toNode.y + toNode.h / 2) * s
};
const dx = toCenter.x - fromCenter.x;
const dy = toCenter.y - fromCenter.y;
const len = Math.hypot(dx, dy) || 1;
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, lenTrim * 0.3) * curveSign;
const cpx = midX + nx * curve;
const cpy = midY + ny * curve;
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 ${startX} ${startY} Q ${cpx} ${cpy} ${endX} ${endY}`,
color: link.bgColor || '#6fa7d8',
opacity: clamp((link.bgOpacity ?? 255) / 255, 0.1, 1),
width: lineWidth,
dotRadius,
duration
}];
});
});
const powerFlowBgStyle = computed(() => {
const alpha = clamp((props.widget.bgOpacity ?? 255) / 255, 0, 1);
const base = hexToRgba(props.widget.bgColor, alpha);
const dot = hexToRgba('#9aa7b4', 0.25);
const dotSize = 16 * props.scale;
return {
backgroundColor: base,
backgroundImage: `radial-gradient(${dot} 0.9px, transparent 1px), linear-gradient(140deg, rgba(255,255,255,0.9) 0%, ${base} 70%)`,
backgroundSize: `${dotSize}px ${dotSize}px, 100% 100%`,
backgroundPosition: '0 0, 0 0'
};
});
const computedStyle = computed(() => {
const w = props.widget;
const s = props.scale;
const style = getBaseStyle(w, s);
style.borderRadius = `${w.radius * s}px`;
style.overflow = 'hidden';
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
Object.assign(style, getBorderStyle(w, s));
} else {
style.border = `1px solid ${hexToRgba('#94a3b8', 0.35)}`;
}
Object.assign(style, getShadowStyle(w, s));
return style;
});
</script>