QuickTemplate で始めよう

MoNo.RAIL には MoNo.RAIL.Samples というサンプルプロジェクト群が含まれています。

ここでは MoNo.RAIL.Samples/MoNoStudioSamples/QuickTemplate/ というフォルダに含まれているサンプルコードについて解説します。

Quick Template とは

  • Visual Studio 2015 向けの F# のプロジェクトテンプレートです。
  • MoNo.Studio 3D のプラグインを開発するためのテンプレートです。
  • MoNo.Studio 3D のメニュー(コマンド)や、エンティティ、シーンなどをプラグイン出来ます。
../../_images/overview.png

Samples メニュー

QuickTemplate プロジェクトをビルドし実行すると、MoNo.Studio 3D のメインメニューに次のようなサンプルメニューが加わっていることが分かります。

../../_images/samplemenu.png

これらのメニューは付属しているサンプルコードによって生成されています。 ソースファイルとサンプルメニューの対応関係は下記の通り。

source file sample menu
MessageMenu.fs MessageBox Samples
OpenGLMenu.fs OpenGL Samples
OperationMenu.fs Operation Samples
EntityMenu.fs Entity Samples
AsyncMenu.fs Async/Progress Samples

サンプルコード解説

Samples メニュー

メインメニューのプラグインは [MEF; Managed Extensibility Framework] の仕組みを利用して行います。 その基本形(ひながた)を下記のコードに示しました。 MoNo.Studio.MenuFactory<’con> を継承したクラスを定義し、CreateMenuItems 関数をオーバーライドしてプラグインするメニューを定義します。

open System.ComponentModel.Composition
open MoNo
open MoNo.Studio

type [<Export (typeof<IMenuFactory>)>] private MyMenuFactory() =
  inherit MenuFactory<QuickAppContext>()
  override __.CreateMenuItems con =
    []

Samples / MessageBox Samples (MessageMenu.fs)

メッセージボックスを表示するだけの最もシンプルなサンプルです。次のような形をしています。

type [<Export (typeof<Studio.IMenuFactory>)>] private MessageMenuFactory() =
  inherit Studio.MenuFactory<QuickAppContext>()

  override __.CreateMenuItems con =
    let items =
      [
        MenuItem.New (...)
        MenuItem.New (...)
        ...
      ]
    [ MenuItem.Folder ("Samples", [ MenuItem.Folder ("MessageBox Samples", items) ]) ]

最も単純なメッセージボックスの表示コマンドは次のように定義されます。

MenuItem.New ("InformationMessage",
  cont {
    do! con.Messenger.Send (Wpf.InformationMessage ("Hello World", "InformationMessage", MessageBoxImage.Information))
  } |> Wpf.Cont.toWpfCommand con None)
  • MenuItem.New() でコマンドメニューを作成します。
  • cont { … } は「継続モナド」の「コンピューテーション式」です。
    • 「継続モナド」「コンピューテーション式」についての知識は必要ありません。
    • とりあえずは cont { … } の中に処理を書けばコマンドとして実行されるという理解で十分です。
    • この MessageBox のサンプルでは「継続モナド」のメリットは特に活かされていません。
  • Wpf.Cont.toWpfCommand 関数によってWPFのコマンド(つまり System.Windows.Input.ICommand)が生成されます。
  • 直接 MessageBox.Show() を呼ばないで、Messenger というオブジェクトに InformationMessage を送信します。
    • このプラグインのコードは MVVM アーキテクチャの ViewModel に位置しています。 ViewModel から View(この場合は MessageBox)に直接アクセスするのは望ましくありません。 従って Messenger を経由して間接的に MessageBox の表示命令を送信する体裁を取ります。
    • この Messenger は汎用の MVVM フレームワークによるものではなく、MoNo.RAIL に独自に定義したものです。

Samples / OpenGL Samples (OpenGLMenu.fs)

ひな形

OpenGL のサンプルコードは概ね次のひな形に従っています。

MenuItem.New ("...",
  cont {
    // MoNo.Graphics.IScene のインスタンスを生成
    let scene =
      { new Graphics.IScene with
          member __.Draw sc =
            // ここに何らかの描画コードを書く
            ... }
    con.Document.Val.Entries.Add (Wpf.Entry scene) // シーンオブジェクトを登録
  } |> Wpf.Cont.toWpfCommand con None)
  • OpenGL 命令によって何らかのオブジェクトの描画を追加するサンプルメニューです。
  • MoNo.Graphics.IScene インターフェスを実装することによって自由に描画することが出来ます。
  • 生成した IScene オブジェクトを con.Document に追加することによってビューにオブジェクトが現れます。
