戻る

直線エンティティを自作してみよう (2)

〜 OpenGLプラットフォーム「ヒスイ」 チュートリアル 〜

2007年9月

このチュートリアルは "直線エンティティを自作してみよう (1)" の続きです。下記の項目について学びます。

ハイライト表示のカスタマイズ

デフォルトのハイライト表示

自作した直線エンティティ MyLine をクリックすると、そのエンティティは選択状態になり、ハイライトされます。ヒスイにおけるデフォルトのハイライト表示は、市松模様のテクスチャ画像を正面からマッピングしたもの、という仕様となっています。しかし、この仕様は適切でない場合も多いと思います。少なくとも直線エンティティの場合にはあまりパッとしたハイライト表示とは言えないでしょう。

ハイライト表示を自分で定義しよう

そこで、ここでは直線エンティティ MyLine のハイライト表示を自分で定義する方法について説明します。下記の仕様で MyLine がハイライトされるように定義してみましょう。

シーンクラスに IHighlight インターフェイスを実装

ハイライト表示をカスタマイズするには、シーンクラスに Hisui.Graphics.IHighlight インターフェイスを実装します。MyLine エンティティのシーンクラスとして MyLineScene クラスが定義してあるはずです。その MyLineScene クラスに IHighlight を実装します。

[Hisui.Graphics.Scene( typeof( MyLine ) )]
class MyLineScene : Hisui.Graphics.IScene, Hisui.Graphics.IHighlight
{
  ...
  
  public void PreHighlight(
    Hisui.Core.IEntry entry, Hisui.Graphics.ISceneContext sc )
  {
  }

  public void PostHighlight(
    Hisui.Core.IEntry entry, Hisui.Graphics.ISceneContext sc )
  {
  }
}

MyLine が選択されると、Draw() メソッドの直前に PreHighlight() メソッドが呼ばれ、Draw() メソッドの直後に PostHighlight() メソッドが呼ばれます。つまり、次の順でメソッドがコールされます。

  1. PreHighlight()
  2. Draw()
  3. PostHighlight()

選択時は直線を太く描画する

下記のように、PreHighlight で LineWidth を太くし、PostHighlight で元に戻します。

  public void PreHighlight(
    Hisui.Core.IEntry entry, Hisui.Graphics.ISceneContext sc )
  {
    sc.LineWidth *= 4;
  }

  public void PostHighlight(
    Hisui.Core.IEntry entry, Hisui.Graphics.ISceneContext sc )
  {
    sc.LineWidth /= 4;
  }

選択時は直線の端点に赤い点を描画する

下記のように、PostHighlight で両端点に赤い点を描画します。

  public void PreHighlight(
    Hisui.Core.IEntry entry, Hisui.Graphics.ISceneContext sc )
  {
    sc.LineWidth *= 4;
  }

  public void PostHighlight(
    Hisui.Core.IEntry entry, Hisui.Graphics.ISceneContext sc )
  {
    sc.LineWidth /= 4;
    
    float size = sc.PointSize;  // 現在の PointSize をバックアップ
    Color color = sc.Color;     // 現在の Color をバックアップ
    sc.PointSize = 8;     // 大きめの点
    sc.Color = Color.Red; // 赤い点
    GL.glBegin( GL.GL_POINTS );
    GL.glVertex3d( _line.P1.x, _line.P1.y, _line.P1.z );
    GL.glVertex3d( _line.P2.x, _line.P2.y, _line.P2.z );
    GL.glEnd();
    sc.PointSize = size;  // PointSize を元に戻す
    sc.Color = color;     // Color を元に戻す
  }

確認してみよう

ヒスイを起動して MyLine エンティティを作成し、マウスで選択してみましょう。期待通りのハイライト表示が得られたでしょうか?

直線の端点移動

ゴール

MyLine エンティティを選択すると、両端点が赤くハイライトされるようになりました。そのハイライトされた端点を、マウスドラッグで移動できるようにしたい、というのが本節のゴールです。ポイントは、特に明示的にコマンドを起動するわけではない、ということです。MyLine エンティティを選択しただけで自動的に端点移動モードがONになるようにしたいのです。

イベントハンドラの設定

ヒスイでは、ver1.4 からエンティティクラス毎にイベントハンドラが登録できるようになりました。この機能を使うと、MyLine エンティティが選択された状態で発生したビューのマウスイベントをハンドリングできるようになります。

まず、次のように Handlers クラスを新規作成します。Handler 属性が付いているところがポイントです。

[Hisui.Ctrl.Handler]
static class Handlers
{
}

ここに MouseDown イベントのハンドラを追加してみましょう。

[Hisui.Ctrl.Handler]
static class Handlers
{
  [Hisui.Ctrl.Handler( Hisui.Ctrl.EventType.MouseDown )]
  static bool OnMouseDown( MyLine self, Hisui.Graphics.IView view, MouseEventArgs e )
  {
    return true;
  }
}

幾つかポイントを箇条書きします。

次のように書いて、MouseDown でメッセージボックスが表示されるか確認してみましょう。

[Hisui.Ctrl.Handler]
static class Handlers
{
  [Hisui.Ctrl.Handler( Hisui.Ctrl.EventType.MouseDown )]
  static bool OnMouseDown( MyLine self, Hisui.Graphics.IView view, MouseEventArgs e )
  {
    MessageBox.Show( "MouseDown" );
    return true;
  }
}

イベントハンドラで端点移動

ではいよいよ、端点を移動するイベントハンドラを作成していきます。

