home link download back number special issue

HMDT - Special Issue / Sketch BP / 円弧ツールのドローオブジェクトの作成の実装


- Modification -

円弧ツールのドローオブジェクト作成の実装

円弧ツールのドローオブジェクトの作成に必要なもの

ここでは、円弧ツールの、ドローオブジェクト作成のコードの実装に必要なものを、考えてみるよ。

まず、基本的に、SKTGraphic のサブクラスでドローオブジェクトを作るには、bezierPath を実装してやればいいんだ。実際に、SKTGraphic の bezierPath メソッドには、こう書いてある。

Sketch/DocumentModel.subproj/SKTGraphic.m
- (NSBezierPath *)bezierPath {
    // Subclasses that just have a simple path override this to return it.  
    // The basic drawInView:isSelected: implementation below will stroke 
    // and fill this path.  Subclasses that need more complex drawing will 
    // just override drawInView:isSelected:.
    return nil;
}

つまり、「ドローオブジェクトが、単に 1 つのベジエ曲線で表現できるなら、この bezierPath をオーバーライドして、そのベジエ曲線を返せばいい。もっと複雑な描画が必要なら、drawInView:isSelected: もオーバーライドしてね」っていうことだ。

円弧ツールの場合は、仕様の節で考えた通り、2 つのベジエ曲線が必要なんだ。だから、この bezierPath を単にオーバーライドするだけでは不十分で、drawInView:isSelected: をオーバーライドしてやんないといけない。このことから、作るべきメソッドが分かるね。まず drwaInView:isSelected:。それと、bezierPath の代わりになる、2 種類のベジエ曲線を作るためのメソッド。これを bezierPathForStroke:andPathForFill: って名前にしよう。

  • drwaInView:isSelected:
  • bezierPathForStroke:andPathForFill:

あと、考えなきゃいけないのが、弧の象元のサポート。象元を決定しなきゃいけないのは、まずドローオブジェクトを作るとき。これは、SKTGraphic に追加した directionOfCreating を呼び出すことで知ることができる。あとは、作られたドローオブジェクトのサイズを変更するとき。普通にサイズを変更しているときは問題ないんだけど、反転するときがやばい。水平か垂直に反転すると、象元が変わるんだ。

この反転をつかまえてやる必要がある。都合のいいことに、SKTGraphic がこの機能を提供してるんだ。flipHorizontallyflipVertically ね。コメントを見てみると、

Sketch/DocumentModel.subproj/SKTGraphic.m
- (void)flipHorizontally {
    // Some subclasses need to know.
    return;
}

- (void)flipVertically {
    // Some subclasses need to know.
    return;
}

「なんかのサブクラスはこれを必要とするんじゃない?」とのこと。ありがたく使わせてもらおう。というわけで、この 2 つをオーバーライドする。

  • flipHorizontally
  • flipVertically

これらのメソッドを実装してやるんだ。


円弧ツールのドローオブジェクト作成の実装

じゃ、ごりごりと実装していくぜ!


drawInView:isSelected:

まずは drawInView:isSelected:。このメソッドは、ドローオブジェクトを画面に描くときに呼び出されるんだ。

Sketch/DocumentModel.subproj/SKTArc.m (created by mkino)
- (void)drawInView:(SKTGraphicView *)view isSelected:(BOOL)flag {
    NSBezierPath*	pathForStroke;
    NSBezierPath*	pathForFill;
    [self bezierPathForStroke:&pathForStroke 
               andPathForFill:&pathForFill];
    
    if (pathForStroke && pathForFill) {
        if ([self drawsFill]) {
            [[self fillColor] set];
            [pathForFill fill];
        }
        if ([self drawsStroke]) {
            [[self strokeColor] set];
            [pathForStroke stroke];
        }
    }
    if (flag) {
        [self drawHandlesInView:view];
    }
}

親クラスの実装と似ている。違うところは、親クラスでは bezierPath を使って、描くべきベジエ曲線を取り出しているんだけど、SKTArc では、新たに実装される bezierPathForStroke:andPathForFill: を呼び出しているところだね。それを使って得られたベジエ曲線を使って、fillstroke を呼んで、画面に描いてやるんだ。


bezierPathForStroke:andPathForFill:

続いて bezierPathForStroke:andPathForFill:。このメソッドが、円弧ツールの実質的な中核だ。

