MaCocoa 051

Capitolo 051 - Rotazioni e Rivoluzioni, parte 1

Capitolo sofferto; avevo cominciato ardito con l'idea di introdurre la possibilità di ruotare gli elementi grafici di un angolo a piacere. Mi sono accorto strada facendo che le cose si sarebbero complicate troppo, a meno di non disfare alcune classi. Ho allora stravolto la struttura degli elementi, rifacendo una consistente parte della classe CCE_BasicForm (e discendenti). Nel farlo, mi sono imbattuto in nuovi concetti, potenti ma non troppo semplici. Il testo è diviso in due capitoli per non appesantire troppo le pagine.

Sorgenti: documentazione Apple.

Prima stesura: 18 agosto 2004.

Trasformazioni affini

C'è un concetto fondamentale, che deve essere chiaro a tutti coloro che producono programmi di grafica vettoriale per un computer, in mezzo tra matematica e geometria. Sto parlando della trasformazione affine, e della conseguente classe messa a disposizione da Cocoa, NSAffineTransform.

Per disegnare punti, linee e curve, c'è bisogno di un sistema di coordinate (cartesiane ortogonali) di riferimento. In generale, esistono diversi sistemi di riferimento SdR: ad esempio, c'è il SdR relativo al monitor, e c'è il SdR relativo alla finestra. Lo stesso pixel dello schermo ha una diversa rappresentazione, sotto forma di coordinate, nel SdR del monitor e nel SdR della finestra in cui si trova. Normalmente, è possibile passare da un SdR ad un altro attraverso semplici calcoli: l'insieme di questi calcoli è chiamato Trasformazione (affine, perché mantiene alcune proprietà geometriche, quali il parallelismo). Nel caso dei SdR citati, la trasformazione è una semplice traslazione; per passare dalle coordinate (x,y) del punto P nel SdR del monitor a quelle (x',y') nel SdR della finestra, basta conoscere le coordinate (Tx,Ty) dell'origine del SdR della finestra nel SdR del monitor. Allora i calcoli da effettuare sono:

x = x' + Tx
y = y' + Ty

figura 01

figura 01

Questo giochino funziona se i due SdR hanno lo stesso orientamento, ovvero se l'angolo formato dai due semiassi positivi delle ascisse è nullo. Tuttavia, in molti casi, ciò non è vero: occorre tenere conto anche della possibilità di una rotazione relativa tra i due sistemi di riferimento. Per ottenere la trasformazione ci sono alcune considerazioni trigonometriche da fare, ma alla fine, se non sbaglio, per passare dalle coordinate (x,y) del punto P nel primo SdR alle coordinate (x',y') nel secondo SdR, che condivide col primo la stessa origine, ma è ruotato di un angolo alfa, si ha:

x = x' cos(alfa) - y' sin (alfa)
y = x' sin(alfa) + y' cos(alfa)

figura 02

figura 02

Trasformazioni più complesse della semplice traslazione e rotazione attorno all'origine si ottengono componendo tra loro queste due trasformazioni semplici. Ad esempio, una rotazione attorno ad un punto Q qualunque si ottiene con tre trasformazioni semplici: una traslazione nel punto Q, la rotazione attorno alla nuova origine Q, una nuova traslazione per tornare al punto di partenza. È estremamente importante la sequenza: le stesse trasformazioni in sequenza diversa producono risultati diversi. Ad esempio, con la sequenza precedente, se eseguo prima le due traslazioni, queste si annullano l'una con l'altra, ed il risultato della sequenza traslazione - traslazione - rotazione produce la sola rotazione nell'origine, mentre il risultato di traslazione - rotazione - traslazione produce come risultato la rotazione attorno al punto Q, ben diverso dall'origine.

Matrici di classe

Ora, sarebbe molto comodo avere un meccanismo che permetta di rappresentare in modo semplice una trasformazione complessa, e di ottenere tale trasformazione combinando in maniera opportuna le trasformazioni semplici. Tale meccanismo esiste, ma utilizza un concetto matematico che per me è molto semplice e pratico, ma è spesso superiore alla normale cultura matematica dispoibile: sto parlando delle matrici.

Le trasformazioni bidimensionali si possono rappresentare in maniera pratica attraverso matrici di tre elementi per tre, con la convenzione di rappresentare i punti nel piano attraverso un vettore di tre elementi. Dato un punto P=(x,y), lo si rappresenta con il vettore [x y 1]; la trasformazione è invece rappresentata da una matrice 3x3:

T = [ a d 0
      b e 0
      c f 1 ]

La trasformazione del punto P attraverso la trasformazione affine T produce il nuovo punto (lo stesso punto, ma con diversa rappresentazione) P' = P T. Attraverso l'algebra delle ed alcune semplici considerazioni, si arriva facilmente ad un insieme di regole per gestire la composizione delle trasformazioni.

Tuttavia, piuttosto che dilungarmi in queste considerazioni matematiche, illustro la classe NSAffineTransform, che semplifica il tutto nascondendo questi stupidi dettagli.

La classe NSAffineTransform è un meccanismo che rappresenta queste trasformazioni affini e permette di lavorare comodamente senza preoccuparsi più di tanto di matrici e vettori.

Ci sono ovviamente dei metodi per costruire trasformazioni, da zero (+transform, che produce la trasformazione identica, che lascia tutto come sta) o a partire da un'altra trasformazione (initWithTransform). A questo punto, ci sono metodi per costruire trasformazioni più complesse a parire dalle trasformazioni semplici: il metodo translateXBy:yBy: concatena una traslazione, i metodi rotateByDegrees: e rotateByRadians: eseguono rotazioni nell'origine (in gradi o radianti); ci sono metodi per comporre trasformazioni (appendTrasform: e prependTransform:) e per costruire la trasformazione inversa (invert). Chi fosse curioso di vedere le matrici che eseguono le operazioni di trasformazione, può usare il metodo transformStruct che appunto restituisce tale matrice.

Poi ci sono dei metodi per applicare queste trasformazioni: per trasformare le coordinate di un punto c'è transformPoint:, per trasformare dimensioni (cioé, per strutture NSSize) c'è transformSize:, ed infine per trasformare interni percorsi (istanze della classe NSBezierPath) c'è transformPath:.

Ci sono poi due importanti metodi, set e concat, per applicare le trasformazioni indicate al contesto grafico corrente. Ora, quando sono in corso operazioni di disegno all'interno di una finestra, Cocoa mantiene un contesto grafico, ovvero un insieme di variabili proprie di quell'ambiente di disegno; tra le variabili, il sistema di riferimento corrente, il colore, le dimensioni delle linee, eccetera. Se miaTF è una istanza della classe NSAffineTransform, l'istruzione [ miaTF set ] impone al corrente contesto grafico la trasformazione indicata. L'istruzione [ miaTF concat ], invece, concatena la trasformazione indicata alla corrente in uso. Per spiegare meglio la cosa, faccio un esempio; supponiamo di voler disegnare un rettangolo, descritto dal NSBezierPath mioPath, ruotato di venti gradi rispetto alle coordinate della view corrente. Costruisco allora una trasformazione miaFT più o meno in questo modo:

miaTF = [ NSAffineTransform transform ];
[ miaTF rotateByDegrees: 20 ];

Quando, in un qualche metodo di disegno, sono nel punto di tracciatura, eseguo la seguente sequenza:

[ miaTF concat ];
[ mioPath stroke ] ;

Ho per prima cosa cambiato il sistema di riferimento, effettuando una rotazione in aggiunta alla trasformazione correte (che è quella che trasforma il SdR del monitor nel SdR della view corrente); poi ho disegnato il rettangolo; questo, di per sè, non è ruotato; ma avendo cambiato il sistema di riferimento, sarà disegnato ruotato di venti gradi.

In alternativa, avrei potuto lavorare in questo modo:

mioPath = [ miaTF transformPath: mioPath ];
[ mioPath stroke ];

Qui, invece di cambiare il SdR, ho trasformato il path ruotandolo come richiesto, e poi ho disegnato direttamente il path.

I nuovi elementi grafici

Facendo tesoro delle trasformazioni affini, ho ristrutturato le classi CCE_BasicForm e discendenti in maniera completamente differente rispetto al passato. Parto dal seguente principio: ogni elemento ha un proprio SdR, all'interno del quale hanno luogo le varie operazioni di disegno. In questo modo, ogni elemento possiede un punto di origine del SdR chiamato drawPoint ed un angolo di rotazione rotAngle rispetto al SdR della finestra. Ogni elemento possiede poi una dimensione (localSize, una struttura NSSize), ovvero una larghezza ed una altezza all'interno della quale si trova il disegno dell'elemento. Il disegno dell'elemento avviene sempre attraverso un elemento della classe NSBezierPath (tutti, a parte CCE_ElemGroup...). Completano le variabili d'istanza le due trasformazioni affini localTF e reverTF che permettono di passare dal SdR della finestra al SdR proprio dell'elemento. La classe CCE_BasicForm risulta quindi così costruita nelle sue variabili d'istanza:

@interface CCE_BasicForm : NSObject {
    // identificatore dell'elemento
    long            objID ;
    // punto nodale dell'elemento
    NSPoint            drawPoint ;
    // angolo di rotazione corrente
    float            rotAngle ;
    // lista degli handle, sono al massimo otto
    int                numOfHdl ;
    NSPoint            hdlList[8] ;
    // la view in cui si trova
    CoverView        * ownerView ;

    // metto da parte la tf affine
    NSAffineTransform * localTF ;
    NSAffineTransform * reverTF ;
    // dimensioni del rettangolo in coordinate locali
    NSSize            localSize ;
    // tengo il percorso per meglio disegnare
    NSBezierPath    * theDrawPath ;

    // spessore della linea di contorno
    float            cceLineWidth ;
    // colore dell'eventuale interno e del contorno
    NSColor            * cceFillColor ;
    NSColor            * cceLineColor ;
    // se devo colorare l'interno o meno
    BOOL            cceIsFilled ;
    BOOL            cceIsStroked ;
    // se l'elemento e' selezionato o meno
    BOOL            cceIsSelected ;
    // se l'lemento puo' essere manipolato o meno
    BOOL            cceIsLocked ;
}

Scompare il rettangolo elemLimit, che tanto mi aveva dato da fare nella precedente realizzazione; più avanti si vedrà come è degnamente rimpiazzato. Compaiono degli ovvi e banali nuovi metodi accessor.

L'inizializzazione dell'elemento è cambiata:

- (id)
initWithId: (int) ident inView: (CoverView*) cv
{
    // classica inizializzazione della superclasse
    self = [super init];
    if (self)
    {
        // inizializzazione delle variabili d'istanza comune
        // objId, e' compito del chiamante
        objID = ident ;
        // disegnato nell'origine, per il momento
        drawPoint = NSMakePoint( 0, 0 );
        // con angolo di rotazione nullo
        [ self setRotAngle: 0.0F ];
        // aggiusto le trasformazioni affini
        [ self calcLocalTranform ];
        [ self setTheDrawPath: [NSBezierPath bezierPathWithRect:
                    NSMakeRect(0.0, 0.0, 1.0, 1.0) ] ];
        // ci sono otto handle standard
        numOfHdl = 8 ;
        [ self buildHdlList ];
        // coverView, e' compito del chiamante
        ownerView = cv ;
        // spessore della linea, poca roba
        ...
    }
    return self;
}

Ho infatti preferito racchiudere in una unica istruzione l'inizializzazione di un elemento dotandolo di identificatore e di view in cui rappresentarlo. C'è una istruzione importante, il calcolo delle trasformazioni:

- (void)
calcLocalTranform
{
    // trasformazione affine per la rototraslazione
    NSAffineTransform *transform = [NSAffineTransform transform];
    // rototraslazione
    [ transform translateXBy: drawPoint.x yBy: drawPoint.y ];
    [ transform rotateByDegrees: rotAngle ];
    [ self setLocalTF: transform ];
    // gia' che ci sono, calcolo anche l'inversa
    [ transform invert ];
    [ self setReverTF: transform ];
}

La costruzione della trasformazione è molto semplice: si esegue la traslazione nel punto origine o poi la rotazione dell'angolo previsto; calcolo anche la trasformazione inversa, che è sempre utile.

Per vedere come questi nuovi concetti si applicano, passo a discutere la realizzazione della classe CCE_Rect. Cambia anche qui il metodo per l'inizializzazione.

- (id)
initWithId: (int) ident inView: (CoverView*) cv withRect: (NSRect) aRect andAngle: (float) angle
{
    self = [ super initWithId: ident inView: cv ] ;
    if ( self )
    {
        // uso l'origine di aRect come punto di partenza
        [ self setRotAngle: angle ] ;
        [ self buildMeFromPt: aRect.origin withSize: aRect.size ];
    }
    return self ;
}

Oltre all'identificatore e alla view che lo contiene, il rettangolo è costruito a partire da un rettangolo (ovvio) e dall'angolo di rotazione. Il metodo si limita a chiamare il costruttore della superclasse, ad impostare l'angolo di rotazione con un metodo accessor, e a chiamare il nuovo metodo buildMeFromPt:withSize:, che sostituisce il precedente metodo per due punti.