イベントハンドラも、コマンドと同様に IEnumerator<Hisui.Ctrl.IOperation> を返すことが出来ます。つまり、イベントハンドラ内でマウスオペレーションを扱うことが出来るのです。

  [Hisui.Ctrl.Handler( Hisui.Ctrl.EventType.MouseDown )]
  static IEnumerator<Hisui.Ctrl.IOperation>
    OnMouseDown( MyLine self, Hisui.Graphics.IView view, MouseEventArgs e )
  {
  }

端点移動は、マウス左ボタンのドラッグで行います。左ボタン以外の MouseDown イベントは無視するようにします。

  [Hisui.Ctrl.Handler( Hisui.Ctrl.EventType.MouseDown )]
  static IEnumerator<Hisui.Ctrl.IOperation>
    OnMouseDown( MyLine self, Hisui.Graphics.IView view, MouseEventArgs e )
  {
    if ( e.Button != MouseButtons.Left ) yield break;
  }

次に、MouseDown の位置に端点があるかどうかを判定します。

  [Hisui.Ctrl.Handler( Hisui.Ctrl.EventType.MouseDown )]
  static IEnumerator<Hisui.Ctrl.IOperation>
    OnMouseDown( MyLine self, Hisui.Graphics.IView view, MouseEventArgs e )
  {
    if ( e.Button != MouseButtons.Left ) yield break;
    
    var eyeshot = view.Camera.GetEyeshotLine( e.Location );
    if ( eyeshot.Distance( self.P1 ) < 8 * view.Camera.LengthPerPixel ) {
      // ここに P1 をドラッグする処理を記述
    }
    else if ( eyeshot.Distance( self.P2 ) < 8 * view.Camera.LengthPerPixel ) {
      // ここに P2 をドラッグする処理を記述
    }
  }

P1をドラッグする処理は次のように記述できます。

    if ( eyeshot.Distance( self.P1 ) < 8 * view.Camera.LengthPerPixel ) {
      var p1 = self.P1;
      var up = new Hisui.Ctrl.LButtonUp( view.Events );
      up.MouseMove += ( sender, ee ) =>
        {
          var pt1 = new Hisui.Geom.Point2i( e.Location );
          var pt2 = new Hisui.Geom.Point2i( ee.Location );
          self.P1 = p1 + view.Camera.ScreenToWorld( pt2 - pt1 );
          view.Refresh();
        };
      yield return up;
    }

P2 をドラッグする処理も同様に記述したら、動作を確認してみましょう。選択するだけで端点がドラッグできましたか?

なお、端点移動の詳細は「Hisuiチュートリアル / 直線作図機能を作ってみよう (2)」で作成した端点移動コマンドを参考にしてください。

Undo/Redo への対応

Undo, Redo してみよう

Undo、Redo は次のボタンで操作できます。

では直線を作図して、Undo や Redo を試してみましょう。Undo すると作図した直線が削除され、Redo で復活することが分かると思います。

「端点の移動」の Undo/Redo

では「端点の移動」は Undo/Redo できるでしょうか。残念ながらこのままでは Undo/Redo できません。MyLine クラスに Undo/Redo 用の細工が必要です。

IMemorable インターフェイス

MyLine エンティティを Undo/Redo 対応するには、MyLine クラスに Hisui.Core.IMemorable インターフェイスを実装することになります。

ではここで、IMemorable インターフェイスの定義を見ておきましょう。

namespace Hisui.Core
{
  public interface IMemorable
  {
    void AddRef();
    void Release();
  }
}

え?このインターフェイスでどうしてUndo/Redoが実現できるの?と不思議に思われるかと思います。実際のところ、このインターフェイス自体はあまりUndo/Redo機能の実装に寄与していません。このインターフェイスが提供するのは、定義から明らかなように、内部に参照カウンタ機能を保持することのみです。

なんのための参照カウンタかを簡単に説明します。まず、どんなオブジェクトがUndo/Redo動作を必要とするのか考えてみましょう。すると次のことが分かります。

つまり、Documentから(直接/間接を問わず)参照されているオブジェクトが Undo/Redo の対象となることが分かります。この Document からの参照の有無をカウントするのが IMemorable インターフェイスの役割なのです。そして参照カウンタが 1 以上の場合に Undo/Redo に必要な状態の記憶や再生などの処理が走ります。

最も簡単な IMemorable 実装

もっともシンプルな実装方法を下記コードに示します。

class MyLine : Hisui.Geom.IBoundary3d, Hisui.Core.IMemorable
{
  readonly Hisui.Core.IMemorable<Hisui.Geom.Point3d> _p1;
  readonly Hisui.Core.IMemorable<Hisui.Geom.Point3d> _p2;

  public MyLine( Hisui.Geom.Point3d p1, Hisui.Geom.Point3d p2 )
  {
    _p1 = p1
    _p2 = p2
    _p1 = Hisui.CoreUT.Memorable( p1 );
    _p2 = Hisui.CoreUT.Memorable( p2 );
  }

  public Hisui.Geom.Point3d P1
  {
    get { return _p1; }
    get { return _p1.Value; }
  }
  public Hisui.Geom.Point3d P2
  {
    get { return _p2; }
    get { return _p2.Value; }
  }

  public void AddRef()
  {
    _p1.AddRef();
    _p2.AddRef();
  }

  public void Release()
  {
    _p1.Release();
    _p2.Release();
  }
}

この実装では、メンバ変数を Hisui.Core.IMemorable<T> 型に置き換えてしまい、肝心の Undo/Redo 機能はメンバ変数に委譲する構造となっています。大抵の場合はこのやり方で Undo/Redo 対応が済んでしまうと思います。

試してみよう

以上で Undo/Redo の実装は終了です。早速起動して、端点移動が Undo/Redo できるか試してみましょう。


戻る

Copyright © 2010, 株式会社カタッチ
http://www.quatouch.com