OpenGL Direct Call
member __.Draw sc =
  // MoNo.OpenGL.GL クラスを利用すると P/Invoke により直接 OpenGL の API を呼び出すことができます。
  GL.glDisable( GL.GL_LIGHTING )  // ライティングを無効化
  GL.glColor3f( 1.f, 1.f, 0.f )   // 色を設定
  GL.glBegin( GL.GL_LINES )
  GL.glVertex3f( 0.f, 0.f, 0.f )
  GL.glVertex3f( 1.f, 0.f, 0.f )
  GL.glEnd()
  • MoNo.OpenGL ネームスペースの GL クラスに用意されている OpenGL のAPI(P/Invoke)を利用しています。
  • MoNo.RAIL にはより便利にラップされたAPIが用意されているので、直接 OpenGL を呼び出す方法はあまり推奨しません。
  • (そもそも現在では glBegin / glEnd 自体が推奨されないAPIですが…)
By MoNo.OpenGL.MGL
member __.Draw sc =
  MGL.LightingEnabled <- false
  MGL.Color <- System.Drawing.Color.Pink.ToColor4f()
  use gl = MGL.Begin( GLPrimType.Lines )
  gl.Vertex( Point3d.Zero )
  gl.Vertex( Point3d( 1.0, 1.0, 1.0 ) )
  • MoNo.OpenGL ネームスペースの MGL クラスに用意されているラッパーAPIを利用しています。
  • ごく薄いラッパーなので、OpenGL を直接利用するのとそれほど大差ありません。
By MoNo.Graphics.ISceneContext
member __.Draw sc =
  use scope = sc.Push() // scope を利用すると Lighting 等の状態変更が using スコープ内に限定されます
  scope.Lighting <- false
  scope.Color <- System.Drawing.Color.Plum
  sc.DrawLines (fun gl -> gl.Vertices( Point3d.Zero, Point3d( 1.0, 2.0, 3.0 ) ) )
  • MoNo.Grpahics.ISceneContext に用意されているメソッドを利用しています。
  • scope に設定した色やライティングなどの状態は、スコープを抜けるときに元に戻ります。
VertexArray (MoNo.OpenGL.MGL)
// 頂点配列を作成
let array =
  [|
    Point3d( 0.0, 0.0, 0.0 )
    Point3d( 0.2, 0.0, 0.0 )
    Point3d( 0.2, 0.2, 0.0 )
    Point3d( 0.0, 0.2, 0.0 )
    Point3d( 0.0, 0.4, 0.0 )
    Point3d( 0.2, 0.4, 0.0 )
    Point3d( 0.2, 0.6, 0.0 )
    Point3d( 0.0, 0.6, 0.0 )
  |]
...
member __.Draw sc =
  use scope = sc.Push()
  scope.Lighting <- false
  scope.Color <- System.Drawing.Color.Salmon
  MGL.DrawArrays (GLPrimType.LineStrip, array)
  • MGL に用意されたラッパーAPIを利用して頂点配列を描画しています。
VertexArray (MoNo.GraphicsUT)
// 頂点配列を作成
let array =
  GraphicsUT.CreateScene (GLPrimType.LineStrip,
    [|
      Point3d( 0.0, 0.0, 0.0 )
      Point3d( 0.2, 0.0, 0.0 )
      Point3d( 0.2, 0.2, 0.0 )
      Point3d( 0.0, 0.2, 0.0 )
      Point3d( 0.0, 0.4, 0.0 )
      Point3d( 0.2, 0.4, 0.0 )
      Point3d( 0.2, 0.6, 0.0 )
      Point3d( 0.0, 0.6, 0.0 )
    |])
  • GraphicsUT に用意されたAPIで頂点配列による IScene オブジェクトを生成しています。
  • このAPIでポリゴンを描画すると、MoNo.Studio 3D のツールバーにある “Edge” ボタンによってポリゴンのエッジを表示させることが出来るようになります。
  • ポリゴンのエッジを表示するときは、エッジの描画がちらつかないようにするために glPolygonOffset() の設定が行われたりします。