- (void)
buildMeFromPt: (NSPoint) stPt withSize: (NSSize) dim
{
    // stPt sono le coordinate (view) del punto base
    // dim e' la distanza (locale) del secondo punto da stPt
    NSRect aRect ;
    // trasformo le distanze in coordinate locali
    localSize = dim ;
    // imposto il nuovo punto iniziale di disegno
    drawPoint = stPt ;
    // l'angolo di rotazione non cambia
    // aggiorno la trasformazione delle coordinate
    [ self calcLocalTranform ];
    // rettangolo che costuisce l'elemento
    aRect = NSMakeRect( 0, 0, localSize.width, localSize.height );
    // per mia convenienza conservo il percorso
    [ self setTheDrawPath: [NSBezierPath bezierPathWithRect: aRect ] ];
    // metto a posto il rettangolo che lo contiene
    [ self buildHdlList ] ;
    [ ownerView setNeedsDisplay: YES ];
}

Il metodo è piuttosto semplice e lineare: aggiusta le variabili interne, calcolando le nuove trasformazioni, predispone poi il percorso theDrawPath. Il percorso rappresenta sempre il rettangolo nel SdR dell'elemento, ed è quindi costruito a partire da un rettangolo posizionato a (0,0) e di dimensione opportuna. Scomparsa la variabile d'istanza elemLimit, il cui metodo accessor forzava il calcolo degli handle, occorre esplicitamente calcolare le coordinate degli handle. Anche in questo caso, gli handle possiedono corrdinate nel SdR dell'elemento, per cui il metodo corrispondente diventa:

- (void)
buildHdlList
{
    // gli handle sono genericamente ai quattro estremi del
    // rettangolo limite e nei quattro punti mediani
    hdlList[0] = NSMakePoint( 0, 0) ;
    hdlList[2] = NSMakePoint( localSize.width, 0) ;
    hdlList[4] = NSMakePoint( localSize.width, localSize.height) ;
    hdlList[6] = NSMakePoint( 0, localSize.height ) ;
    hdlList[1].x = (hdlList[0].x + hdlList[2].x ) * 0.5 ;
    hdlList[1].y = (hdlList[0].y + hdlList[2].y ) * 0.5 ;
    hdlList[3].x = (hdlList[2].x + hdlList[4].x ) * 0.5 ;
    hdlList[3].y = (hdlList[2].y + hdlList[4].y ) * 0.5 ;
    hdlList[5].x = (hdlList[4].x + hdlList[6].x ) * 0.5 ;
    hdlList[5].y = (hdlList[4].y + hdlList[6].y ) * 0.5 ;
    hdlList[7].x = (hdlList[6].x + hdlList[0].x ) * 0.5 ;
    hdlList[7].y = (hdlList[6].y + hdlList[0].y ) * 0.5 ;
}

E adesso, attenzione al metodo di disegno del rettangolo:

- (void)
drawElement: (NSRect) inRect
{
    // per salvare il contesto
    NSGraphicsContext *context = [NSGraphicsContext currentContext];
    [context saveGraphicsState];
    [ localTF concat ];
    // disegno del testo
    if ( cceIsStroked )
    {
        // imposto il colore del contorno
        [ cceLineColor set ];
        // imposto lo spessore del contorno
        [ theDrawPath setLineWidth: [ self cceLineWidth ]];
        // disegno il contorno
        [ theDrawPath stroke ];
    }
    // se il rettangolo e' pieno
    if ( cceIsFilled )
    {
        // imposto il colore del pieno
        [ cceFillColor set ];
        // riempi tutto
        [ theDrawPath fill ];
    }
    [ self drawHandles ] ;
    // ripristino del contesto
    [context restoreGraphicsState];
}

La prima istruzione si preoccupa di memorizzare il contesto grafico corrente della view; infatti l'operazione successiva lo modifica, cambiando il SdR concatenando la trasformazione relativa all'elemento. A questo punto, tornano le consuete operazioni di disegno, che avvengono nel SdR dell'elemento. Alla fine, sono disegnati gli handle ed è ripristinato il contesto grafico precedente (cioé, ritorna il SdR della view). Se così non fosse, il SdR utilizzato per il disegno successivo sarebbe ancora quello dell'elemento rettangolare (cosa che invece non va bene).

