From 5e76163f907f5d36c4d449f3062eb0aa52ab589d Mon Sep 17 00:00:00 2001 From: gabriele-cusato Date: Tue, 24 Mar 2026 22:27:44 +0100 Subject: [PATCH] fixato anche il problema delle barre vuote nell'overlay di disegno e la versione a 1.0.0 --- HandTranscriptMd/manifest.json | 2 +- HandTranscriptMd/src/drawing-canvas.ts | 105 ++++++++++++++++++++----- HandTranscriptMd/src/editor-view.ts | 64 ++++++++++++--- NOTES.md | 23 ++++++ 4 files changed, 161 insertions(+), 33 deletions(-) diff --git a/HandTranscriptMd/manifest.json b/HandTranscriptMd/manifest.json index abd86ce..223ff02 100644 --- a/HandTranscriptMd/manifest.json +++ b/HandTranscriptMd/manifest.json @@ -1,7 +1,7 @@ { "id": "handwriting-to-markdown", "name": "HandTranscriptMd", - "version": "0.1.0", + "version": "1.0.0", "minAppVersion": "0.15.0", "description": "Inline handwriting canvas with conversion to structured markdown", "author": "GabrieleC", diff --git a/HandTranscriptMd/src/drawing-canvas.ts b/HandTranscriptMd/src/drawing-canvas.ts index e8b7ad7..ceb96eb 100644 --- a/HandTranscriptMd/src/drawing-canvas.ts +++ b/HandTranscriptMd/src/drawing-canvas.ts @@ -71,19 +71,45 @@ export class DrawingCanvas { // Callback debug: se impostato, mostra Notice all'utente per ogni evento IME/touch private debugFn: ((msg: string) => void) | null = null; + // Device Pixel Ratio: scala il buffer interno per display ad alta densità (Retina, ecc.) + private dpr: number; + // Dimensione logica CSS del canvas (in pixel logici, non fisici) + private logicalWidth: number; + private logicalHeight: number; + // Spazio coordinate dei tratti salvati: cresce quando il display si allarga, non scende mai. + // Garantisce che i tratti rimangano nell'SVG anche dopo una rotazione portrait. + private worldWidth: number; + // Scala orizzontale di visualizzazione: logicalWidth / worldWidth. + // < 1 quando il display è più stretto del mondo (es. portrait dopo landscape): il contenuto + // si comprime per mostrare tutto senza tagliare nulla. + private viewScale = 1.0; + // Mantenuto per compatibilità ma sempre 0 (non usiamo centering, solo scaling) + private viewOffsetX = 0; + constructor(container: HTMLElement, width: number, height: number, defaultHeight: number, mobileMode = false, debugFn: ((msg: string) => void) | null = null) { - this.canvas = document.createElement('canvas'); - this.canvas.width = width; - this.canvas.height = height; + this.dpr = window.devicePixelRatio || 1; + this.worldWidth = width; + this.logicalWidth = width; + this.logicalHeight = height; this.defaultHeight = defaultHeight; this.mobileMode = mobileMode; this.debugFn = debugFn; + + this.canvas = document.createElement('canvas'); + // Dimensione CSS: pixel logici → il browser mostra il canvas a questa dimensione + this.canvas.style.width = width + 'px'; + this.canvas.style.height = height + 'px'; + // Buffer interno: pixel fisici moltiplicati per il DPR → nessuna pixelazione + this.canvas.width = Math.round(width * this.dpr); + this.canvas.height = Math.round(height * this.dpr); this.canvas.classList.add('hwm_canvas'); // touch-action: none sul canvas previene scroll/zoom durante il disegno this.canvas.style.setProperty('touch-action', 'none', 'important'); container.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d')!; + // Scala il context: da questo punto tutte le coordinate ctx sono in pixel logici + this.ctx.scale(this.dpr, this.dpr); this.clearBackground(); // Stato iniziale nella history (canvas vuoto) @@ -104,6 +130,26 @@ export class DrawingCanvas { onChange(cb: () => void) { this.changeCb = cb; } // Registra callback per quando l'altezza cambia (utile per auto-scroll nell'overlay) onResize(cb: () => void) { this.resizeCb = cb; } + + // Adatta il canvas alla larghezza di display indicata (rotazione schermo, apertura modal). + // - Se il display è più largo del mondo attuale → worldWidth cresce (nuova area disegnabile). + // - Se il display è più stretto → worldWidth resta invariato; i tratti vengono compressi + // orizzontalmente (viewScale < 1) per mostrare tutto senza tagliare nulla nel SVG. + setDisplayWidth(displayWidth: number) { + if (displayWidth === this.logicalWidth) return; + if (displayWidth > this.worldWidth) { + // Espansione: il mondo si allarga con il display + this.worldWidth = displayWidth; + } + // Aggiorna larghezza logica e fattore di scala + this.logicalWidth = displayWidth; + this.viewScale = this.logicalWidth / this.worldWidth; + this.canvas.style.width = displayWidth + 'px'; + // Cambiare canvas.width resetta il context → ri-applicare la scala DPR + this.canvas.width = Math.round(displayWidth * this.dpr); + this.ctx.scale(this.dpr, this.dpr); + this.redraw(); + } // Abilita scroll manuale con il dito sul canvas. // touch-action resta 'none' (la penna non trigga scroll del browser), // il dito scrolla il container via JS. @@ -143,8 +189,9 @@ export class DrawingCanvas { setLineWidth(w: number) { this.lineWidth = w; } getStrokes(): Stroke[] { return [...this.strokes]; } - getWidth(): number { return this.canvas.width; } - getHeight(): number { return this.canvas.height; } + // Ritorna le dimensioni nel sistema di coordinate mondo (usato per l'SVG viewBox) + getWidth(): number { return this.worldWidth; } + getHeight(): number { return this.logicalHeight; } setBackground(bgColor: string, lineColor: string) { this.bgColor = bgColor; @@ -195,7 +242,11 @@ export class DrawingCanvas { resizeHeight(newHeight: number) { if (newHeight < 100) return; - this.canvas.height = newHeight; + this.logicalHeight = newHeight; + this.canvas.style.height = newHeight + 'px'; + // canvas.height resetta il context → ri-applicare la scala DPR + this.canvas.height = Math.round(newHeight * this.dpr); + this.ctx.scale(this.dpr, this.dpr); this.redraw(); } @@ -298,15 +349,16 @@ export class DrawingCanvas { // Se un'animazione è già in corso non lanciarne un'altra: // ripartire da un'altezza intermedia causerebbe un effetto di restringimento. if (this.animFrameId !== null) return; - if (pt.y > this.canvas.height - this.EXPAND_MARGIN) { - const newHeight = this.canvas.height + this.EXPAND_AMOUNT; - this.animateHeight(newHeight); + // Confronto in pixel logici: pt.y è in coordinate mondo, logicalHeight è logica + if (pt.y > this.logicalHeight - this.EXPAND_MARGIN) { + const newLogicalH = this.logicalHeight + this.EXPAND_AMOUNT; + this.animateHeight(newLogicalH); } } - private animateHeight(targetHeight: number) { - const startHeight = this.canvas.height; - if (startHeight === targetHeight) return; + private animateHeight(targetLogicalH: number) { + const startLogicalH = this.logicalHeight; + if (startLogicalH === targetLogicalH) return; if (this.animFrameId !== null) { cancelAnimationFrame(this.animFrameId); @@ -320,9 +372,14 @@ export class DrawingCanvas { const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); - const h = Math.round(startHeight + (targetHeight - startHeight) * eased); + // Altezza in pixel logici per questa frame + const h = Math.round(startLogicalH + (targetLogicalH - startLogicalH) * eased); - this.canvas.height = h; + this.logicalHeight = h; + this.canvas.style.height = h + 'px'; + // canvas.height è in pixel fisici; cambiarlo resetta il context → ri-scalare + this.canvas.height = Math.round(h * this.dpr); + this.ctx.scale(this.dpr, this.dpr); this.redraw(); if (this.currentStroke) { this.drawFullStroke(this.currentStroke); @@ -344,11 +401,10 @@ export class DrawingCanvas { private eventToPoint(e: PointerEvent): Point { const rect = this.canvas.getBoundingClientRect(); - const scaleX = this.canvas.width / rect.width; - const scaleY = this.canvas.height / rect.height; + // Divide per viewScale per tornare alle coordinate mondo (invarianti al cambio orientamento) return { - x: (e.clientX - rect.left) * scaleX, - y: (e.clientY - rect.top) * scaleY, + x: (e.clientX - rect.left) / this.viewScale, + y: (e.clientY - rect.top), pressure: e.pressure > 0 ? e.pressure : 0.5, }; } @@ -411,8 +467,9 @@ export class DrawingCanvas { /* --- Rendering --- */ private clearBackground() { - const w = this.canvas.width; - const h = this.canvas.height; + // Usa pixel logici: ctx.scale(dpr, dpr) è già applicato nel constructor/resize + const w = this.logicalWidth; + const h = this.logicalHeight; this.ctx.fillStyle = this.bgColor; this.ctx.fillRect(0, 0, w, h); @@ -439,6 +496,9 @@ export class DrawingCanvas { if (pts.length < 2) return; const ctx = this.ctx; + // Scala orizzontale: comprime i tratti mondo nello spazio logico disponibile + ctx.save(); + ctx.scale(this.viewScale, 1.0); ctx.strokeStyle = stroke.color; ctx.lineWidth = stroke.width; ctx.lineCap = 'round'; @@ -460,6 +520,7 @@ export class DrawingCanvas { ctx.lineTo(last.x, last.y); } ctx.stroke(); + ctx.restore(); } private drawSegment(stroke: Stroke) { @@ -467,6 +528,9 @@ export class DrawingCanvas { if (pts.length < 2) return; const ctx = this.ctx; + // Stessa scala di drawFullStroke per coerenza durante il disegno live + ctx.save(); + ctx.scale(this.viewScale, 1.0); ctx.strokeStyle = stroke.color; ctx.lineWidth = stroke.width; ctx.lineCap = 'round'; @@ -490,5 +554,6 @@ export class DrawingCanvas { ctx.quadraticCurveTo(curr.x, curr.y, endX, endY); } ctx.stroke(); + ctx.restore(); } } diff --git a/HandTranscriptMd/src/editor-view.ts b/HandTranscriptMd/src/editor-view.ts index a4c0c55..2b6e6e6 100644 --- a/HandTranscriptMd/src/editor-view.ts +++ b/HandTranscriptMd/src/editor-view.ts @@ -47,6 +47,8 @@ export class DrawingEditorView extends ItemView { private saveTimer: ReturnType | null = null; // Listener per aggiornare la classe dark al cambio bgMode private bgModeListener: ((bgMode: string) => void) | null = null; + // ResizeObserver per adattare il canvas al layout reale (inclusa rotazione schermo) + private displayRo: ResizeObserver | null = null; constructor(leaf: WorkspaceLeaf, plugin: HandwritingPlugin) { super(leaf); @@ -85,6 +87,9 @@ export class DrawingEditorView extends ItemView { this.plugin.bgModeListeners.delete(this.bgModeListener); this.bgModeListener = null; } + // Ferma l'osservatore di resize (orientamento schermo) + this.displayRo?.disconnect(); + this.displayRo = null; } /* ---------- Costruisce la UI dell'editor ---------- */ @@ -194,14 +199,17 @@ export class DrawingEditorView extends ItemView { const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' }); // Carica tratti dal file SVG - const { strokes, canvasHeight: savedH } = await this.loadStrokes(); + const { strokes, canvasWidth: savedW, canvasHeight: savedH } = await this.loadStrokes(); const { canvasWidth, canvasHeight } = this.plugin.settings; + // Usa la larghezza salvata nel viewBox SVG come worldWidth iniziale: garantisce che + // setDisplayWidth() non tagli i tratti disegnati in sessioni precedenti più larghe. + const w = savedW ?? canvasWidth; const h = savedH ?? canvasHeight; const debugFn = this.plugin.settings.debugMode ? (msg: string) => new Notice(msg, 3000) : null; // Crea il canvas - this.canvas = new DrawingCanvas(canvasWrap, canvasWidth, h, canvasHeight, isMobile, debugFn); + this.canvas = new DrawingCanvas(canvasWrap, w, h, canvasHeight, isMobile, debugFn); this.canvas.setBackground(bgColor, lineColor); this.canvas.setColor(colors[0]!); // Su mobile: dito = scroll, penna = disegno @@ -216,6 +224,18 @@ export class DrawingEditorView extends ItemView { this.canvas.loadStrokes(remapped); } + // Adatta il canvas alla larghezza reale del container e la mantiene sincronizzata + // ad ogni cambio di orientamento (portrait ↔ landscape). + // L'observer resta attivo per tutta la vita della tab; viene rimosso in onClose(). + // Se clientWidth è ancora 0 (tab non renderizzata), salta e riprova al prossimo evento. + this.displayRo = new ResizeObserver(() => { + const displayW = scrollWrap.clientWidth || el.clientWidth; + if (displayW === 0) return; + this.canvas?.setDisplayWidth(displayW); + }); + this.displayRo.observe(scrollWrap); + this.displayRo.observe(el); + // Resize handle (visibile ma non interattivo) const handle = scrollWrap.createDiv({ cls: 'hwm_resize-handle hwm_resize-handle--disabled' }); handle.createEl('span', { text: '⋯' }); @@ -268,15 +288,21 @@ export class DrawingEditorView extends ItemView { /* ---------- File I/O ---------- */ - private async loadStrokes(): Promise<{ strokes: Stroke[]; canvasHeight: number | null }> { + private async loadStrokes(): Promise<{ strokes: Stroke[]; canvasWidth: number | null; canvasHeight: number | null }> { const file = this.app.vault.getAbstractFileByPath(this.svgPath); if (file instanceof TFile) { const content = await this.app.vault.read(file); const strokes = parseSvgStrokes(content); - const m = content.match(/viewBox="0 0 \d+ (\d+)"/); - return { strokes, canvasHeight: m ? parseInt(m[1] ?? '0') : null }; + // Legge sia la larghezza che l'altezza dal viewBox per ripristinare + // il worldWidth originale (evita il taglio dei tratti oltre settings.canvasWidth) + const m = content.match(/viewBox="0 0 (\d+) (\d+)"/); + return { + strokes, + canvasWidth: m ? parseInt(m[1] ?? '0') : null, + canvasHeight: m ? parseInt(m[2] ?? '0') : null, + }; } - return { strokes: [], canvasHeight: null }; + return { strokes: [], canvasWidth: null, canvasHeight: null }; } private async saveSvg() { @@ -569,12 +595,13 @@ export class DrawingModal extends Modal { const scrollWrap = el.createDiv({ cls: 'hwm_editor-scroll' }); const canvasWrap = scrollWrap.createDiv({ cls: 'hwm_canvas-wrap' }); - const { strokes, canvasHeight: savedH } = await this.loadStrokes(); + const { strokes, canvasWidth: savedW, canvasHeight: savedH } = await this.loadStrokes(); const { canvasWidth, canvasHeight } = this.plugin.settings; + const w = savedW ?? canvasWidth; const h = savedH ?? canvasHeight; const debugFn = this.plugin.settings.debugMode ? (msg: string) => new Notice(msg, 3000) : null; - this.canvas = new DrawingCanvas(canvasWrap, canvasWidth, h, canvasHeight, isMobile, debugFn); + this.canvas = new DrawingCanvas(canvasWrap, w, h, canvasHeight, isMobile, debugFn); this.canvas.setBackground(bgColor, lineColor); this.canvas.setColor(colors[0]!); if (isMobile) this.canvas.allowFingerScroll(scrollWrap); @@ -584,6 +611,15 @@ export class DrawingModal extends Modal { this.canvas.loadStrokes(remapped); } + // Espande il canvas a tutta la larghezza del modal (elimina le bande laterali). + // requestAnimationFrame garantisce che il layout del modal sia pronto prima di misurarlo. + requestAnimationFrame(() => { + const displayW = scrollWrap.clientWidth; + if (this.canvas && displayW > canvasWidth) { + this.canvas.setDisplayWidth(displayW); + } + }); + const handle = scrollWrap.createDiv({ cls: 'hwm_resize-handle hwm_resize-handle--disabled' }); handle.createEl('span', { text: '⋯' }); handle.classList.toggle('hwm_resize-handle--dark', isDark); @@ -617,14 +653,18 @@ export class DrawingModal extends Modal { }); } - private async loadStrokes(): Promise<{ strokes: Stroke[]; canvasHeight: number | null }> { + private async loadStrokes(): Promise<{ strokes: Stroke[]; canvasWidth: number | null; canvasHeight: number | null }> { const file = this.app.vault.getAbstractFileByPath(this.svgPath); if (file instanceof TFile) { const content = await this.app.vault.read(file); - const m = content.match(/viewBox="0 0 \d+ (\d+)"/); - return { strokes: parseSvgStrokes(content), canvasHeight: m ? parseInt(m[1] ?? '0') : null }; + const m = content.match(/viewBox="0 0 (\d+) (\d+)"/); + return { + strokes: parseSvgStrokes(content), + canvasWidth: m ? parseInt(m[1] ?? '0') : null, + canvasHeight: m ? parseInt(m[2] ?? '0') : null, + }; } - return { strokes: [], canvasHeight: null }; + return { strokes: [], canvasWidth: null, canvasHeight: null }; } private async saveSvg() { diff --git a/NOTES.md b/NOTES.md index d8e5c89..45034e5 100644 --- a/NOTES.md +++ b/NOTES.md @@ -5,6 +5,29 @@ Per istruzioni, architettura e task aperti vedi `CLAUDE.md`. --- +## ✅ Task completati (sessione 2026-03-24 — parte 2) + +### Canvas a tutto schermo nell'overlay (DPR + viewScale + fix portrait) — IMPLEMENTATO ✅ + +**Obiettivo**: eliminare le bande laterali vuote nell'overlay di disegno (modal Windows e tab Android), senza pixelazione e con supporto alla rotazione portrait/landscape. + +**Architettura introdotta** (`src/drawing-canvas.ts`): +- `dpr` — device pixel ratio; il buffer interno del canvas è `logicalWidth * dpr` fisici, ma il contesto (`ctx.scale(dpr, dpr)`) lavora sempre in pixel logici → nessuna pixelazione su display Retina. +- `logicalWidth/Height` — dimensione CSS effettiva del canvas; aggiornata ad ogni cambio di orientamento. +- `worldWidth` — spazio coordinate dei tratti salvati nel SVG. **Non scende mai**: se l'utente disegna in landscape (1280px) e poi passa a portrait (720px), `worldWidth` rimane 1280 così il SVG non perde tratti. +- `viewScale = logicalWidth / worldWidth` — fattore di scala orizzontale. In portrait (720/1280 ≈ 0.56) tutto il contenuto viene compresso per mostrarlo senza tagliare. `eventToPoint()` divide la coordinata CSS per `viewScale` per tornare alle coordinate mondo; `drawFullStroke/drawSegment` applicano `ctx.scale(viewScale, 1.0)` prima di disegnare. +- `setDisplayWidth(w)` — chiamato dal `ResizeObserver` ad ogni cambio orientamento: se `w > worldWidth` espande il mondo; se `w < worldWidth` aggiorna solo `logicalWidth` e `viewScale`. +- Ogni modifica di `canvas.width/height` resetta il contesto → `ctx.scale(dpr, dpr)` viene ri-applicato subito dopo. + +**Windows — `DrawingModal`** (`src/editor-view.ts`): un `requestAnimationFrame` dopo la costruzione misura `scrollWrap.clientWidth` e chiama `setDisplayWidth`. + +**Android — `DrawingEditorView`** (`src/editor-view.ts`): un `ResizeObserver` su `scrollWrap` e `el` chiama `setDisplayWidth` ogni volta che il layout cambia (inclusa rotazione). Il riferimento è salvato in `this.displayRo` e disconnesso in `onClose()`. + +**Bug fix — SVG tagliato alla riapertura in portrait** (`src/editor-view.ts`): entrambe le `loadStrokes()` (in `DrawingEditorView` e `DrawingModal`) leggono ora **anche la larghezza** dal `viewBox` dell'SVG (`viewBox="0 0 W H"`). Il canvas viene creato con `worldWidth = savedW ?? settings.canvasWidth`, così riaprendolo in portrait non si perde il `worldWidth` della sessione precedente. +**File**: `src/drawing-canvas.ts`, `src/editor-view.ts`. + +--- + ## ✅ Task completati (sessione 2026-03-24) ### Fix focus perso dopo `window.confirm()` — RISOLTO ✅