Texture Mapping
let texture = MGL.CreateTextureObject( bitmap );
...
member __.Draw sc =
  use scope = sc.Push()
  scope.Lighting <- false
  scope.Color <- System.Drawing.Color.White
  use binding = texture.Bind() // テクスチャをバインドします。
  sc.DrawQuads (fun gl ->
    gl.TexCoord( 0.0, 0.0 ); gl.Vertex( 0.0, 0.0, 0.0 )
    gl.TexCoord( 1.0, 0.0 ); gl.Vertex( 1.0, 0.0, 0.0 )
    gl.TexCoord( 1.0, 1.0 ); gl.Vertex( 1.0, 1.0, 0.0 )
    gl.TexCoord( 0.0, 1.0 ); gl.Vertex( 0.0, 1.0, 0.0 ))

Samples / Operation Samples (OperationMenu.fs)

Click (simple)
cont {
   // クリックを1回検出します。
   let! e = con.ViewContext.Events.MouseClick |> Cont.ofObservable
   do! con.Messenger.Send (Wpf.InformationMessage (
      sprintf "Clicked: Location = %A" e.Location, "Click Sample"))
}
  • 継続のコンピュテーション式の利点を活かしてプログラミングできます。
  • MouseClick イベントを Cont.ofObservable で Cont<’a> に変換し、その結果を let! で受け取るとクリック待ち状態に入ります。
  • ユーザーがビューをクリックすると処理が次の行に「継続」します。
  • 上記サンプルではクリックした座標値がメッセージボックスで表示されます。
  • クリック待ち状態の時にツールバーの “Abort” ボタンを押すと、コマンドが中断されます。

この例は Cont(継続モナド)を理解するためには適したシンプルなサンプルコードとなっていますが、実際の MoNo.RAIL アプリケーションの開発においては次の例の GetLButtonClick() 等を推奨します。継続モナドについては Cont(継続)モナド を参照して下さい。

Click
cont {
  // クリックを1回検出します。クリック待ち状態のマウスカーソルを Corsors.Hand に設定しています。
  let! sender, e = con.GetLButtonClick (fun op -> op.Cursor <- Forms.Cursors.Hand)
  do! con.Messenger.Send (Wpf.InformationMessage (sprintf "Clicked: Location = %A" e.Location, "Click Sample"))
}
  • ひとつ前の「Click (simple)」とほぼ同様ですが、クリックの検出に con.GetLButtonClick() を使用している点だけが異なっています。
  • この関数は引数に渡した関数によってオペレーション op の設定を行うことができます。この例ではマウスカーソルを Cursors.Hand に設定しています。
Click Twice
cont {
  // クリックを2回検出します。
  let! _, e1 = con.GetLButtonClick ()
  let! _, e2 = con.GetLButtonClick ()
  do! con.Messenger.Send (Wpf.InformationMessage (sprintf "1st click = %A\n2nd click = %A" e1.Location e2.Location, "Click Twice Sample"))
}
  • 同様のコードでクリックを2回検出することも出来ます。
Draw Line
cont {
  // 一点目のクリックを検出します。
  let! view, e1 = con.GetLButtonClick ()

  // 直線の両端点の座標を作成します。
  let p1 = view.Camera.ScreenToWorld e1.Location
  let p2 = ref p1 // p2 はこの時点では p1 と同じ座標を設定しておきます

  // 二点目のクリックを検出します。
  let! _, e2 = con.GetLButtonClick (fun op ->
    op.MouseMove.Add (fun e -> p2 := view.Camera.ScreenToWorld e.Location; view.Invalidate())
    op.WorldScenes.Add (fun sc ->
      use scope = sc.Push()
      scope.Color <- Drawing.Color.Turquoise
      sc.DrawLines (fun gl -> gl.Vertices (p1, !p2))) |> ignore)

  // 2点 p1, p2 から Polyline3d オブジェクトを生成します。
  let pol = Polyline (false, [| p1; !p2 |])

  // エンティティを登録します。
  con.Document.Val.Entries.Add (Wpf.Entry pol)
}
  • このサンプルではビューのクリックを2回検出して2点間を結ぶ直線を作図するコマンドを定義しています。
  • 2点目のクリック待ち状態の間は、MouseMove のイベント発生時に作図される直線のプレビュー表示が更新されるようになっています。
  • 2点からなる Polyline<Point3d> オブジェクトを生成してドキュメントに登録しています。