In effetti, tutti i metodi di disegno dei vari elementi avranno un prologo (salvataggio del contesto e impostazione del nuovo SdR) ed un epilogo (disegno degli handle e ripristino del contesto), per cui ho diviso il lavoro in due passi. La classe CCE_BasicForm si occupa del prologo e dell'epilogo, ed ogni elemento delle proprie operazioni specifiche; di conseguenza in CCE_BasicForm si trova il metodo generale di disegno:

- (void)
drawElement: (NSRect) inRect
{
    // per salvare il contesto
    NSGraphicsContext *context = [NSGraphicsContext currentContext];
    [context saveGraphicsState];
    [ localTF concat ];
    [ self specificDrawing: inRect ];
    [ self drawHandles ] ;
    // ripristino del contesto
    [context restoreGraphicsState];
}

- (void)
specificDrawing: (NSRect) inRect
{
    // da sovrascrivere
}

All'interno delle varie sottoclassi, i metodi specifici; ad esempio per il rettangolo, l'ormai consueta sequenza di istruzioni:

- (void)
specificDrawing: (NSRect) inRect
{
    // disegno del contorno
    if ( cceIsStroked )
    {
         ...
    }
    // se il rettangolo e' pieno
    if ( cceIsFilled )
    {
         ...
    }
}

Le altre classi di elementi

Gli elementi CCE_Circle e CCE_Text sono stati modificati; con l'intento di semplificarli sono diventati sottoclassi di CCE_Rect (invece che direttamente di CCE_BasicForm); in effetti per i cerchi le cose sono piuttosto semplici. Per l'inizializzazione:

- (id)
initWithId: (int) ident inView: (CoverView*) cv withRect: (NSRect) aRect andAngle: (float) angle
{
    self = [ super initWithId: ident inView: cv withRect: aRect andAngle: angle ] ;
    if ( self )
    {
        // aggiusto il path perche' sia un cerchio
        [ self setTheDrawPath: [NSBezierPath bezierPathWithOvalInRect:
            NSMakeRect( 0, 0, localSize.width, localSize.height) ]];
    }
    return self ;
}

Si utilizza l'inizializzazione propria di un rettangolo, per poi rimpiazzare brutalmente il percorso con un ovale invece che lasciare il rettangolo standard. La stessa cosa succede per la costruzione per punto e dimensione:

- (void)
buildMeFromPt: (NSPoint) stPt withSize: (NSSize) dim
{
    NSRect aRect ;
    [ super buildMeFromPt: stPt withSize: dim ] ;
    aRect = NSMakeRect( 0, 0, localSize.width, localSize.height );
    [ self setTheDrawPath: [NSBezierPath bezierPathWithOvalInRect: aRect ] ];
}

Addirittura, il metodo di disegno non va riscritto, in quanto l'ovalità dell'elemento è racchiusa dal percorso.

Più complicata, ma solo per la gestione delle classi relative al testo, la classe CCE_Text. L'inizializzazione diventa:

- (id)
initWithId: (int) ident inView: (CoverView*) cv
    withAttributedString: (NSAttributedString *) theText inRect: (NSRect) aRect withAngle: (float) angle
{
    self = [ super initWithId: ident inView: cv withRect: aRect andAngle: angle] ;
    if ( self )
    {
        NSTextStorage * loctextStorage ;
        NSLayoutManager *layoutManager;
        NSTextContainer *textContainer;
        // layout gestisce come il testo e' rappresentato
        loctextStorage = [[[NSTextStorage alloc] initWithAttributedString: theText] autorelease ];
        layoutManager = [[[NSLayoutManager alloc] init] autorelease ];
        [loctextStorage addLayoutManager:layoutManager];
        // indica dove il testo e' rappresentato
        textContainer = [[[NSTextContainer alloc] init] autorelease ];
        // dovrei specificare un NSSize per il textcontainer
        [ theTextCont setContainerSize: localSize ];
        [layoutManager addTextContainer:textContainer];
        // assegno il textStorage
        [ self setTextStorage: loctextStorage ];        
        theTextCont = textContainer ;
    }
    return self ;
}

Il metodo per il disegno è bellissimo:

- (void)
specificDrawing: (NSRect) inRect
{
    NSRange glyphRange ;
    // recupero il primo layout
    NSLayoutManager *layoutManager = [ [ textStorage layoutManagers ] objectAtIndex: 0] ;
    [ super specificDrawing: inRect ] ;
    // disegno del testo
    [ theTextCont setContainerSize: localSize ];
    // recupero il range del testo
    glyphRange = [layoutManager glyphRangeForTextContainer: theTextCont ];
    [ layoutManager drawGlyphsForGlyphRange:glyphRange atPoint: NSMakePoint(0,0) ];
}

