- 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 がこの機能を提供してるんだ。flipHorizontally と flipVertically ね。コメントを見てみると、
|
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: を呼び出しているところだね。それを使って得られたベジエ曲線を使って、fill と stroke を呼んで、画面に描いてやるんだ。
◆ 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 は、_bounds の origin。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 で割れば、反転するようになってるんだ。
この性質を利用して、flipHorizontally と flipVertically を実装する。
|
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;
}
|
これで、反転もオッケーだ!
|