home link download back number special issue

HMDT - Special Issue / Sketch BP / ベジエ曲線ツールのドローオブジェクトの作成の実装


- Modification -

ベジエ曲線ツールのドローオブジェクトの作成の実装

ベジエ曲線ツールのドローオブジェクトの作成に必要なもの

じゃ、ベジエ曲線ツールの実装の話だ。まず、必要なインスタンス変数を考えてみよう。

必要なものは、ポリゴンツールのときと変わらない。各点の配列と、パスが閉じているかどうかを示すフラグだ。

Sketch/DocumentModel.subproj/SKTBezierPath.h (created by mkino)
@interface SKTBezierPath : SKTGraphic {
    @private
    NSMutableArray* _points;
    BOOL            _isPathClosed;
    ...

}

ただし、点の内容はポリゴンツールのときとは違うんだ。ベジエ曲線は、線が通るアンカーポイントと、その線の曲がり具合を制御するコントロールポイントでできているんだ。アンカーポイント 1 つにつき、コントロールポイント 2 つが定義される。

SKTBezierPath クラスでは、この 2 種類の点を 1 つの配列に突っ込むことにした。突っ込む順番は、最初のアンカーポイント、最初のアンカーポイントの後ろ側のコントロールポイント、2 つめのアンカーポイントの前側のコントロールポイント、2 つめのコントロールポイント、2 つめのアンカーポイントの後ろ側のコントロールポイント、、、っていう順序だ。

index kind of point
0 Anchor point 0
1 Control point 0, 1
2 Control point 1, 0
3 Anchor point 1
4 Control point 1, 1
5 Control point 2, 0
6 Anchor point 2

点は、アンカーポイントで始まって、必ずアンカーポイントで終わるものとする。だから、最初の 1 つめの点以降は、追加するときは 3 つの点をセットで追加するようにする。パスが閉じているときは、最初のアンカーポイントと最後のアンカーポイントの座標を同じにするんだ。

で、実際に実装するには creatWithEvent:inView: をオーバーライドするんだ。


ポリゴンツールのドローオブジェクトの作成の実装

で、実際の実装の話なんだけど、いつも通り createWithEvent:inView: をオーバーライドする。ポリゴンのときと結構似ているので、そちらと見比べてみてくれ。

Sketch/DocumentModel.subproj/SKTBezierPath.m (created by mkino)
- (BOOL)createWithEvent:(NSEvent *)event inView:(SKTGraphicView *)view {
    NSPoint	point = [view convertPoint:[event locationInWindow] fromView:nil];
    NSPoint	lastAnchorPt;
    NSRect	drawingBounds, lastDrawingBounds;
    BOOL	snapsToGrid = [view snapsToGrid];
    float	spacing = [view gridSpacing];
    BOOL	echoToRulers = [[view enclosingScrollView] rulersVisible];
    NSEventType	type;
    
    [self startBoundsManipulation];
    if (snapsToGrid) {
        point.x = floor((point.x / spacing) + 0.5) * spacing;
        point.y = floor((point.y / spacing) + 0.5) * spacing;
    }
    [self setBounds:NSMakeRect(point.x, point.y, 0.0, 0.0)];
    if (echoToRulers) {
        [view beginEchoingMoveToRulers:[self bounds]];
    }
    
    // Append first anchor point
    [self appendFirstAnchorPoint:point];
    lastAnchorPt = point;
    
    lastDrawingBounds.origin = point;
    lastDrawingBounds.size.width = 1;
    lastDrawingBounds.size.height = 1;
    
    // Tracking loop
    while (1) {
        event = [[view window] nextEventMatchingMask:
            (NSLeftMouseDownMask | 
             NSLeftMouseDraggedMask | 
             NSLeftMouseUpMask)];
        point = [view convertPoint:[event locationInWindow] fromView:nil];
        if (snapsToGrid) {
            point.x = floor((point.x / spacing) + 0.5) * spacing;
            point.y = floor((point.y / spacing) + 0.5) * spacing;
        }
        
        type = [event type];
        if (type == NSLeftMouseDown && [event clickCount] == 1) {
            // Append points for a bezier path
            [self appendAnchorPoint:point 
                      controlPoint1:_lastControlPoint 
                      controlPoint2:point];
            
            lastAnchorPt = point;
            _lastControlPoint = point;
        }
        else if (type == NSLeftMouseDragged) {
            int		index = [self indexOfLastAnchorPoint];
            NSPoint	flippedCtrlPt;
            flippedCtrlPt.x = 2 * lastAnchorPt.x - point.x;
            flippedCtrlPt.y = 2 * lastAnchorPt.y - point.y;
            
            // Move the last control point
            if (index != 0) {
                [self movePointAtIndex:index - 1 toPoint:flippedCtrlPt];
            }
            _lastControlPoint = point;
        }
        else if (type == NSLeftMouseUp) {
            if ([_points count] > 1) {
                int	index = [self pointUnderPoint:lastAnchorPt];
                if (index == 0) {
                    // Fit to first point
                    [self movePointAtIndex:[self indexOfLastAnchorPoint] 
                                   toPoint:[self pointAtIndex:0]];
                    
                    // Close path and go out from loop
                    _isPathClosed = YES;
                    break;
                }
            }
        }
        
        // Set bounds and calculate drawing bounds
        [self setBounds:[[self bezierPath] bounds]];
        drawingBounds = [self drawingBounds];
        [view setNeedsDisplayInRect:
        NSUnionRect(drawingBounds, lastDrawingBounds)];
        lastDrawingBounds = drawingBounds;
        
        if (echoToRulers) {
            [view continueEchoingMoveToRulers:[self bounds]];
        }
        
        // In case of double click
        if ([event clickCount] >= 2) {
            break;
        }
    }
    if (echoToRulers) {
        [view stopEchoingMoveToRulers];
    }
    
    [self stopBoundsManipulation];
    
    return !NSEqualRects([self bounds], NSZeroRect);
}

