14 KiB
Obsidian Handwriting Plugin — Contesto per Claude Code
Chi sono / Setup esistente
- Sviluppatore IT con esperienza in C#, JavaScript, TypeScript, Python, SQL Server, VB6
- Vault Obsidian organizzato:
Project/CLIENTI/NOME_CLIENTE/Progetto/- Sottocartella
_docs/sincronizzata via Google Drive - Struttura:
01_Riunioni/,02_Documentazione/,03_UI_Diagrammi/,DECISIONS.md,IDEAS.md,TODO.md
- Sottocartella
- Su PC usa Claude Code che legge i file markdown direttamente
- Tablet Android con pennino (in valutazione acquisto)
Obiettivo: Plugin Obsidian "Handwriting to Markdown"
Cosa voglio
Un plugin Obsidian che inserisce un riquadro canvas inline in un file .md dove posso:
- Scrivere a mano con il pennino (su tablet Android)
- Il testo scritto viene convertito automaticamente in markdown strutturato:
# testo→ H1,## testo→ H2,### testo→ H3- testo→ lista,1. testo→ lista numerata- [ ] testo→ checkbox,- [x]→ checkbox spuntata> testo→ blockquote`testo`→ codice inline,```js ... ```→ blocco codice==testo==→ highlight,**testo**→ grassetto,*testo*→ corsivo~~testo~~→ barrato,---→ separatore
- Il risultato è testo markdown puro inserito nel file
.mdesistente - I disegni/schemi restano come immagine SVG linkati nel markdown
- Bidirezionale: modificabile sia da tablet che da PC
- Nessun formato proprietario — i file devono essere leggibili anche senza il plugin installato
Requisiti tecnici
- Funziona su Android (Obsidian Mobile) e Windows
- Sync via Google Drive (già configurato)
- I file
.mddevono restare leggibili da Claude Code su PC - Preferenza per soluzioni senza dipendenze cloud dove possibile
Stato attuale — Fase 6 (pannello portale inline + bgMode live update + fix CSS tema) — TESTATO
File del plugin (HandTranscriptMd/src/)
| File | Cosa fa |
|---|---|
main.ts |
Entry point: registra embed, editor view, comando, ribbon, settings, previewCallbacks per sync inline↔tab |
settings.ts |
Impostazioni: cartella SVG, dimensioni canvas, sfondo, lingue OCR, chiave API Gemini, toggle hwmHandwritingMode; mostra versione (plugin.manifest.version) e branch (PLUGIN_BRANCH costante hardcoded) nell'header della pagina impostazioni |
drawing-canvas.ts |
Motore disegno Canvas API: Bézier quadratiche, penna, gomma parziale, undo/redo history-based, auto-expand, righe foglio, allowFingerScroll() |
svg-utils.ts |
Conversione tratti ↔ SVG (dati riedit in <desc> JSON), righe e sfondo inclusi nell'SVG |
embed.ts |
Code block processor → preview SVG inline + pannello portale (figlio dello span, position:absolute); click matita → Modal (Windows) o nuova tab (Android); badge mode; onBgModeRemap per aggiornare SVG al cambio tema |
editor-view.ts |
DrawingEditorView (tab dedicata, Android) + DrawingModal (overlay fullscreen, Windows), toolbar completa, auto-save |
recognizer.ts |
IRecognizer interface + GeminiRecognizer: invia PNG base64 a Gemini API, restituisce testo |
md-parser.ts |
parseMarkdown(): post-processa testo OCR riga per riga applicando sintassi markdown |
Architettura attuale (Fase 3+)
Due formati supportati:
- NUOVO (default):
![[_handwriting/hw_xxx.svg]]— SVG visibile nativamente anche senza plugin - LEGACY:
```handwriting {"id":..., "svg":...}```— vecchio formato, mantenuto per compatibilità
Preview inline — Nuovo formato wiki:
- Obsidian renderizza
![[svg]]come<span class="internal-embed image-embed">[img][/img]</span> - MutationObserver su
document.bodyintercetta gli span consrc*="_handwriting/"non appena appaiono nel DOM (funziona sia in reading view che in live preview dove il post-processor non viene chiamato) tryDecorate()con flagdata-hwm-decorated="1"per evitare doppia elaborazione; ritenta dopo 150ms se la classeimage-embednon è ancora presente (caricamento asincrono)- Classe badge mode:
span.classList.toggle('hwm-badge-mode', hwmHandwritingMode)— unica modifica allo span quando lo switch è attivo - Pannello portale (
hwm_portal-panel) con 4 bottoni: ✏️ (apre Modal su Windows / tab su Android), 📄 (converti OCR), ↕️ (comprimi/espandi), ✕ (elimina).- Posizione:
container.appendChild(panel)— il pannello è un figlio diretto dello span;position: absolute; top: 6px; right: 6pxancorato allo span (che haposition: relative). Non più indocument.body. - Nessun RAF loop: eliminato il loop
requestAnimationFramee il listenerscroll. Il pannello segue lo span naturalmente nel DOM. - Cleanup:
plugin.register(() => panel.remove())— il pannello viene rimosso quando il plugin viene disabilitato. - Click dei bottoni: lo span ha
pointer-events: none(per handwriting Android); il pannello hapointer-events: autoin CSS per ripristinare i click. - Nascondere con modal aperto: Desktop →
panel.style.display = 'none'nel click handler, ripristinato nel callbackmodal.onClosed; Mobile →workspace.on('layout-change', ...)per rilevare apertura/chiusura tab.
- Posizione:
- Comprimi/Espandi: usa
container.style.height + overflow: hiddensullo span contenitore — modifica solo l'altezza visibile senza toccare la larghezza dell'immagine. - Refresh immagine:
previewCallbacks.set(embedId, ...)aggiornaimg.srccon cache-bust?t=timestampdopo ogni salvataggio dalla tab editor (Obsidian non aggiorna automaticamente![[svg]]in live preview quando il file cambia) - NO data-URI: il src dell'img resta sempre l'URL vault (
http://localhost/_capacitor_file_/...) — un data-URI verrebbe interpretato da Android come drawing surface
Preview inline — Formato legacy:
- Mostra l'SVG come CSS
background-imagesu un<div>(no<img>) - 3 bottoni inline (
<div role="button">, non<button>) dentro il container del code block - Bottone portale singolo (
hwm_portal-btn, cerchio matita) indocument.bodyper aprire la tab editor
Editor disegno — due modalità (editor-view.ts):
DrawingEditorView extends ItemView — usato su Android:
- Canvas in un DOM completamente separato da CodeMirror → nessun conflitto handwriting Android
- Top bar: bottone ← a sinistra (chiude tab), toolbar completa a destra
- Scroll container:
overflow-y: autoper canvas più grandi dello schermo - Finger scroll:
allowFingerScroll(scrollContainer)— dito scrolla, penna disegna - Auto-save debounced 2s +
plugin.refreshPreview()aggiorna la preview inline
DrawingModal extends Modal — usato su Windows:
- Overlay fullscreen (
hwm_modal:95vw × 90vh) che appare sopra il documento - Stessa toolbar e stesso canvas di
DrawingEditorView onClosed?: () => void— callback invocato alla chiusura per ripristinarepanel.style.displaynel pannello portalereplaceCodeBlock/removeCodeBlockgestiscono sia formato wiki![[svg]]che legacy (prova prima wiki, poi code block come fallback)private bgModeListenerregistrato inbuildEditor()(dopocolorBtns) e rimosso inonClose()— aggiorna topbar, toolbar, pallini colore e canvas al cambio bgMode in tempo reale (utile se le impostazioni fossero accessibili con modal aperto)
Comportamento bottone matita (pannello portale):
Platform.isDesktop→new DrawingModal(...).open()— si apre nella stessa finestraPlatform.isMobile→workspace.getLeaf('tab')conDrawingEditorView- Bottone matita nascosto:
modalOpen || tabOpen— scompare sia quando Modal è aperto (Windows) sia quando la tab è aperta (Android)
Modalità handwriting (hwmHandwritingMode):
- Switch nelle impostazioni: "Modalità handwriting Android"
- Se ON:
document.body.classList.add('hwm-handwriting-mode')+ classehwm-badge-modesullo span → CSS riduce l'SVG a 48px di altezza (badge/thumbnail) - Se OFF (default): SVG piena, comportamento normale
- Si applica immediatamente senza ricaricare il plugin (toggle in settings chiama
document.body.classList.toggle(...); all'avvio viene applicato inregisterEmbed())
Funzionalità implementate
- Preview SVG inline nel markdown via code block
handwriting(immagine statica, no canvas) - Editor in tab dedicata — click sulla preview apre una tab Obsidian separata
- Curve smooth — Bézier quadratiche con tecnica midpoint
- Gomma parziale — cancella solo i punti toccati, taglia i tratti in segmenti
- Undo/Redo — basato su history di stati (funziona sia per disegno che per gomma)
- Auto-expand — il canvas si espande con animazione smooth + auto-scroll nel container
- Clear → reset alla dimensione di default con animazione
- Righe orizzontali — foglio a righe (32px), sia nel canvas che nell'SVG
- Temi sfondo — chiaro/scuro/custom con color picker nelle impostazioni
- Remapping colori automatico — i tratti si adattano al cambio tema (nero↔bianco, blu↔azzurro, ecc.)
- Toolbar completa — penna, gomma, 4 colori, undo, redo, clear, converti, salva, elimina (X)
- Elimina riquadro — da inline (3 bottoni) o da tab editor
- Auto-save — salvataggio debounced 2s + refresh preview inline
- SVG standard — file
.svgnella cartella_handwriting/, visibili da qualsiasi dispositivo - Palette colori adattiva — colori scuri su sfondo chiaro, colori chiari su sfondo scuro
- OCR via Gemini — SVG → PNG base64 → Gemini 3.1 Flash Lite →
md-parser→ sostituisce code block - Archiviazione SVG — dopo la conversione, SVG spostato in
_handwriting/_converted/AAAA-MM-GG_HH-MM-SS.svg - Settings OCR — chiave API Gemini (campo password) + lingue OCR configurabili (default:
it, en) - Supporto Android — penna disegna, dito scrolla, nessun conflitto handwriting
- Comprimi/Espandi — preview inline si può compattare all'altezza di default (freccia con rotazione 180°)
- Bottone portale — pannello
position: absolutefiglio diretto dello span (no RAF loop, no scroll listener, no document.body) - Preview non tappabile — click handler rimosso; l'unico modo per aprire l'editor è il bottone matita nel pannello portale
- Modal Windows — click matita su Desktop apre
DrawingModal(overlay fullscreen nella stessa finestra, no tab separata) - Nuova tab Android — click matita su Mobile apre
DrawingEditorViewin tab separata - Switch handwriting — toggle nelle Settings: se ON, i riquadri mostrano solo una piccola anteprima (48px) per non bloccare lo stylus handwriting nel testo
Embedding nel markdown
Nuovo formato (default):
![[_handwriting/hw_abc123.svg]]
Il file SVG è visibile come immagine anche senza il plugin. I tratti sono salvati come JSON in <desc class="hwm-strokes"> dentro l'SVG.
Legacy (backward compat):
```handwriting
{"id":"hw_abc123","svg":"_handwriting/hw_abc123.svg"}
```
Deploy (comandi copia-incolla per PowerShell)
Nota: usare Git Bash esplicitamente con
--loginperché PowerShell usa WSL bash di default (che non riconosce i percorsi Windows) e senza--loginil PATH non include i tool Unix (dirname, cp, wc, ecc.).
# Build + deploy al vault locale (solo PC)
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; & "C:\Program Files\Git\bin\bash.exe" --login deploy.sh
# Build + deploy su Google Drive (per testare su tablet Android)
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; & "C:\Program Files\Git\bin\bash.exe" --login cloudDeploy.sh
Percorsi vault
- Vault locale di test:
C:\Projects\CLIENTI\IOTTI\IOTTI_APP\_docs\handwriting-to-markdown\- Plugin in
.obsidian\plugins\handwriting-to-markdown\
- Plugin in
- Vault Google Drive (tablet):
C:\Users\gabri\Il mio Drive (gabrielecusato@gmail.com)\Projects\handwriting-to-markdown\
Come sviluppare
# Dev mode (watch)
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; npm run dev
# Dopo ogni modifica per testare su PC:
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; bash deploy.sh
# Dopo ogni modifica per testare su tablet Android:
cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; node esbuild.config.mjs production; bash cloudDeploy.sh
# In Obsidian: Ctrl+P → "Reload app without saving"
Note architetturali
Come funziona il flusso OCR
canvas.getStrokes()→ array diStroke[]strokesToSvg()→ stringa SVG (già usata per il salvataggio)DOMParser→SVGElementDOMsvgToBase64Png(svgEl)→ PNG base64 via canvas HTML temporaneo (Blob URL → Image → canvastoDataURL)GeminiRecognizer.recognize(base64)→ POST a Gemini coninline_data+ promptparseMarkdown(testo)→ post-processing riga per rigaarchiveSvg()→ sposta SVG in_converted/con nome timestampreplaceEmbedWithMarkdown()→ regex sul file.mdsostituisce il code block
Perché Gemini e non API native Android
navigator.createHandwritingRecognizerera un Origin Trial Chrome sperimentale, mai arrivato a stable- Obsidian Mobile usa WebView → API non disponibile su Xiaomi Pad 5 né altri dispositivi
window.prompt()non funziona in Electron (Obsidian desktop)- Gemini REST API funziona identicamente su Windows e Android
Modello Gemini usato
gemini-3.1-flash-lite-preview — documentazione: https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-lite-preview
Storico sessioni, bug risolti e funzionalità completate → vedi
NOTES.mdI task completati vanno spostati inNOTES.md; in questa sezione restano solo i task ancora da fare.
Regole di sviluppo
Keyword parser: ogni volta che si aggiunge o rimuove una keyword accettata in
src/md-parser.ts, va aggiornata anche la tabellaKEYWORDSinsrc/settings.ts(sezione "Keyword riconosciute dal parser OCR"). Le due liste devono essere sempre sincronizzate.
Prossimi passi
Task aperti
- Keyword personalizzate nelle impostazioni —
src/settings.ts+src/md-parser.ts:- Permettere all'utente di aggiungere keyword custom nella pagina impostazioni
- Ogni keyword custom ha: nome (es.
FIRMA), output markdown (es.— Mario Rossi) - Le keyword custom vengono caricate in
expandKeywordsinsieme a quelle predefinite