From d70a5ca63e0c7e0f7eafae9cefae4719f8e21592 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 22 Sep 2025 12:23:39 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Erweitert=20die=20Funktionalit=C3=A4t=20in?= =?UTF-8?q?=20PredefinedActivities.vue=20um=20die=20M=C3=B6glichkeit,=20ei?= =?UTF-8?q?ne=20=C3=9Cbungszeichnung=20zu=20erstellen.=20F=C3=BCgt=20ein?= =?UTF-8?q?=20Zeichen-Tool=20hinzu,=20das=20die=20Zeichnungsdaten=20speich?= =?UTF-8?q?ert=20und=20automatisch=20als=20Bild-Link=20verwendet,=20wenn?= =?UTF-8?q?=20kein=20Bild-Link=20vorhanden=20ist.=20Aktualisiert=20die=20B?= =?UTF-8?q?enutzeroberfl=C3=A4che=20zur=20Bild-=20und=20Zeichnungshinzuf?= =?UTF-8?q?=C3=BCgung.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/CourtDrawingTool.vue | 1305 ++++++++++++++++++ frontend/src/views/PredefinedActivities.vue | 47 +- 2 files changed, 1351 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/CourtDrawingTool.vue diff --git a/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue new file mode 100644 index 0000000..dabc61b --- /dev/null +++ b/frontend/src/components/CourtDrawingTool.vue @@ -0,0 +1,1305 @@ + + + + + diff --git a/frontend/src/views/PredefinedActivities.vue b/frontend/src/views/PredefinedActivities.vue index 2ceb3b5..b9aee47 100644 --- a/frontend/src/views/PredefinedActivities.vue +++ b/frontend/src/views/PredefinedActivities.vue @@ -56,7 +56,7 @@

Bild hinzufügen

-

Du kannst entweder einen Link zu einem Bild eingeben oder ein Bild hochladen:

+

Du kannst entweder einen Link zu einem Bild eingeben, ein Bild hochladen oder eine Übungszeichnung erstellen:

+ +
+
Übungszeichnung erstellen
+ +
+
Hochgeladene Bilder:
@@ -97,9 +107,13 @@ + + + + diff --git a/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue index 736592a..065ecc9 100644 --- a/frontend/src/components/CourtDrawingTool.vue +++ b/frontend/src/components/CourtDrawingTool.vue @@ -588,8 +588,12 @@ export default { this.drawArrowToTarget(ctx, tableX, tableY, tableWidth, tableHeight); } - // Kreise für zusätzliche Schläge auf der linken Seite anzeigen (nur nach Schlag-Auswahl für rechte Seite) - if (this.nextStrokeType && this.nextStrokeSide && this.targetPosition) { + // Kreise für zusätzliche Schläge auf der linken Seite anzeigen + // Sollen bereits erscheinen, sobald rechts alles gewählt ist (Aufschlag-Seite): + // also wenn strokeType, spinType und targetPosition gesetzt sind. + // Zusätzlich weiterhin, wenn bereits nextStrokeType/Side gewählt wurden. + if ((this.strokeType && this.spinType && this.targetPosition) || + (this.nextStrokeType && this.nextStrokeSide && this.targetPosition)) { this.drawLeftSideTargetCircles(ctx, tableX, tableY, tableWidth, tableHeight); } diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index ecd4a33..fb8cc55 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -126,10 +126,11 @@ - 🖼️ + + 🖼️ {{ (item.predefinedActivity && item.predefinedActivity.code && item.predefinedActivity.code.trim() !== '') ? item.predefinedActivity.code : (item.predefinedActivity ? item.predefinedActivity.name : item.activity) }} @@ -161,10 +162,12 @@ - 🖼️ + + 🖼️ + {{ (groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.code && groupItem.groupPredefinedActivity.code.trim() !== '') ? groupItem.groupPredefinedActivity.code : groupItem.groupPredefinedActivity.name }} @@ -319,8 +322,13 @@
-
- +
+ +
@@ -397,10 +405,11 @@ import apiClient from '../apiClient.js'; import Multiselect from 'vue-multiselect'; import Sortable from 'sortablejs'; import PDFGenerator from '../components/PDFGenerator.js'; +import CourtDrawingRender from '../components/CourtDrawingRender.vue'; export default { name: 'DiaryView', - components: { Multiselect }, + components: { Multiselect, CourtDrawingRender }, data() { return { date: null, @@ -436,6 +445,8 @@ export default { showDropdown: false, showImage: false, imageUrl: '', + showRenderModal: false, + renderModalData: null, groups: [], currentTimeBlockId: null, newGroupName: '', @@ -526,6 +537,36 @@ export default { }, }, methods: { + drawingDataFor(pa) { + // Zeichnungsdaten können bereits als Objekt vorliegen oder als JSON-String + try { + if (!pa) return null; + if (pa.drawingData && typeof pa.drawingData === 'object') { + console.debug('DiaryView: drawingData (object) gefunden für', pa.id); + return pa.drawingData; + } + if (pa.drawingData && typeof pa.drawingData === 'string') { + const parsed = JSON.parse(pa.drawingData); + console.debug('DiaryView: drawingData (string→parsed) für', pa.id, parsed); + return parsed; + } + // Fallback: falls über images[0].drawingData geliefert wurde + if (pa.images && pa.images.length) { + // Nimm das erste Bild, das drawingData enthält + const withData = pa.images.find(img => !!img.drawingData); + if (withData) { + const parsedImg = typeof withData.drawingData === 'string' + ? JSON.parse(withData.drawingData) + : withData.drawingData; + console.debug('DiaryView: drawingData aus images für', pa.id, 'imageId=', withData.id, parsedImg); + return parsedImg; + } + } + } catch (e) { + console.warn('DiaryView: drawingData parse error:', e); + } + return null; + }, async init() { if (this.isAuthenticated && this.currentClub) { const response = await apiClient.get(`/diary/${this.currentClub}`); @@ -1119,17 +1160,43 @@ export default { }, closeImage() { this.showImage = false; + this.showRenderModal = false; + this.renderModalData = null; this.imageUrl = ''; }, - showActivityImage(imageLink) { - // Erstelle vollständige URL mit korrektem Port - if (imageLink && imageLink.startsWith('/api/')) { - this.imageUrl = `http://localhost:3000${imageLink}`; - } else { - this.imageUrl = imageLink; + async showActivityImage(imageLink) { + try { + // Vorherige Object-URL freigeben + if (this.imageUrl && this.imageUrl.startsWith('blob:')) { + URL.revokeObjectURL(this.imageUrl); + } + + if (imageLink && imageLink.startsWith('/api/')) { + // Über API mit Auth und als Blob laden → vermeidet ORB + const path = imageLink.replace(/^\/api\//, ''); + const resp = await apiClient.get(`/${path}`, { responseType: 'blob' }); + this.imageUrl = URL.createObjectURL(resp.data); + } else { + this.imageUrl = imageLink; + } + this.showImage = true; + } catch (e) { + console.error('Bild laden fehlgeschlagen:', e); + alert('Bild konnte nicht geladen werden.'); + } + }, + async openActivityVisual(pa) { + // Bevorzugt Rendering, ansonsten Bild + const data = this.drawingDataFor(pa); + if (data) { + // Erzeuge temporär ein PNG im Client: Canvas offscreen rendern über Renderer-Komponente ist aufwändig. + // Einfacher: öffne ein kleines Modal mit der Renderer-Komponente statt imageUrl. + this.renderModalData = data; + this.showRenderModal = true; + } else if (pa.imageLink) { + await this.showActivityImage(pa.imageLink); } - this.showImage = true; }, async loadMemberImage(member) { try { @@ -1915,6 +1982,12 @@ img { margin: 0 auto; } +.memberImage > div, .memberImage canvas { + /* falls Komponenten-Inhalt (Renderer) da ist, mittig zeigen */ + display: block; + margin: 0 auto; +} + .groups { display: flex; flex-direction: row; From f4187512ba0316cf499ea329351437ede7f604aa Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 25 Sep 2025 19:35:13 +0200 Subject: [PATCH 5/7] =?UTF-8?q?Erweitert=20die=20Funktionalit=C3=A4t=20zur?= =?UTF-8?q?=20Erstellung=20und=20Aktualisierung=20von=20vordefinierten=20A?= =?UTF-8?q?ktivit=C3=A4ten,=20indem=20das=20Feld=20f=C3=BCr=20Zeichnungsda?= =?UTF-8?q?ten=20in=20den=20entsprechenden=20Controllern,=20Modellen=20und?= =?UTF-8?q?=20Services=20hinzugef=C3=BCgt=20wird.=20Aktualisiert=20die=20B?= =?UTF-8?q?enutzeroberfl=C3=A4che=20in=20CourtDrawingTool.vue=20und=20Pred?= =?UTF-8?q?efinedActivities.vue,=20um=20die=20Handhabung=20von=20Zeichnung?= =?UTF-8?q?sdaten=20zu=20verbessern=20und=20die=20Logik=20f=C3=BCr=20das?= =?UTF-8?q?=20Laden=20und=20Speichern=20von=20Zeichnungen=20zu=20optimiere?= =?UTF-8?q?n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../predefinedActivityController.js | 8 ++-- backend/models/PredefinedActivity.js | 5 +++ backend/services/predefinedActivityService.js | 2 + frontend/src/components/CourtDrawingTool.vue | 24 +++++++--- frontend/src/views/PredefinedActivities.vue | 44 +++++++++++-------- 5 files changed, 55 insertions(+), 28 deletions(-) diff --git a/backend/controllers/predefinedActivityController.js b/backend/controllers/predefinedActivityController.js index 102a2f1..1320a7a 100644 --- a/backend/controllers/predefinedActivityController.js +++ b/backend/controllers/predefinedActivityController.js @@ -5,8 +5,8 @@ import fs from 'fs'; export const createPredefinedActivity = async (req, res) => { try { - const { name, code, description, durationText, duration, imageLink } = req.body; - const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink }); + const { name, code, description, durationText, duration, imageLink, drawingData } = req.body; + const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData }); res.status(201).json(predefinedActivity); } catch (error) { console.error('[createPredefinedActivity] - Error:', error); @@ -42,8 +42,8 @@ export const getPredefinedActivityById = async (req, res) => { export const updatePredefinedActivity = async (req, res) => { try { const { id } = req.params; - const { name, code, description, durationText, duration, imageLink } = req.body; - const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink }); + const { name, code, description, durationText, duration, imageLink, drawingData } = req.body; + const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData }); res.status(200).json(updatedActivity); } catch (error) { console.error('[updatePredefinedActivity] - Error:', error); diff --git a/backend/models/PredefinedActivity.js b/backend/models/PredefinedActivity.js index 749c6a5..63c7d8d 100644 --- a/backend/models/PredefinedActivity.js +++ b/backend/models/PredefinedActivity.js @@ -19,6 +19,11 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', { type: DataTypes.TEXT, allowNull: true, }, + drawingData: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'JSON string with metadata for Court Drawing Tool' + }, durationText: { type: DataTypes.STRING, allowNull: true, diff --git a/backend/services/predefinedActivityService.js b/backend/services/predefinedActivityService.js index 8152d3a..2dd9cdd 100644 --- a/backend/services/predefinedActivityService.js +++ b/backend/services/predefinedActivityService.js @@ -15,6 +15,7 @@ class PredefinedActivityService { durationText: data.durationText, duration: data.duration, imageLink: data.imageLink, + drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null, }); } @@ -32,6 +33,7 @@ class PredefinedActivityService { durationText: data.durationText, duration: data.duration, imageLink: data.imageLink, + drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null, }); } diff --git a/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue index 065ecc9..0ec848f 100644 --- a/frontend/src/components/CourtDrawingTool.vue +++ b/frontend/src/components/CourtDrawingTool.vue @@ -176,7 +176,6 @@
-
@@ -196,6 +195,10 @@ export default { drawingData: { // Added for loading saved drawing data type: Object, default: null + }, + allowImageUpload: { // Neu: steuert, ob das Tool ein Bild erzeugt/hochlädt + type: Boolean, + default: true } }, data() { @@ -1092,7 +1095,7 @@ export default { } }, - async saveDrawing() { + async saveDrawing() { console.log('CourtDrawingTool: saveDrawing called'); try { @@ -1116,7 +1119,10 @@ export default { }; console.log('CourtDrawingTool: drawingData created:', drawingData); - + // Immer Metadaten nach oben geben + this.$emit('update-drawing-data', drawingData); + + if (this.allowImageUpload) { // Konvertiere DataURL zu Blob für Upload const response = await fetch(dataURL); const blob = await response.blob(); @@ -1132,9 +1138,15 @@ export default { console.log('CourtDrawingTool: emitting upload-image event'); this.$emit('upload-image', file, drawingData); console.log('CourtDrawingTool: upload-image event emitted'); - - // Emittiere das File für Upload (Parent-Komponente macht den Upload) - console.log('CourtDrawingTool: File ready for upload'); + } else { + // Kein Bild-Upload mehr: gebe lediglich die Zeichnungsdaten an den Parent weiter, + // damit Felder (Kürzel/Name/Beschreibung) gefüllt werden können + this.$emit('update-fields', { + code: this.getFullCode ? this.getFullCode() : '', + name: this.getFullTitle ? this.getFullTitle() : '', + description: '' + }); + } } catch (error) { console.error('CourtDrawingTool: Error in saveDrawing:', error); } diff --git a/frontend/src/views/PredefinedActivities.vue b/frontend/src/views/PredefinedActivities.vue index 4ca2b5b..ae71ad1 100644 --- a/frontend/src/views/PredefinedActivities.vue +++ b/frontend/src/views/PredefinedActivities.vue @@ -81,8 +81,10 @@ v-model="editModel.drawingData" :activity-id="editModel.id" :drawing-data="editModel.drawingData" + :allow-image-upload="false" @save="onDrawingSave" @update-fields="onUpdateFields" + @update-drawing-data="onUpdateDrawingData" @upload-image="onDrawingImageUpload" @image-uploaded="onImageUploaded" /> @@ -150,6 +152,15 @@ export default { } }, methods: { + parseDrawingData(value) { + if (!value) return null; + if (typeof value === 'object') return value; + try { return JSON.parse(value); } catch (e) { return null; } + }, + normalizeActivity(activity, images) { + const drawingData = this.parseDrawingData(activity && activity.drawingData); + return { ...(activity || {}), drawingData }; + }, async reload() { const r = await apiClient.get('/predefined-activities'); this.activities = r.data || []; @@ -159,23 +170,12 @@ export default { const r = await apiClient.get(`/predefined-activities/${a.id}`); const { images, ...activity } = r.data; this.images = images || []; - - // Lade Zeichnungsdaten aus dem ersten Bild, falls vorhanden - let drawingData = null; - if (images && images.length > 0 && images[0].drawingData) { - try { - drawingData = JSON.parse(images[0].drawingData); - console.log('PredefinedActivities: Loaded drawingData:', drawingData); - } catch (error) { - console.error('PredefinedActivities: Error parsing drawingData:', error); - } - } else { - // Keine Bilder vorhanden - setze drawingData explizit auf null - console.log('PredefinedActivities: No images found, setting drawingData to null'); - drawingData = null; + // Server-Daten normalisieren und ggf. image-drawingData fallbacken + let model = this.normalizeActivity(activity, images); + if (!model.drawingData && images && images.length > 0 && images[0].drawingData) { + model.drawingData = this.parseDrawingData(images[0].drawingData); } - - this.editModel = { ...activity, drawingData }; + this.editModel = model; }, async reloadImages() { if (this.editModel && this.editModel.id) { @@ -226,11 +226,14 @@ export default { if (this.editModel.id) { const { id, ...payload } = this.editModel; + if (payload.drawingData && typeof payload.drawingData === 'object') { + payload.drawingData = payload.drawingData; + } const r = await apiClient.put(`/predefined-activities/${id}`, payload); - this.editModel = r.data; + this.editModel = this.normalizeActivity(r.data); } else { const r = await apiClient.post('/predefined-activities', this.editModel); - this.editModel = r.data; + this.editModel = this.normalizeActivity(r.data); } // Nach dem Speichern (sowohl CREATE als auch UPDATE): Bild hochladen falls vorhanden @@ -328,6 +331,11 @@ export default { this.editModel.description = fields.description; } }, + onUpdateDrawingData(data) { + if (this.editModel) { + this.editModel.drawingData = data; + } + }, async onDrawingImageUpload(file, drawingData) { console.log('onDrawingImageUpload called with file:', file); From 4bfa6a58895e6d2cf24fb891ec416bd9c0b03678 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 1 Oct 2025 09:41:07 +0200 Subject: [PATCH 6/7] =?UTF-8?q?Erweitert=20die=20Funktionalit=C3=A4t=20in?= =?UTF-8?q?=20CourtDrawingRender.vue=20und=20CourtDrawingTool.vue=20zur=20?= =?UTF-8?q?Verbesserung=20der=20Zeichnungslogik.=20F=C3=BCgt=20neue=20Offs?= =?UTF-8?q?et-Parameter=20f=C3=BCr=20Zielkreise=20hinzu=20und=20optimiert?= =?UTF-8?q?=20die=20Berechnung=20der=20Zielpositionen.=20Entfernt=20die=20?= =?UTF-8?q?Schaltfl=C3=A4chen=20f=C3=BCr=20das=20manuelle=20Speichern=20in?= =?UTF-8?q?=20CourtDrawingTool.vue=20zugunsten=20einer=20automatischen=20S?= =?UTF-8?q?peicherung.=20Aktualisiert=20die=20Benutzeroberfl=C3=A4che=20in?= =?UTF-8?q?=20PredefinedActivities.vue=20zur=20Unterst=C3=BCtzung=20der=20?= =?UTF-8?q?neuen=20Zeichnungsdaten-Logik.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CourtDrawingRender.vue | 99 +++++++++++-------- frontend/src/components/CourtDrawingTool.vue | 60 ++++++++--- frontend/src/views/PredefinedActivities.vue | 4 - 3 files changed, 105 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/CourtDrawingRender.vue b/frontend/src/components/CourtDrawingRender.vue index 295c80e..4481ae0 100644 --- a/frontend/src/components/CourtDrawingRender.vue +++ b/frontend/src/components/CourtDrawingRender.vue @@ -66,14 +66,27 @@ export default { primaryColor: '#d32f2f', // rechts -> target (rot) secondaryColor: '#1565c0', // zurück (blau) width: 6, - headLength: 24 + headLength: 24, + vhOffsetX: 5, + vhOffsetY: 8, + rhOffsetX: 10, + rhOffsetY: 0 }, targetCircles: { radius: 13, + rightXOffset: 20, + middleXOffset: 40, topYOffset: 45, bottomYOffset: 45, transparency: 0.25 }, + leftTargetCircles: { + radius: 10, + leftXOffset: 20, + rightXOffset: 40, + topYOffset: 40, + bottomYOffset: 40 + }, hitMarker: { radius: 10.5, fill: '#ffffff', @@ -223,17 +236,17 @@ export default { ctx.stroke(); }, computeRightTargetPosition(number) { - // replicate the same geometry as in the drawing tool + // Geometrie wie im Tool: nutzt rightXOffset/middleXOffset const tableWidth = this.config.table.width; const tableHeight = this.config.table.height; const tableX = (this.config.canvas.width - tableWidth) / 2; const tableY = (this.config.canvas.height - tableHeight) / 2; - const centerX = tableX + tableWidth / 2; const cfg = this.config.targetCircles; - const x1 = tableX + tableWidth - 30; // rightmost column (1,2,3) - const x3 = centerX + (x1 - centerX) * 0.35; // near net (7,8,9) - const x2 = (x1 + x3) / 2; // middle column (4,5,6) + const x1 = tableX + tableWidth - cfg.rightXOffset; // (1,2,3) + const x3 = tableX + tableWidth / 2 + cfg.middleXOffset; // (7,8,9) + const xdiff = x3 - x1; + const x2 = x3 - xdiff / 2; // (4,5,6) const positions = { 1: { x: x1, y: tableY + cfg.topYOffset }, @@ -249,22 +262,23 @@ export default { return positions[number] || null; }, getStartPoint() { - const cfg = this.config.startCircles; - const tableWidth = this.config.table.width; - const tableHeight = this.config.table.height; - const tableX = (this.config.canvas.width - tableWidth) / 2; - const tableY = (this.config.canvas.height - tableHeight) / 2; - const map = { - AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' - }; - const selected = map[this.drawingData?.selectedStartPosition] || 'middle'; - const circleX = tableX - cfg.x; // Links vom Tisch - const topY = tableY + cfg.topYOffset; - const midY = tableY + tableHeight / 2; - const botY = tableY + tableHeight - cfg.bottomYOffset; - const y = selected === 'top' ? topY : selected === 'bottom' ? botY : midY; - // arrow should start slightly to the right of the start circle - return { x: circleX + cfg.radius + 6, y }; + // Startpunkt wie im Tool abhängig von Schlagseite (VH/RH) + const sc = this.config.startCircles; + const ar = this.config.arrows; + const tblW = this.config.table.width; + const tblH = this.config.table.height; + const tableX = (this.config.canvas.width - tblW) / 2; + const tableY = (this.config.canvas.height - tblH) / 2; + const circleX = tableX - sc.x; // Kreis links vor dem Tisch + const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' }; + const pos = map[this.drawingData?.selectedStartPosition] || 'middle'; + const y = pos === 'top' ? tableY + sc.topYOffset : pos === 'bottom' ? tableY + tblH - sc.bottomYOffset : tableY + tblH / 2; + const isVH = (this.drawingData?.strokeType || 'VH') === 'VH'; + const startX = isVH + ? circleX + sc.radius + ar.vhOffsetX + : circleX + sc.radius + ar.rhOffsetX; + const startYOffset = isVH ? ar.vhOffsetY : ar.rhOffsetY; + return { x: startX, y: y + startYOffset }; }, drawArrow(ctx, from, to, color, label) { const { width, headLength } = this.config.arrows; @@ -318,7 +332,8 @@ export default { const spinAbbrev = this.abbrevSpin(this.drawingData.spinType); // Text gehört an die Quelle (ohne "target") const sourceLabel = `${strokeSide} ${spinAbbrev}`.trim(); - this.drawArrow(ctx, from, to, this.config.arrows.primaryColor); + const toEnd = { x: to.x - this.config.targetCircles.radius, y: to.y }; + this.drawArrow(ctx, from, toEnd, this.config.arrows.primaryColor); // Unter dem Startkreis beschriften const startCenter = this.getStartCircleCenter(); this.drawLabelBelow(ctx, sourceLabel, startCenter); @@ -328,18 +343,20 @@ export default { const leftTarget = this.drawingData.nextStrokeTargetPosition ? Number(this.drawingData.nextStrokeTargetPosition) : null; if (tp && leftTarget) { // source near previous right target - const sourceRight = this.computeRightTargetPosition(tp); + const sourceRightCenter = this.computeRightTargetPosition(tp); // left target mapping: mirror scheme to left half - const toLeft = this.computeLeftTargetPosition(leftTarget); + const toLeftCenter = this.computeLeftTargetPosition(leftTarget); // Zielmarkierung links - this.drawHitMarker(ctx, toLeft); + 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, sourceRight); + this.drawLabelBelow(ctx, targetLabel, sourceRightCenter); } }, getStartCircleCenter() { @@ -370,28 +387,32 @@ export default { ctx.stroke(); }, computeLeftTargetPosition(number) { - // mirror target grid to left side + // Spiegelung wie im Tool: nutzt leftTargetCircles Offsets const tableWidth = this.config.table.width; const tableHeight = this.config.table.height; const tableX = (this.config.canvas.width - tableWidth) / 2; const tableY = (this.config.canvas.height - tableHeight) / 2; - const centerX = tableX + tableWidth / 2; - const cfg = this.config.targetCircles; + const cfg = this.config.leftTargetCircles; - const x1 = tableX + 30; // leftmost column - const x3 = centerX - (centerX - x1) * 0.35; // near net - const x2 = (x1 + x3) / 2; + const x1 = tableX + cfg.leftXOffset; // linke Spalte (Lang) + const x3 = tableX + tableWidth / 2 - cfg.rightXOffset; // nah am Netz (Kurz) + const xdiff = x3 - x1; + const x2 = x3 - xdiff / 2; + // Gespiegelte Y-Zuordnung wie im Tool: + // 1,4,7 = unten (VH gespiegelt) + // 2,5,8 = mitte + // 3,6,9 = oben (RH gespiegelt) const positions = { - 1: { x: x1, y: tableY + cfg.topYOffset }, + 1: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset }, 2: { x: x1, y: tableY + tableHeight / 2 }, - 3: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset }, - 4: { x: x2, y: tableY + cfg.topYOffset }, + 3: { x: x1, y: tableY + cfg.topYOffset }, + 4: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset }, 5: { x: x2, y: tableY + tableHeight / 2 }, - 6: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset }, - 7: { x: x3, y: tableY + cfg.topYOffset }, + 6: { x: x2, y: tableY + cfg.topYOffset }, + 7: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset }, 8: { x: x3, y: tableY + tableHeight / 2 }, - 9: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset } + 9: { x: x3, y: tableY + cfg.topYOffset } }; return positions[number] || null; }, diff --git a/frontend/src/components/CourtDrawingTool.vue b/frontend/src/components/CourtDrawingTool.vue index 0ec848f..76cdafb 100644 --- a/frontend/src/components/CourtDrawingTool.vue +++ b/frontend/src/components/CourtDrawingTool.vue @@ -172,11 +172,7 @@
- -
- - -
+ @@ -323,13 +319,16 @@ export default { }, watch: { strokeType() { + this.emitDrawingData(); this.updateTextFields(); }, spinType() { this.drawCourt(); // Neu zeichnen wenn sich Schnittoption ändert + this.emitDrawingData(); this.updateTextFields(); }, targetPosition() { + this.emitDrawingData(); this.updateTextFields(); }, selectedStartPosition() { @@ -337,6 +336,7 @@ export default { if (this.canvas && this.ctx) { this.drawCourt(); } + this.emitDrawingData(); }, drawingData: { handler(newVal, oldVal) { @@ -889,6 +889,7 @@ export default { this.selectedCirclePosition = clickedCircle.circlePosition; this.selectedStartPosition = clickedCircle.position; this.drawCourt(); // Neu zeichnen für Kreis-Hervorhebung + this.emitDrawingData(); this.updateTextFields(); return; } @@ -898,13 +899,14 @@ export default { if (clickedTarget) { this.targetPosition = clickedTarget; - // Wenn keine Startposition ausgewählt ist, setze eine Standard-Startposition + // Wenn keine Startposition ausgewählt ist, setze standardmäßig die mittlere Startposition (AS2) if (!this.selectedStartPosition) { - this.selectedStartPosition = 'AS'; + this.selectedStartPosition = 'AS2'; this.selectedCirclePosition = 'middle'; } this.drawCourt(); // Neu zeichnen für Zielposition-Hervorhebung + this.emitDrawingData(); this.updateTextFields(); return; } @@ -915,6 +917,7 @@ export default { if (clickedLeftTarget) { this.nextStrokeTargetPosition = clickedLeftTarget; this.drawCourt(); + this.emitDrawingData(); this.updateTextFields(); return; } @@ -943,9 +946,9 @@ export default { // Prüfe Abstand zu den Kreisen const circles = [ - { x: circleX, y: topCircleY, position: 'AS', circlePosition: 'top' }, - { x: circleX, y: middleCircleY, position: 'AS', circlePosition: 'middle' }, - { x: circleX, y: bottomCircleY, position: 'AS', circlePosition: 'bottom' } + { x: circleX, y: topCircleY, position: 'AS1', circlePosition: 'top' }, + { x: circleX, y: middleCircleY, position: 'AS2', circlePosition: 'middle' }, + { x: circleX, y: bottomCircleY, position: 'AS3', circlePosition: 'bottom' } ]; for (const circle of circles) { @@ -1252,27 +1255,38 @@ export default { 'Gegenläufer': 'GL' }; - // Label-Mapping für Zielpositionen - const targetLabelMap = { + // Suffix-Mapping für Startposition (an AS direkt anhängen, ohne Leerzeichen) + const startSuffixMap = { + 'AS1': 'vL', + 'AS2': 'vM', + 'AS3': 'vR' + }; + const startSuffix = startSuffixMap[this.selectedStartPosition] || ''; + const startCode = `AS${startSuffix}`; + + // Label-Mapping für erstes Ziel (Hauptpfeil): altes Schema + const mainTargetLabelMap = { '1': 'VH L', '2': 'M L', '3': 'RH L', '4': 'VH H', '5': 'M H', '6': 'RH H', '7': 'VH K', '8': 'M K', '9': 'RH K' }; + // Label-Mapping für Zusatzschläge: wie Hauptziel (VH L, M L, RH L ...) + const additionalTargetLabelMap = mainTargetLabelMap; - let code = `${this.selectedStartPosition} ${this.strokeType}`; + let code = `${startCode} ${this.strokeType}`; if (this.spinType) { const spinCode = spinCodeMap[this.spinType] || this.spinType; code += ` ${spinCode}`; } if (this.targetPosition) { - const targetLabel = targetLabelMap[this.targetPosition] || this.targetPosition; + const targetLabel = mainTargetLabelMap[this.targetPosition] || this.targetPosition; code += ` → ${targetLabel}`; } // Zusätzliche Schläge hinzufügen if (this.additionalStrokes.length > 0) { this.additionalStrokes.forEach(stroke => { - const targetLabel = targetLabelMap[stroke.targetPosition] || stroke.targetPosition; + const targetLabel = additionalTargetLabelMap[stroke.targetPosition] || stroke.targetPosition; code += ` / ${stroke.side} ${stroke.type} → ${targetLabel}`; }); } @@ -1342,6 +1356,7 @@ export default { // Canvas neu zeichnen this.drawCourt(); + this.emitDrawingData(); }, updateTextFields() { @@ -1358,6 +1373,21 @@ export default { description: description }); } + , + emitDrawingData() { + const drawingData = { + selectedStartPosition: this.selectedStartPosition, + strokeType: this.strokeType, + spinType: this.spinType, + targetPosition: this.targetPosition, + nextStrokeType: this.nextStrokeType, + nextStrokeSide: this.nextStrokeSide, + nextStrokeTargetPosition: this.nextStrokeTargetPosition, + exerciseCounter: this.exerciseCounter, + additionalStrokes: this.additionalStrokes + }; + this.$emit('update-drawing-data', drawingData); + } } } diff --git a/frontend/src/views/PredefinedActivities.vue b/frontend/src/views/PredefinedActivities.vue index ae71ad1..8b69b8d 100644 --- a/frontend/src/views/PredefinedActivities.vue +++ b/frontend/src/views/PredefinedActivities.vue @@ -78,15 +78,11 @@
Übungszeichnung erstellen
From afd96f5df1c27e35aba7ecb23ef7fcd23902c6e9 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 1 Oct 2025 09:42:15 +0200 Subject: [PATCH 7/7] =?UTF-8?q?Optimiert=20die=20Berechnung=20der=20Startp?= =?UTF-8?q?osition=20im=20CourtDrawingRender.vue,=20indem=20die=20Offset-L?= =?UTF-8?q?ogik=20f=C3=BCr=20vertikale=20und=20horizontale=20Zeichnungen?= =?UTF-8?q?=20pr=C3=A4zisiert=20wird.=20Kommentiert=20den=20Code=20zur=20b?= =?UTF-8?q?esseren=20Verst=C3=A4ndlichkeit=20der=20Offset-Berechnungen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/CourtDrawingRender.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/CourtDrawingRender.vue b/frontend/src/components/CourtDrawingRender.vue index 4481ae0..24225b2 100644 --- a/frontend/src/components/CourtDrawingRender.vue +++ b/frontend/src/components/CourtDrawingRender.vue @@ -277,7 +277,8 @@ export default { const startX = isVH ? circleX + sc.radius + ar.vhOffsetX : circleX + sc.radius + ar.rhOffsetX; - const startYOffset = isVH ? ar.vhOffsetY : ar.rhOffsetY; + // VH: unterhalb seitlich (circleRadius + vhOffsetY), RH: rechts (rhOffsetY = 0) + const startYOffset = isVH ? (sc.radius + ar.vhOffsetY) : ar.rhOffsetY; return { x: startX, y: y + startYOffset }; }, drawArrow(ctx, from, to, color, label) {