Draw Polyline
cont {
   let mutable pol = Polyline.empty
   let mutable scene = GraphicsUT.CreateScene ignore
   let mutable cursorPos = Point3d.Zero

   // Operation オブジェクトを作ります
   let op = MoNo.Ctrl.Operation con.ViewContext

   // 現在作図中の折れ線を描画するシーンを登録します
   op.WorldScenes.Add (fun sc ->
     use scope = sc.Push()
     scope.Color <- Drawing.Color.Yellow
     scene.Draw sc
     if pol.Points.Length > 0 then
       sc.DrawLines (fun gl -> gl.Vertices (pol.Points.[pol.Points.Length - 1], cursorPos))) |> ignore

   // 現在のマウスカーソルの位置をワールド座標系の点として取得します
   op.MouseMove.AddHandler (fun sender e ->
     let view = sender :?> Graphics.IView
     cursorPos <- view.Camera.ScreenToWorld e.Location
     con.ViewContext.Invalidate ())

   // マウスカーソル位置の点を折れ線に追加します
   op.MouseClick.Add (fun _ ->
     pol <-
       Immutarray.append pol.Points (Immutarray [| cursorPos |])
       |> Polyline.ofImmutarray false
     scene <- Graphics.SceneFactory.NewInstance pol
     con.ViewContext.Invalidate ())

   // スペースキーで作図を終了します
   op.KeyDown.Add (fun e -> if e.KeyCode = Forms.Keys.Space then op.Exit ())

   // オペレーションを作動させます
   do! con.ByOperation op

   // 折れ線エンティティを登録します。
   if pol.Points.Length >= 2 then con.Document.Val.Entries.Add (Wpf.Entry pol)
}

複数回のクリックを検出して折れ線を作図するサンプルコードです。スペースキーで折れ線作図を完了します。

実は cont コンピューテーション式には、 while 文などのループ文が使えないという制約があります。従って上のサンプル Draw Line で用いた方法の延長でループ文によって複数回のクリックを検出するという方法は使えません。

そこでここでは次の方法を採っています。

  1. MoNo.Ctrl.Operation オブジェクトを生成し、イベントハンドラ等を設定
    1. WorldScenes に作図途中の折れ線をプレビューする描画処理を追加
    2. MouseMove イベントで現在のカーソル位置をワールド座標系に変換
    3. MouseClick イベントで作図中の折れ線オブジェクトに点を追加
    4. KeyDown イベントでスペースキーを検知したら Exit() メソッドを呼び出して完了
  2. do! con.ByOperation op によってオペレーションを実行
  3. オペレーションが完了したら作成された折れ線を登録

実は今まで使ってきた con.GetLButtonClick() 等の処理は、内部では MoNo.Ctrl.Operation クラスが動いています。これらについては OperationDriver 周辺の仕組みについて を参照して下さい。

Object Picking
cont {
  let! view, e = con.GetLButtonClick (fun op -> op.Cursor <- Forms.Cursors.Hand)
  let pick = view.PickAt<Wpf.Entry> (e.Location.ToPoint2i())
  if pick <> null then
    do! con.Messenger.Send (Wpf.InformationMessage (sprintf "HitObject = %A" pick.HitObject.Object, "Object Picking"))
}
  • ビュー上に表示されているオブジェクトのクリックを検出するサンプルです。
  • STLをインポートするなどして、何らかのオブジェクトをビューに表示した状態でこのコマンドを実行してください。
  • オブジェクトの検出に成功すると MessageBox で情報が表示されます。

Samples / Entity Samples (EntityMenu.fs)

Polyline3d (= Polyline&lt;Point3d&gt;)
cont {
  // MoNo.Geometries.Polyline<Point3d> エンティティを生成
  let polyline =
    [| Point3d( 0.0, 0.0, 0.0 )
       Point3d( 1.0, 0.0, 0.0 )
       Point3d( 1.0, 1.0, 0.0 )
       Point3d( 0.0, 1.0, 1.0 ) |] |> Polyline.ofArray false

  // シーングラフにエントリを登録
  con.Document.Val.Entries.Add (Wpf.Entry polyline)
}
  • 折線オブジェクトの生成サンプルです。
Tris3d (= Soup&lt;Triangle3d&gt;)
cont {
  let tris = Tris3d ([| Triangle3d( Point3d( 0.0, 0.1, 0.3 ), Point3d( 0.2, 0.0, 0.5 ), Point3d( 0.4, 0.3, 0.0 ) )
                        Triangle3d( Point3d( 0.8, 0.5, 0.4 ), Point3d( 0.0, 0.2, 0.4 ), Point3d( 0.2, 0.1, 0.6 ) )
                        Triangle3d( Point3d( 0.8, 0.6, 0.3 ), Point3d( 0.3, 0.5, 0.4 ), Point3d( 0.2, 0.2, 0.0 ) )
                        Triangle3d( Point3d( 0.5, 0.2, 0.5 ), Point3d( 0.3, 0.6, 0.1 ), Point3d( 0.0, 0.3, 0.7 ) ) |])
  con.Document.Val.Entries.Add (Wpf.Entry tris)
}
  • 三角形のポリゴンスープの生成サンプルです。
  • Triangle3d の配列を保持しているだけの単純なエンティティです。
  • STL形式のファイルをインポートするとこのエンティティとして読み込まれます。