処理の流れも、ポリゴンのときと同じく、

  • イベントの待ち
  • アンカーポイントとコントロールポイントのハンドル
  • 再描画領域の確定

っていう流れになっている。これらのうち、イベントの待ち、と、再描画領域の確定、の部分は、ポリゴンのときとほとんど同じなので、アンカーポイントとコントロールポイントのハンドルにしぼって見てみよう。


アンカーポイントとコントロールポイントのハンドル

このメソッドでは、トラッキングループを作って、その中でアンカーポイントと、コントロールポイントをセットしているんだ。トラッキングループでは、NSLeftMouseDown、NSLeftMouseDragged、NSLeftMouseUp のイベントを待っている。それぞれを見てみよう。


- NSLeftMouseDown、アンカーポイントとコントロールポイントの追加

まず、マウスを押したとき。このときは、そのポイントを新しいアンカーポイントとして追加する。同時に、そいつの前 2 つ分のコントロールポイントも追加する。下の図でいうと、Anchor point 1 を追加するときは、Control point 0, 1 と Control point 1, 0 を追加するんだ。

Control point 1, 1 は、この時点では追加されない。次のアンカーポイントを追加するときに、されるんだ。

Sketch/DocumentModel.subproj/SKTBezierPath.m (created by mkino)
- (BOOL)createWithEvent:(NSEvent *)event inView:(SKTGraphicView *)view {
    ...

    // Tracking loop
    while (1) {
        ...

        if (type == NSLeftMouseDown && [event clickCount] == 1) {
            // Append points for a bezier path
            [self appendAnchorPoint:point 
                      controlPoint1:_lastControlPoint 
                      controlPoint2:point];
            
            lastAnchorPt = point;
            _lastControlPoint = point;
        }
       
        ...
}

Control Point 1, 0 は、この時点ではアンカーポイントといっしょ。ドラッグしたときに、動かされる。


- NSLeftMouseDragged、コントロールポイントの移動

続いてマウスのドラッグのとき。ここではコントロールポイントを移動する。

Sketch/DocumentModel.subproj/SKTBezierPath.m (created by mkino)
- (BOOL)createWithEvent:(NSEvent *)event inView:(SKTGraphicView *)view {
    ...

    // Tracking loop
    while (1) {
        ...

        else if (type == NSLeftMouseDragged) {
            int		index = [self indexOfLastAnchorPoint];
            NSPoint	flippedCtrlPt;
            flippedCtrlPt.x = 2 * lastAnchorPt.x - point.x;
            flippedCtrlPt.y = 2 * lastAnchorPt.y - point.y;
            
            // Move the last control point
            if (index != 0) {
                [self movePointAtIndex:index - 1 toPoint:flippedCtrlPt];
            }
            _lastControlPoint = point;
        }
        
        ...
}

現在のポイントがコントールポイントの 1 つになる。もう 1 つのコントロールポイントは、アンカーポイントを挟んで、反対側になる。それを求めたものが、flippedCtrlPt だ。そして、その値で、配列の中にあるコントロールポイントの値を変更してやる。これで、ドラッグに応じてコントロールポイントが動くぜ。


- NSLeftMouseUp、パスを閉じる

で、最後。マウスのボタンを離したとき。ここでは、パスのクローズの処理が必要になる。

最後のアンカーポイントの位置が、最初のアンカーポイントの範囲に入るなら、最後のポイントと最初のポイントを完全に重ねて、パスを閉じるんだ。

Sketch/DocumentModel.subproj/SKTBezierPath.m (created by mkino)
- (BOOL)createWithEvent:(NSEvent *)event inView:(SKTGraphicView *)view {
    ...
    
    // Tracking loop
    while (1) {
        ...

        else if (type == NSLeftMouseUp) {
            if ([_points count] > 1) {
                int	index = [self pointUnderPoint:lastAnchorPt];
                if (index == 0) {
                    // Fit to first point
                    [self movePointAtIndex:[self indexOfLastAnchorPoint] 
                                   toPoint:[self pointAtIndex:0]];
                    
                    // Close path and go out from loop
                    _isPathClosed = YES;
                    break;
                }
            }
        }
        
    ...
}

これで、ベジエ曲線オブジェクトの出来上がり。


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

HMDT - Download / Sketch BP


Home | Link | Download | Back Number | Speciall Issue

Sketch BP

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

HMDT