mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-03-11 17:48:44 -05:00
feat(gantt): create arrow SVG overlay component for relations
This commit is contained in:
131
frontend/src/components/gantt/GanttRelationArrows.vue
Normal file
131
frontend/src/components/gantt/GanttRelationArrows.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<svg
|
||||
class="gantt-relation-arrows"
|
||||
:width="width"
|
||||
:height="height"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<marker
|
||||
id="arrowhead-danger"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
refX="6"
|
||||
refY="4"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0,0 8,4 0,8"
|
||||
fill="var(--danger)"
|
||||
/>
|
||||
</marker>
|
||||
<marker
|
||||
id="arrowhead-grey"
|
||||
markerWidth="8"
|
||||
markerHeight="8"
|
||||
refX="6"
|
||||
refY="4"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0,0 8,4 0,8"
|
||||
fill="var(--grey-500)"
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
v-for="(arrow, index) in arrows"
|
||||
:key="`arrow-${index}`"
|
||||
:d="computePath(arrow)"
|
||||
:stroke="arrow.color"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
:marker-end="getMarkerEnd(arrow)"
|
||||
class="gantt-arrow"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {GanttArrow} from '@/helpers/ganttRelationArrows'
|
||||
|
||||
defineProps<{
|
||||
arrows: GanttArrow[]
|
||||
width: number
|
||||
height: number
|
||||
rowHeight: number
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Computes a bezier curve path for an arrow.
|
||||
* Uses horizontal bezier curves that curve around obstacles.
|
||||
*/
|
||||
function computePath(arrow: GanttArrow): string {
|
||||
const {startX, startY, endX, endY} = arrow
|
||||
|
||||
// Horizontal distance
|
||||
const dx = endX - startX
|
||||
const dy = endY - startY
|
||||
|
||||
// Control point offset (how much the curve bends)
|
||||
const cpOffset = Math.min(Math.abs(dx) * 0.4, 60)
|
||||
|
||||
if (dx >= 0) {
|
||||
// Target is to the right - simple S-curve
|
||||
const cp1x = startX + cpOffset
|
||||
const cp1y = startY
|
||||
const cp2x = endX - cpOffset
|
||||
const cp2y = endY
|
||||
|
||||
return `M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`
|
||||
} else {
|
||||
// Target is to the left - need to route around
|
||||
// Go right first, then down/up, then left to target
|
||||
const routeOffset = 30
|
||||
const midY = startY + (dy > 0 ? routeOffset : -routeOffset)
|
||||
|
||||
const cp1x = startX + routeOffset
|
||||
const cp1y = startY
|
||||
const cp2x = startX + routeOffset
|
||||
const cp2y = midY
|
||||
|
||||
const cp3x = endX - routeOffset
|
||||
const cp3y = midY
|
||||
const cp4x = endX - routeOffset
|
||||
const cp4y = endY
|
||||
|
||||
return `M ${startX} ${startY} ` +
|
||||
`C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${startX + routeOffset} ${midY} ` +
|
||||
`L ${endX - routeOffset} ${midY} ` +
|
||||
`C ${cp3x} ${cp3y}, ${cp4x} ${cp4y}, ${endX} ${endY}`
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkerEnd(arrow: GanttArrow): string {
|
||||
return arrow.relationKind === 'blocking'
|
||||
? 'url(#arrowhead-danger)'
|
||||
: 'url(#arrowhead-grey)'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gantt-relation-arrows {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.gantt-arrow {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user