Mesh3d (= Mesh&lt;Point3d&gt;)
cont {
  let mesh =
    let vertices = // 頂点配列
      [| Point3d( 0.0, 0.0, 1.0 )
         Point3d( 1.0, 0.0, 1.0 )
         Point3d( 1.0, 1.0, 1.0 )
         Point3d( 0.0, 1.0, 1.0 ) |]
    Mesh3d (vertices, [| Facet (0, 1, 2); Facet (0, 2, 3) |])
  con.Document.Val.Entries.Add (Wpf.Entry mesh)
}
  • 三角形メッシュの生成サンプルです。
  • 四角形や多角形の面は保持できません。
  • 頂点配列と面(ファセット)の配列の2つで構成されます。
  • ファセットは三角形を構成する頂点のインデックスを3つで構成されます。

Samples / Async/Progress Samples (AsyncMenu.fs)

Async
let traceLine s = Diagnostics.Trace.WriteLine s
...
cont {
  let! result = con.ByAsync (async {
      for i = 1 to 10 do
        traceLine "calculating ..."
        Threading.Thread.Sleep 300
      traceLine "done."
      return 1234
    })
  do! con.Messenger.Send (Wpf.InformationMessage (sprintf "result = %d" result, "Async Calculation"))
}
  • ダミーの計算処理を非同期で(バックグラウンドで)実行するサンプルです。
  • ダミー処理の実行中もビュー操作などが可能であることを確認して下さい。
  • 計算処理が終わると次の行に処理が「継続」します。
  • 計算途中でツールバーの “Abort” ボタンを押すと計算が中断されます。
Progress
let calcProgressive1 () =
  Progress.Make (fun token ->
    trace "calcProgressive1: "
    for i = 1 to 10 do
      Threading.Thread.Sleep 200
      trace "."
      token.Notify 0.1
    traceLine "done.")

let calcProgressive2 () =
  Progress.Make (fun token ->
    token.Run (0.6, calcProgressive1 ())
    Threading.Thread.Sleep 500
    traceLine "calcProgressive2 - 1"
    token.Notify 0.2
    Threading.Thread.Sleep 500
    traceLine "calcProgressive2 - 2"
    token.Notify 0.2
    Math.PI )
...
cont {
  let! result = con.ByProgress (calcProgressive2 ())
  do! con.Messenger.Send (Wpf.InformationMessage (sprintf "result = %f" result, "Async Calculation"))
}
  • ダミーの計算処理を非同期で(バックグラウンドで)実行するサンプルです。
  • このサンプルでは計算の進捗を通知し、画面左下のプログレスバーに進捗度が表示されます。
  • 計算途中でツールバーの “Abort” ボタンを押すと計算が中断されます。

コンテキストメニュー (ContextMenu.fs)

type [<Export (typeof<IContextMenuFactory>)>] Tris3dMenu () =
  inherit ContextMenuFactory<Tris3d> ()
  override __.CreateMenuItems (target, entry, con) =
    [
      MenuItem.New ("facet count",
        con.CreateCommand ( cont {
          do! con.Messenger.Send (Wpf.InformationMessage (sprintf "count = %d" target.Items.Length, "facet count"))
        }))
    ]
  • MEF アーキテクチャを利用してコンテキストメニューをプラグイン出来ます。
  • MoNo.Studio.ContextMenuFactory<対象エンティティの型> を継承します。
  • それ以外はメインメニューのプラグインと同じです。
  • 上記サンプルでは Tris3d (= MoNo.Geometries.Soup<Triangle<Point3d>>) の面数を表示するコマンドを定義しています。

インポートファイル形式 (FileImporterSample.fs)

type [<Export (typeof<Studio.IFileImporter>)>] private FileImporterSample() =
  interface Studio.IFileImporter with
    member val Caption = "Foo File"
    member val FileFilters = ["*.foo"]
    member __.TryImport path =
      Progress.Make (fun token -> None)
  • MEF アーキテクチャを利用してインポート可能なファイル形式をプラグイン出来ます。
  • IFileImporter を実装してファイル形式のキャプションや拡張子(フィルタ)を設定します。