Si sfruttano brutalmente le capacità della superclasse CCE_Rect per il disegno del rettangolo di contorno ed il riempimento, e si aggiungono le istruzioni specifiche per il testo.

Anche la classe CCE_Line è stata ovviamente modificata. Tuttavia, le diversità rispetto alla classe CCE_Rect si sono ridotte, anche se la maggior parte delle modifiche appariranno più avanti. Ci sono i soliti tre metodi di inizializzazione, costruzione e disegno:

- (id)
initWithId: (int) ident inView: (CoverView*) cv fromPt: (NSPoint) stPt toPt: (NSPoint) endPt
{
    self = [ super initWithId: ident inView: cv ] ;
    if ( self )
    {
        numOfHdl = 2 ;
        [ self buildMeFromPt: stPt withSize: NSMakeSize(endPt.x-stPt.x, endPt.y-stPt.y) ];
        [ self setCceIsStroked: TRUE ];
    }
    return self ;
}

Anche qui, si sfrutta la superclasse ed il metodo di costruzione, sul quale occorre spendere due parole.

- (void)
buildMeFromPt: (NSPoint) stPt withSize: (NSSize)newSize
{
    NSBezierPath *path = [NSBezierPath bezierPath];
    // calcola la lunghezza del segmento
    float c3 = sqrt( newSize.width*newSize.width + newSize.height*newSize.height) ;
    // calcolo l'angolo formato dal segmento col sistema di riferimento della
    // finestra, e lo trasformo in numero tra 0 e 360
    [ self setRotAngle: (180 * atan2( newSize.height, newSize.width) / M_PI) ] ;
    // il punto base e' il primo punto
    drawPoint = stPt ;
    [ self calcLocalTranform ];
    // la dimensioen della linea e' la sua lunghezza e zero.
    localSize = NSMakeSize( c3, 0) ;
    // costruisco il percorsoin coordinate locali
    [path moveToPoint: NSMakePoint(0,0) ];
    [path lineToPoint: NSMakePoint(c3,0) ];
    // lo assegno
    [ self setTheDrawPath: path ];
    // costruisco gli handle
    [ self buildHdlList ] ;
    [ ownerView setNeedsDisplay: YES ];
}

Per rappresentare un segmento, ho deciso di mantenere la stessa filosofia: spostamento in punto origine e rotazione. Di più: la linea riposa sempre sulla retta delle ascisse del SdR locale. Per costruire dunque la linea, mi occorre calcolare la sua lunghezza: è la radice quadrata della somma dei quadrati costruiti sulla variabile NSSize. Infatti NSSize contiene nei suoi campi width ed height la distanza (nel SdR della view, attenzione) tra i due punti costituenti il segmento. L'angolo di rotazione, con un po' di trigonometria, è dato dall'arcotangente del rapporto height/width. Per mantenere correttamente il segno e quindi il valore dell'angolo, uso la funzione atan2(,), che si trova allo scopo nelle librerie matematiche standard del C. Imposto l'angolo di rotazione (dopo averlo convertito in gradi: atan2(,) restituisce radianti), il nuovo punto origine e calcolo le matrici di trasformazione. Poi costruisco il percorso con una linea che va dall'origine fino alla lunghezza prevista, sull'asse delle ascisse.

Devo costruire gli handle:

- (void)
buildHdlList
{
    // due soli handle, che sono i punti estremi
    hdlList[0] = NSMakePoint( 0, 0 ) ;
    hdlList[1] = NSMakePoint( localSize.width, 0) ;
}

E poi, il metodo di disegno:

- (void)
specificDrawing: (NSRect) inRect
{
    // imposto il colore
    [ cceLineColor set ];
    // imposto lo spessore della linea
    [ theDrawPath setLineWidth: [ self cceLineWidth ]];
    // disengo la linea
    [ theDrawPath stroke ];
}

Questa nuova struttura degli elementi ha un interessante impatto sul resto dei metodi di CCE_BasicForm; questi ne risulteranno semplificati, e con meno differenze tra i vari elementi.

Creazione e modifica

Il metodo per gestire la creazione di un elemento (rettangolare in prima battuta) è presente all'interno della classe CCE_BasicForm.

