Logica dietro a "annulla" per dipingere su una tela

1

Sto lavorando su un modulo che consente agli utenti una funzione di base per la pittura. Voglio supportare l'annullamento di eventuali modifiche apportate alla superficie pittorica. Supponendo che abbiamo uno strato (per semplicità), qual è un modo ragionevole per archiviare la pittura in modo che possano essere annullati?

L'idea che avevo era di incapsulare gli eventi di disegno come una sorta di oggetto annullabile. Se qualcuno preme, disegna e rilascia, i nuovi dati aggiunti saranno contenuti in un oggetto di quali azioni sono state eseguite. Questo evento potrebbe contenere qualcosa come "colore ARBG di dimensione X in [matrice di punti trascinati dall'utente]".

Non sono sicuro che questo sia il modo ottimale per farlo. Se sono stati eseguiti molti comandi di modifica, la mia logica sarebbe quella di riavvolgere l'inizio e quindi di applicare nuovamente ogni "modifica" apportata alla scena, renderizzata e visualizzata, senza che l'interfaccia utente diventi lenta (esclusi i casi marginali).

È fattibile? C'è un modo migliore / più ottimale? Questa è la prima volta che ho tentato una cosa del genere e mi piacerebbe assicurarmi che imparerò il modo migliore per farlo.

Nota: il mio obiettivo non è utilizzare alcuna libreria e gestirla tramite la mia codifica.

    
posta Water 09.03.2015 - 00:05
fonte

3 risposte

2

Il metodo più semplice? Basta limitare la dimensione del buffer di annullamento. Come se tu permettessi solo di annullare le ultime 20 azioni e di mantenere una copia dello stato di 20 azioni fa.

Se annulli, ci saranno 19 azioni nel buffer di annullamento e la ripetizione di quelle è più semplice di tutte dall'inizio.

Tuttavia, estendendoti puoi tenere copie dello stato ogni X azioni. Questa è la stessa idea dei backup diff nei database, ogni mese viene eseguito un backup completo, quindi viene applicata una differenza giornaliera. Poi, quando hai bisogno di ripristinarti, devi solo prendere l'ultimo backup completo e riapplicare le differenze memorizzate dall'ultimo backup.

    
risposta data 09.03.2015 - 00:13
fonte
3

Lo facevo in modo simile al modo in cui mi stai proponendo dove ho calcolato un rettangolo di delimitazione per il tratto, capito quali pixel sono stati modificati, memorizzando i loro valori prima della modifica e registrando una voce di annullamento che avrebbe scambiato il vecchio valori di pixel con i nuovi valori di pixel su annulla / ripeti. E per i grandi colpi l'ho persino ottimizzato in modo che invece di una sub-immagine del rettangolo gigante per il tratto, lo spezzasse in regioni rettangolari più piccole.

Questo è un bel po 'di lavoro per una sola operazione legata all'immagine! È arrivato al punto in cui stavo spendendo più o meno tempo implementando la logica annullabile di ogni operazione come l'operazione stessa e trascorrendo decisamente molto più tempo a eseguire il debug della logica di annullamento errata rispetto alla logica dell'operazione stessa.

Così mi è venuto in mente un approccio molto più semplice che mi permettesse di applicare lo stesso approccio per annullare le operazioni di immagine e renderle non distruttive: un approccio per dominarle tutte. E questo si riduce al solo pensare a un'immagine come una raccolta di riquadri immagine, diciamo 64x64 pixel ciascuno:

Ora,primachel'utenteiniziadipingere,puoisemplicementecopiarel'interadannatacosainunavocediannullamento,adeccezionedellacopiasolodipuntatoricontaticonriferimentoaciascunaporzionedell'immagine.Quindi,mentrel'utenteiniziaadipingeresull'immagine,sirendonoletesseredell'immaginechesonocambiateunivocheconunapprocciosimileaquellodellestrutturedidatipersistentiimmutabiliosipuòusarelacopiaperscritturapertessera(qualunquetipodiinterfacciaeschemadiutilizzosiadattatu,ilpuntoèevitaredicopiarel'immaginenellasuainterezzaquandovengonomodificatesoloalcuneparti):

Orapuoisemplicementeassegnareletesseredell'immaginechesonostatetoccatedall'operazione(letesserescuresopralequalisonostatetoccatedall'utente).Puoicopiaresuperficialmenteletesserenontrattate.

Conquestoapproccio,indipendentementedall'applicazionedell'immaginechevieneapplicataindipendentementedaldisegnodiformeetrattiodaunfiltroimmagineapplicatoaunaselezioneomascheradiimmagine,lalogicadiannullamentosiriduceaquesta:

beforeuseroperation:copyimagetoundoentryonundo/redo:swapuserimagewithundoimage

Questoètutto.Potrebbenonessereteoricamenteottimale,mailragazzosemplificalecoseperquantoriguardal'editingnondistruttivoel'annullamento.Ilpuntoèottimizzarelacopiainmodotalecheognipiastrellachenonvienetoccatadall'utentevengacopiatainmodosuperficialeinmododapotercopiareleimmaginiadestraesinistrasenzapreoccuparsidifaresploderel'usodellamemoriaesprecaretempoeccessivocopiandotuttoancoraeancora.

Oraapplicoquestoapprocciogeneraleall'annullamentoeall'editingnondistruttivopertutto.Bastacopiarelacosainanticipoequindiapplicarel'operazionedell'utente.Ciòspostaladifficoltàdallacorrettezzaall'ottimizzazione.Ilprossimopassoèquellodiottimizzarerendendolacopiaabuonmercatoperlepartichenonsonostatemodificate.

Unmetododiannullamentoperregolarlitutti

Inquestigiorniinrealtàfavoriscoinmodouniformequestometodopertuttiitipidiapplicazionidisparatedisistemidiannullamento.Ilmodomiglioreperfarlofunzionarecorrettamenteècopiaretuttiidatirilevantinellostackdiannullamento,aquelpuntol'implementazionediventacosì:

Beforeuseroperation:old_state=current_stateAfteruseroperation:ifold_state!=current_state:clearredosstackpusholdstatetoundostackOnundo:pushcurrentstatetoredostacksetcurrentstatetotopofundostackpopfromundostackOnredo:pushcurrentstatetoundostacksetcurrentstatetotopofredostackpopfromredostack

Ciòspostatuttelepreoccupazionisull'ottimizzazione(lacopiaeilconfrontodell'equivalenza)piuttostochesullacorrettezza.Esonoriuscitoaottimizzarloasufficienzaancheperidatichenormalmentevannodagigabyteusandometodicomedescrittonell'approccioprecedenteoinalternativastrutturecompattechecatturanocomei"delta" (differenze tra due strutture) fino al punto in cui il sistema di annullamento in realtà richiede anche meno memoria di quando stavo usando lo schema di comando.

E l'ottimizzazione di pesanti strutture di copia (senza la copia profonda di tutto) e la possibilità di confrontarle rapidamente per l'equivalenza ha spesso degli usi più pratici al di fuori del sistema di annullamento per altre cose come la sicurezza delle eccezioni, la sicurezza del thread, non- editing distruttivo, ecc., dal momento che se riesci a copiare le cose in modo economico, allora tendi ad avere meno motivi per causare effetti collaterali nelle tue funzioni in generale.

    
risposta data 17.12.2017 - 03:16
fonte
1

C'è un altro modo in cui puoi gestire ciò che ho visto fatto da alcune app. Invece di mantenere il risultato di ogni azione che l'utente ha commesso, mantieni l'immagine originale, la ricetta (l'elenco di passaggi che l'utente ha seguito) per ottenere l'immagine precedente, l'immagine precedente e l'immagine corrente. Le immagini originali e precedenti così come la ricetta possono essere sul disco piuttosto che nella memoria principale per risparmiare spazio, se necessario.

Quindi ora puoi tornare a qualsiasi punto della catena di annullamento. L'azione di annullamento più comune è annullare l'ultima cosa. Dato che hai l'immagine precedente, puoi semplicemente sostituirla con quella attuale. Inoltre rimuovi l'ultima transazione nella ricetta. In background, puoi ricreare l'immagine precedente precedente dall'immagine originale e la ricetta nel caso in cui l'utente decida di annullare nuovamente.

Se hai memoria per questo, puoi tenere le ultime versioni n in memoria o su disco per un rapido richiamo dopo l'annullamento. Ma puoi sempre annullare l'inizio perché hai la ricetta usata per creare l'immagine corrente. È anche possibile salvare la ricetta in modo che l'utente possa annullare / ripristinare le azioni eseguite durante i precedenti lanci dell'applicazione.

    
risposta data 11.12.2018 - 05:30
fonte

Leggi altre domande sui tag