fixato anche il problema delle barre vuote nell'overlay di disegno e la versione a 1.0.0

This commit is contained in:
gabriele-cusato 2026-03-24 22:27:44 +01:00
parent 7da1c09850
commit 5e76163f90
4 changed files with 161 additions and 33 deletions

View file

@ -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",

View file

@ -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();
}
}

View file

@ -47,6 +47,8 @@ export class DrawingEditorView extends ItemView {
private saveTimer: ReturnType<typeof setTimeout> | 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() {

View file

@ -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 ✅