Sketch/DocumentModel.subproj/SKTArc.m (created by mkino)
- (void)bezierPathForStroke:(NSBezierPath**)pathForStroke 
             andPathForFill:(NSBezierPath**)pathForFill
{
    NSRect	bounds = [self bounds];
    NSPoint	fromPt, toPt, ctrl1Pt, ctrl2Pt, centerPt;
    
    fromPt = toPt = ctrl1Pt = ctrl2Pt = centerPt = NSZeroPoint;
    *pathForStroke = nil;
    *pathForFill = nil;
    
    // In case of creation
    if ([self isCreating]) {
        _direction = [self directionOfCreating];
    }
    
    // Create points for bezier paths
    switch (_direction) {
    case UpperRightDirection: {
        // Upper right
        fromPt.x = bounds.origin.x;
        fromPt.y = bounds.origin.y;
        toPt.x = bounds.origin.x + bounds.size.width;
        toPt.y = bounds.origin.y + bounds.size.height;
        ctrl1Pt.x = fromPt.x + bounds.size.width / 2.0;
        ctrl1Pt.y = fromPt.y;
        ctrl2Pt.x = toPt.x;
        ctrl2Pt.y = toPt.y - bounds.size.height / 2.0;
        
        break;
    }
    case LowerRightDirection: {
        // Lower right
        fromPt.x = bounds.origin.x;
        fromPt.y = bounds.origin.y + bounds.size.height;
        toPt.x = bounds.origin.x + bounds.size.width;
        toPt.y = bounds.origin.y;
        ctrl1Pt.x = fromPt.x + bounds.size.width / 2.0;
        ctrl1Pt.y = fromPt.y;
        ctrl2Pt.x = toPt.x;
        ctrl2Pt.y = toPt.y + bounds.size.height / 2.0;
        
        break;
    }
    case UpperLeftDirection: {
        fromPt.x = bounds.origin.x + bounds.size.width;
        fromPt.y = bounds.origin.y;
        toPt.x = bounds.origin.x;
        toPt.y = bounds.origin.y + bounds.size.height;
        ctrl1Pt.x = fromPt.x - bounds.size.width / 2.0;
        ctrl1Pt.y = fromPt.y;
        ctrl2Pt.x = toPt.x;
        ctrl2Pt.y = toPt.y - bounds.size.height / 2.0;
        
        break;
    }
    case LowerLeftDirection: {
        fromPt.x = bounds.origin.x + bounds.size.width;
        fromPt.y = bounds.origin.y + bounds.size.height;
        toPt.x = bounds.origin.x;
        toPt.y = bounds.origin.y;
        ctrl1Pt.x = fromPt.x - bounds.size.width / 2.0;
        ctrl1Pt.y = fromPt.y;
        ctrl2Pt.x = toPt.x;
        ctrl2Pt.y = toPt.y + bounds.size.height / 2.0;
        
        break;
    }
    default : {
        // Return nil paths
        return;
    }
    }
    
    centerPt.x = fromPt.x;
    centerPt.y = toPt.y;
    
    // Create the path for stroking
    *pathForStroke = [NSBezierPath bezierPath];
    [*pathForStroke moveToPoint:fromPt];
    [*pathForStroke curveToPoint:toPt 
                   controlPoint1:ctrl1Pt 
                   controlPoint2:ctrl2Pt];
    [*pathForStroke setLineWidth:[self strokeLineWidth]];
    
    // Create the path for filling
    *pathForFill = [NSBezierPath bezierPath];
    [*pathForFill moveToPoint:centerPt];
    [*pathForFill lineToPoint:fromPt];
    [*pathForFill curveToPoint:toPt 
                 controlPoint1:ctrl1Pt 
                 controlPoint2:ctrl2Pt];
    [*pathForFill closePath];
}

処理の流れは、

  • ローカル変数の初期化
  • 作成中だったら、作成方向のチェック
  • 円弧の方向に応じて、ベジエ曲線のアンカーポイントとコントロールポイントを決定
  • 2 種類のベジエ曲線の作成

という感じ。この中でも肝になるのが、アンカーポイントとコントロールポイントの決定だ。円弧のベジエ曲線を作るには、4 つのポイントが必要になるんだ。始点と終点になるアンカーポイントが 2 つ。そいつら用のコントロールポイントが 2 つ。

例を見てみよう。たとえば、右上方向(UpperRightDirection)に弧を作るときは、必要なポイントは下の図のような感じだ。

この 4 点を決定すればオッケー。そしてこいつらは、オブジェクトの領域(_bounds)から求めることができる。Anchor point 1 は、_boundsorigin。Anchor point 2 は、_bounds の右下の点。Control point 1 は、Anchor point 1 から _bounds の横幅を半分いったところ。同じように、Control point 2 は、Anchor point 2 から、縦幅を半分登ったところだ。

この要領で、4 つの方向に応じたベジエ曲線を作ってやるんだ。


flipHorizontally, flipVertically

あとは、反転時の _direction の取り扱い。direction の値は、垂直反転時には -1 をかければ、水平反転時には、2 をかけるか 2 で割れば、反転するようになってるんだ。

この性質を利用して、flipHorizontallyflipVertically を実装する。

Sketch/DocumentModel.subproj/SKTArc.m (created by mkino)
- (void)flipHorizontally {
    _direction *= -1;
    
    return;
}

- (void)flipVertically {
    if (abs(_direction) == 1) {
        _direction *= 2;
    }
    else {
        _direction /= 2;
    }
    
    return;
}

これで、反転もオッケーだ!


- ソースコードのダウンロード -

HMDT - Download / Sketch BP


Home | Link | Download | Back Number | Speciall Issue

Sketch BP

mailto: mkino@xd5.so-net.ne.jp

HMDT