Enhance CourtDrawing components with additional target selection and arrow drawing features

Added new target selection buttons for main and additional strokes in CourtDrawingTool.vue, allowing users to explicitly choose target positions. Updated CourtDrawingRender.vue to support drawing up to three additional arrows, alternating between left and right sides, with distinct colors for each stroke. Improved the logic for determining the next stroke side and updated related methods for better clarity and functionality.
This commit is contained in:
Torsten Schulz (local)
2025-10-30 11:09:55 +01:00
parent e23d9fbc44
commit 7ea719c178
2 changed files with 197 additions and 35 deletions

View File

@@ -65,6 +65,8 @@ export default {
arrows: {
primaryColor: '#d32f2f', // rechts -> target (rot)
secondaryColor: '#1565c0', // zurück (blau)
tertiaryColor: '#2e7d32', // dritter Schlag (grün)
quaternaryColor: '#6a1b9a', // vierter Schlag (violett)
width: 6,
headLength: 24,
vhOffsetX: 5,
@@ -336,25 +338,39 @@ export default {
const startCenter = this.getStartCircleCenter();
this.drawLabelBelow(ctx, sourceLabel, startCenter);
}
// Second arrow (optional): from right source to left target
const leftTarget = this.drawingData.nextStrokeTargetPosition ? Number(this.drawingData.nextStrokeTargetPosition) : null;
if (tp && leftTarget) {
// source near previous right target
// Additional arrows (up to 3), alternating left/right, starting from right target
const extras = Array.isArray(this.drawingData.additionalStrokes) ? this.drawingData.additionalStrokes : [];
if (tp && extras.length) {
const colors = [
this.config.arrows.secondaryColor,
this.config.arrows.tertiaryColor,
this.config.arrows.quaternaryColor
];
const max = Math.min(3, extras.length);
const sourceRightCenter = this.computeRightTargetPosition(tp);
// left target mapping: mirror scheme to left half
const toLeftCenter = this.computeLeftTargetPosition(leftTarget);
// Zielmarkierung links
this.drawHitMarker(ctx, toLeftCenter);
const side = this.drawingData.nextStrokeSide || '';
const type = this.drawingData.nextStrokeType || '';
// Text gehört ans Ziel (ohne "extra target")
const targetLabel = `${side} ${type}`.trim();
const sourceRight = { x: sourceRightCenter.x - this.config.targetCircles.radius, y: sourceRightCenter.y };
const toLeft = { x: toLeftCenter.x + this.config.leftTargetCircles.radius, y: toLeftCenter.y };
this.drawArrow(ctx, sourceRight, toLeft, this.config.arrows.secondaryColor);
// Unter dem rechten Ziel (target der ersten Linie) beschriften
this.drawLabelBelow(ctx, targetLabel, sourceRightCenter);
let prevPoint = { x: sourceRightCenter.x - this.config.targetCircles.radius, y: sourceRightCenter.y };
for (let i = 0; i < max; i++) {
const stroke = extras[i];
const side = i % 2 === 0 ? 'left' : 'right';
if (side === 'left') {
const leftNum = Number(stroke.targetPosition);
const toLeftCenter = this.computeLeftTargetPosition(leftNum);
// Zielmarkierung links
this.drawHitMarker(ctx, toLeftCenter);
const toLeft = { x: toLeftCenter.x + this.config.leftTargetCircles.radius, y: toLeftCenter.y };
this.drawArrow(ctx, prevPoint, toLeft, colors[i]);
prevPoint = toLeft; // beginnt an der Pfeilspitze des vorherigen
} else {
const rightNum = Number(stroke.targetPosition);
const toRightCenter = this.computeRightTargetPosition(rightNum);
// Zielmarkierung rechts
this.drawHitMarker(ctx, toRightCenter);
const toRight = { x: toRightCenter.x - this.config.targetCircles.radius, y: toRightCenter.y };
this.drawArrow(ctx, prevPoint, toRight, colors[i]);
prevPoint = toRight;
}
}
}
},
getStartCircleCenter() {

View File

@@ -89,6 +89,22 @@
</div>
</div>
<!-- Zielposition für Hauptschlag: explizite Auswahl 19 als Buttons -->
<div class="target-selection" v-if="spinType">
<span>Zielposition:</span>
<div class="target-buttons">
<button
v-for="n in 9"
:key="`main-target-${n}`"
type="button"
:class="['btn-small', { 'btn-primary': targetPosition === String(n), 'btn-secondary': targetPosition !== String(n) }]"
@click="targetPosition = String(n)"
>
{{ n }}
</button>
</div>
</div>
<!-- Zusätzliche Schläge hinzufügen -->
<div class="additional-strokes" v-if="strokeType && spinType && targetPosition">
@@ -113,6 +129,22 @@
</button>
</div>
<!-- Zielposition für Zusatzschlag: explizite Auswahl 19 als Buttons -->
<div class="next-target-selection">
<span>Zielposition:</span>
<div class="target-buttons">
<button
v-for="n in 9"
:key="`next-target-${n}`"
type="button"
:class="['btn-small', { 'btn-primary': nextStrokeTargetPosition === String(n), 'btn-secondary': nextStrokeTargetPosition !== String(n) }]"
@click="nextStrokeTargetPosition = String(n)"
>
{{ n }}
</button>
</div>
</div>
<div class="next-stroke-buttons">
<button
type="button"
@@ -277,7 +309,7 @@ export default {
textColor: '#000000'
},
arrow: {
color: '#ff0000',
color: '#ff0000', // Hauptschlag immer rot
width: 6,
cap: 'round',
headSize: 8,
@@ -588,9 +620,9 @@ export default {
this.drawLeftSideTargetCircles(ctx, tableX, tableY, tableWidth, tableHeight);
}
// Zusätzlichen Pfeil vom rechten Source zum linken Target zeichnen (wenn linker Target ausgewählt)
if (this.nextStrokeTargetPosition && this.targetPosition) {
this.drawArrowToLeftTarget(ctx, tableX, tableY, tableWidth, tableHeight);
// Zeichne Kette zusätzlicher Schläge (bis zu 3)
if (this.targetPosition) {
this.drawAdditionalArrows(ctx, tableX, tableY, tableWidth, tableHeight);
}
},
@@ -821,6 +853,109 @@ export default {
ctx.stroke();
},
// Ermittelt, welche Tischseite (left/right) als nächstes für Zusatzschlag benutzt wird
getNextAdditionalSide() {
const idx = this.additionalStrokes.length; // 0,1,2
return idx % 2 === 0 ? 'left' : 'right';
},
// Berechne Mittelpunkt eines rechten Zielkreises (1..9)
computeRightTargetCenter(tableX, tableY, tableWidth, tableHeight, number) {
const cfg = this.config.targetCircles;
const x1 = tableX + tableWidth - cfg.rightXOffset;
const x3 = tableX + tableWidth/2 + cfg.middleXOffset;
const xdiff = x3 - x1;
const x2 = x3 - xdiff/2;
const yTop = tableY + cfg.topYOffset;
const yMid = tableY + tableHeight/2;
const yBot = tableY + tableHeight - cfg.bottomYOffset;
const map = {
1: { x: x1, y: yTop }, 2: { x: x1, y: yMid }, 3: { x: x1, y: yBot },
4: { x: x2, y: yTop }, 5: { x: x2, y: yMid }, 6: { x: x2, y: yBot },
7: { x: x3, y: yTop }, 8: { x: x3, y: yMid }, 9: { x: x3, y: yBot }
};
return map[number] || null;
},
// Berechne Mittelpunkt eines linken Zielkreises (1..9 gespiegelt)
computeLeftTargetCenter(tableX, tableY, tableWidth, tableHeight, number) {
const cfg = this.config.leftTargetCircles;
const x1 = tableX + cfg.leftXOffset;
const x3 = tableX + tableWidth / 2 - cfg.rightXOffset;
const xdiff = x3 - x1;
const x2 = x3 - xdiff/2;
const yTop = tableY + cfg.topYOffset;
const yMid = tableY + tableHeight/2;
const yBot = tableY + tableHeight - cfg.bottomYOffset;
const map = {
1: { x: x1, y: yBot }, 2: { x: x1, y: yMid }, 3: { x: x1, y: yTop },
4: { x: x2, y: yBot }, 5: { x: x2, y: yMid }, 6: { x: x2, y: yTop },
7: { x: x3, y: yBot }, 8: { x: x3, y: yMid }, 9: { x: x3, y: yTop }
};
return map[number] || null;
},
// Zeichnet zusätzliche Pfeile abwechselnd rechts/links aus additionalStrokes
drawAdditionalArrows(ctx, tableX, tableY, tableWidth, tableHeight) {
if (!this.additionalStrokes || this.additionalStrokes.length === 0) return;
const max = this.additionalStrokes.length;
// 1: rot, 2: blau, 3: gelb, 4: violett, ab 5: pink
const colors = ['#007bff', '#FFD700', '#6a1b9a', '#ff69b4'];
const rightRadius = this.config.targetCircles.radius;
const leftRadius = this.config.leftTargetCircles.radius;
// Quelle: Ende des ersten Pfeils (rechts)
const firstNum = parseInt(this.targetPosition);
const firstCenter = this.computeRightTargetCenter(tableX, tableY, tableWidth, tableHeight, firstNum);
let prevPoint = { x: firstCenter.x - rightRadius, y: firstCenter.y }; // linke Kante des rechten Kreises
let prevSide = 'right';
for (let i = 0; i < max; i++) {
const stroke = this.additionalStrokes[i];
const side = i % 2 === 0 ? 'left' : 'right';
const targetNum = parseInt(stroke.targetPosition);
// Farben: 2. blau, 3. gelb, 4. violett, ab dem 5. pink
const color = (i < colors.length) ? colors[i] : '#ff69b4';
let toCenter, toPoint;
if (side === 'left') {
toCenter = this.computeLeftTargetCenter(tableX, tableY, tableWidth, tableHeight, targetNum);
toPoint = { x: toCenter.x + leftRadius, y: toCenter.y }; // rechte Kante des linken Kreises
} else {
toCenter = this.computeRightTargetCenter(tableX, tableY, tableWidth, tableHeight, targetNum);
toPoint = { x: toCenter.x - rightRadius, y: toCenter.y }; // linke Kante des rechten Kreises
}
// Linie
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = this.config.arrow.width;
ctx.lineCap = this.config.arrow.cap;
// Nummer
ctx.font = this.config.arrow.counterFont;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const midX = (prevPoint.x + toPoint.x) / 2;
const midY = (prevPoint.y + toPoint.y) / 2 - this.config.arrow.counterOffset;
ctx.fillText(String(i + 2), midX, midY);
// Linie zeichnen
ctx.beginPath();
ctx.moveTo(prevPoint.x, prevPoint.y);
ctx.lineTo(toPoint.x, toPoint.y);
ctx.stroke();
// Pfeilkopf
const angle = Math.atan2(toPoint.y - prevPoint.y, toPoint.x - prevPoint.x);
const sz = this.config.arrow.headSize;
ctx.beginPath();
ctx.moveTo(toPoint.x, toPoint.y);
ctx.lineTo(toPoint.x - sz * Math.cos(angle - Math.PI / 6), toPoint.y - sz * Math.sin(angle - Math.PI / 6));
ctx.moveTo(toPoint.x, toPoint.y);
ctx.lineTo(toPoint.x - sz * Math.cos(angle + Math.PI / 6), toPoint.y - sz * Math.sin(angle + Math.PI / 6));
ctx.stroke();
// nächste Quelle ist die Pfeilspitze dieses Schlages
prevPoint = toPoint;
prevSide = side;
}
},
drawLeftSideTargetCircles(ctx, tableX, tableY, tableWidth, tableHeight) {
const config = this.config.leftTargetCircles;
// Kreise auf der linken Tischhälfte (spiegelbildlich zu rechts)
@@ -885,22 +1020,31 @@ export default {
// Prüfe ob auf eine Zielposition geklickt wurde
const clickedTarget = this.checkTargetPositionClick(clickX, clickY);
if (clickedTarget) {
this.targetPosition = clickedTarget;
// Wenn keine Startposition ausgewählt ist, setze standardmäßig die mittlere Startposition (AS2)
if (!this.selectedStartPosition) {
this.selectedStartPosition = 'AS2';
this.selectedCirclePosition = 'middle';
// Wenn noch kein Hauptziel gesetzt: setze es
if (!this.targetPosition) {
this.targetPosition = clickedTarget;
// Wenn keine Startposition ausgewählt ist, setze standardmäßig die mittlere Startposition (AS2)
if (!this.selectedStartPosition) {
this.selectedStartPosition = 'AS2';
this.selectedCirclePosition = 'middle';
}
this.drawCourt();
this.emitDrawingData();
this.updateTextFields();
return;
}
// Andernfalls: wenn wir einen Zusatzschlag auf die rechte Seite wählen sollen
if (this.targetPosition && this.additionalStrokes.length < 3 && this.getNextAdditionalSide() === 'right') {
this.nextStrokeTargetPosition = clickedTarget;
this.drawCourt();
this.emitDrawingData();
this.updateTextFields();
return;
}
this.drawCourt(); // Neu zeichnen für Zielposition-Hervorhebung
this.emitDrawingData();
this.updateTextFields();
return;
}
// Prüfe ob auf eine linke Zielposition geklickt wurde (nur wenn Schlag für rechte Seite ausgewählt)
if (this.nextStrokeType && this.nextStrokeSide && this.targetPosition) {
// Prüfe ob auf eine linke Zielposition geklickt wurde (nur wenn Zusatzschlag links erwartet)
if (this.nextStrokeType && this.nextStrokeSide && this.targetPosition && this.getNextAdditionalSide() === 'left') {
const clickedLeftTarget = this.checkLeftTargetPositionClick(clickX, clickY);
if (clickedLeftTarget) {
this.nextStrokeTargetPosition = clickedLeftTarget;
@@ -1321,6 +1465,8 @@ export default {
// Counter erhöhen
this.exerciseCounter++;
// Auswahl für nächsten Zusatzschlag zurücksetzen
this.nextStrokeTargetPosition = '';
// Textfelder aktualisieren
this.updateTextFields();