- (BOOL)
handleCreation: (NSEvent *)theEvent
{
    CoverView * view = [self ownerView] ;
    // recupero la coordinata locale
    NSPoint     point1 = [ view convertPoint:
        [theEvent locationInWindow] fromView:nil];
    NSPoint     point2 ;
    BOOL         shiftKeyPress ;
    NSSize        tmpSize ;
    // piglio il punto piu' vicino sulla griglia (se necessario)
    point1 = getNearestSnapPoint( point1, view);
    // inizializzo l'aspetto dell'oggetto (praticamente vuoto)
    [ self buildMeFromPt: point1 withSize: NSMakeSize(1,1) ] ;
    // continuo a ciclare in attesa di un mouseup
    do {
        // recupero il prossimo evento, che sia un movimento del mouse
        // oppure un mouseup (che finisce il tutto)
        theEvent = [[view window] nextEventMatchingMask:
            (NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
        shiftKeyPress = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
        // recupero la coordinata della finestra
        point2 = [view convertPoint:[theEvent locationInWindow] fromView:nil];
        point2 = getNearestSnapPoint( point2, view);
        tmpSize = NSMakeSize(point2.x-point1.x, point2.y-point1.y) ;
        if ( shiftKeyPress )
        {                
            // costringo il rettangolo ad essere quadrato
            float delta1 = tmpSize.width ;
            float delta2 = tmpSize.height ;
            float    dirx = 1, diry = 1 ;    
            if ( delta1 < 0 )
            {
                delta1 = - delta1 ; dirx = -1 ;
            }
            if ( delta2 < 0 )
            {
                delta2 = - delta2 ; diry = -1 ;
            }
            // piglio la dimensione piu' corta
            if ( delta1 < delta2 )
                tmpSize.height = diry * delta1 ;
            else
                tmpSize.width = dirx * delta2 ;
        }
        // aggiusto l'aspetto dell'oggetto    (vedi nota)    
        [ self buildMeFromPt: point1 withSize: tmpSize ] ;
        // forzo un ridisegno della finestra
        [ view setNeedsDisplay: YES ];
    }
    while ( [theEvent type] != NSLeftMouseUp) ;
    // arrivato qui, ho finito il tutto
    return YES;
}

Le modifiche rispetto alla versione precedente si trovano ovviamente nel fatto che è cambiato il metodo di costruzione, che ha adesso bisogno di una grandezza NSSize. C'è una particolarità da notare, ovvero l'invocazione del metodo buildMeFromPt:withSize:. A rigor di programmazione, questo metodo richiede come primo argomento le coordinate di un punto nel SdR della finestra e, come secondo argomento, le dimensioni (larghezza ed altezza) dell'elemento, nel SdR proprio dell'oggetto. Questa istruzione invece fornisce la variabile tmpSize, che esprime dimensioni nel SdR della view. Tuttavia, trattandosi di prima creazione dell'oggetto, l'angolo di rotazione è zero; ciò fa si che le dimensioni siano uguali nei due sistemi di riferimento.

La versione per CCE_Line è del tutto uguale, e non avrebbe bisogno di sovrascrittura se non fosse per poter tracciare correttamente linee orizzontali e verticali quando è premuto il tasto Shift:

...
    if ( shiftKeyPress )
    {        
        float delta1 = tmpSize.width ;
        float delta2 = tmpSize.height ;
        float    dirx = 1, diry = 1 ;    
        if ( delta1 < 0 )
        {
            delta1 = - delta1 ; dirx = -1 ;
        }
        if ( delta2 < 0 )
        {
            delta2 = - delta2 ; diry = -1 ;
        }
        // piglio la dimensione piu' corta
        if ( delta1 < delta2*0.2 )
            tmpSize.width = 0 ;
        else if ( delta2 < delta1*0.2 )
            tmpSize.height = 0 ;
        else if ( delta1 < delta2 )
            tmpSize.height = diry * delta1 ;
        else
            tmpSize.width = dirx * delta2 ;
    }
...

Nel caso della linea, tuttavia, il metodo buildMeFromPt:withSize: richiede come dimensioni grandezze espresse nel SdR della view. A maggior ragione, non devono essere effettuate trasformazioni di sorta.

Per il ri-dimensionamento degli elementi, è stato revisionato il metodo seguente per CCE_BasicForm:

- (BOOL)
handleResize: (NSEvent *)theEvent
    movingHandle: (short) hdlIdx
{
    NSPoint point ;
    NSSize newSize ;
    CoverView * view = [self ownerView] ;
    // indice dello handle che tengo fisso
    static short fixedHandle[8] = { 4, 4, 6, 0, 0, 0, 2, 4 };
    // indice dello handle che devo muovere (opposto)
    static short movingHandle[8] = { 0, 0, 2, 4, 4, 4, 6, 0 };
    // identificatore delle dimensioni che posso modificare:
    // con 2, solo width, con 1 solo height, con 3, w e h
    static short movingPoints[8] = { 3, 1, 3, 2, 3, 1, 3, 2 };
    short movingCoord = movingPoints[ hdlIdx ];
    NSPoint fixedPoint = hdlList[ fixedHandle[ hdlIdx ] ];
    NSPoint movingPoint = hdlList[ movingHandle[hdlIdx] ];
    // se e' locked, non se ne fa nulla
    if ( cceIsLocked ) return YES ;
    // trasformo i punti fissi e mobili nelle coordinate della finestra
    fixedPoint = [ localTF transformPoint: fixedPoint ];
    movingPoint = [ localTF transformPoint: movingPoint ];
    // ridisegno l'oggetto con un nuovo sistema di riferimento
    newSize = [ reverTF transformSize:
        NSMakeSize(movingPoint.x-fixedPoint.x, movingPoint.y-fixedPoint.y) ] ;
    [ self buildMeFromPt: fixedPoint withSize: newSize ] ;
    // ho ricostruito l'oggetto uguale a se stesso; tuttavia, potrebbe essere
    // cambiato il punto base e la rotazione
    // mi preparo lo undo
    [[self undoManager] setActionName: @"BasicForm -> handleResize"];
    [[[self undoManager] prepareWithInvocationTarget: [ownerView theElements]]
        resizeElem: self withSize: localSize ];
    // continuo a ciclare in attesa di un mouseup
    do {
        // recupero il prossimo evento, che sia un movimento del mouse
        // oppure un mouseup (che finisce il tutto)
        theEvent = [[view window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
        // recupero la coordinata della finestra
        point = [view convertPoint:[theEvent locationInWindow] fromView:nil];
        point = getNearestSnapPoint( point, view);
        // ho una nuova dimensione dell'oggetto
        newSize = [ reverTF transformSize:
            NSMakeSize(point.x-fixedPoint.x, point.y-fixedPoint.y) ] ;
        // se il caso, lascio invariata una delle due dimensioni
        if ( movingCoord == 1)
            newSize.width = localSize.width ;
        else if ( movingCoord == 2)
            newSize.height = localSize.height ;
        // ricostruisco l'oggetto con una nuova dimensione
        [ self buildMeFromPt: fixedPoint withSize: newSize] ;
    }
    while ( [theEvent type] != NSLeftMouseUp) ;
    // arrivato qui, ho finito il tutto
    return YES;
}

I vettori fixedHandle e movingHandle sono cambiati, perché è cambiato l'ordine con cui sono calcolati gli handle, ma il concetto di handle fisso e handle mobile è rimasto intatto. È invece cambiato il significato dei codici raccolti in movingPoints; prima permettevano modifiche nelle ascisse e/o nelle ordinate, adesso significato possibilità di modifica nella larghezza e/o altezza dell'elemento. Attenzione adesso che i punti fixedPoint e movingPoint sono espressi nel SdR dell'elemento, e vanno quindi convertiti nel SdR della view usando la trasformazione localTF. C'è poi una istruzione buildMeFromPt:withSize: di costruzione dell'elemento che può apparire superflua (in effetti, si deve ancora far nulla, per cui non è ancora cambiato nulla). In realtà, l'elemento è costruito nuovamente per cambiare (se il caso, ma lo è sette volte su otto) il SdR, incentrandolo sul punto che rimane fisso; il ridisegno serve anche per fissar le idee preparando l'istruzione di Undo; questa avrà come unico parametro la dimensione dell'elemento, contando sul fatto che le operazioni successive di dimensionamento non lo cambiano più.

Finalmente si può partire col ciclo di gestione dei movimenti del mouse. Quando si ottiene il nuovo punto point, si calcolano le nuove dimensioni dell'oggetto; la grandezza newSize va calcolata tenendo conto della trasformazione da SdR della view (in cui sono espresse le coordinate di point e fixedPoint) e quelle dell'elemento (necessarie per il successivo metodo buildMeFromPt:withSize:). La limitazione del dimensionamento dell'oggetto su larghezza e/o altezza è svolta in maniera molto facile, riportando se necessario la dimensione precedente al posto di quella appena calcolata.

Il metodo per CCE_Line è molto simile, e non lo riporto qui, mantenendo esattamente gli stessi concetti, con le opportune modifiche.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).