===================================== QuickTemplate で始めよう ===================================== .. toctree:: :maxdepth: 2 :caption: Contents: :glob: :download:`MoNo.RAIL <../../MoNo.RAIL-release.zip>` には MoNo.RAIL.Samples というサンプルプロジェクト群が含まれています。 ここでは MoNo.RAIL.Samples/MoNoStudioSamples/QuickTemplate/ というフォルダに含まれているサンプルコードについて解説します。 Quick Template とは ===================================== * Visual Studio 2015 向けの F# のプロジェクトテンプレートです。 * MoNo.Studio 3D のプラグインを開発するためのテンプレートです。 * MoNo.Studio 3D のメニュー(コマンド)や、エンティティ、シーンなどをプラグイン出来ます。 .. image:: overview.png Samples メニュー ===================================== QuickTemplate プロジェクトをビルドし実行すると、MoNo.Studio 3D のメインメニューに次のようなサンプルメニューが加わっていることが分かります。 .. image:: 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 関数をオーバーライドしてプラグインするメニューを定義します。 .. code-block:: fsharp open System.ComponentModel.Composition open MoNo open MoNo.Studio type [)>] private MyMenuFactory() = inherit MenuFactory() override __.CreateMenuItems con = [] Samples / MessageBox Samples (MessageMenu.fs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ メッセージボックスを表示するだけの最もシンプルなサンプルです。次のような形をしています。 .. code-block:: fsharp type [)>] private MessageMenuFactory() = inherit Studio.MenuFactory() override __.CreateMenuItems con = let items = [ MenuItem.New (...) MenuItem.New (...) ... ] [ MenuItem.Folder ("Samples", [ MenuItem.Folder ("MessageBox Samples", items) ]) ] 最も単純なメッセージボックスの表示コマンドは次のように定義されます。 .. code-block:: fsharp 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 のサンプルコードは概ね次のひな形に従っています。 .. code-block:: fsharp 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 """"""""""""""""""" .. code-block:: fsharp 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 """""""""""""""""""""" .. code-block:: fsharp 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 """""""""""""""""""""""""""""""""" .. code-block:: fsharp 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) """""""""""""""""""""""""""""""" .. code-block:: fsharp // 頂点配列を作成 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) """"""""""""""""""""""""""""""""" .. code-block:: fsharp // 頂点配列を作成 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 """"""""""""""""" .. code-block:: fsharp 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) """""""""""""""""""" .. code-block:: fsharp 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() 等を推奨します。継続モナドについては :doc:`/src/wpf/cont` を参照して下さい。 Click """""""" .. code-block:: fsharp 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 """""""""""""" .. code-block:: fsharp 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 """"""""""" .. code-block:: fsharp 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 オブジェクトを生成してドキュメントに登録しています。 Draw Polyline """"""""""""""""" .. code-block:: fsharp 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 で用いた方法の延長でループ文によって複数回のクリックを検出するという方法は使えません。 そこでここでは次の方法を採っています。 #. ``MoNo.Ctrl.Operation`` オブジェクトを生成し、イベントハンドラ等を設定 #. WorldScenes に作図途中の折れ線をプレビューする描画処理を追加 #. MouseMove イベントで現在のカーソル位置をワールド座標系に変換 #. MouseClick イベントで作図中の折れ線オブジェクトに点を追加 #. KeyDown イベントでスペースキーを検知したら Exit() メソッドを呼び出して完了 #. ``do! con.ByOperation op`` によってオペレーションを実行 #. オペレーションが完了したら作成された折れ線を登録 実は今まで使ってきた ``con.GetLButtonClick()`` 等の処理は、内部では ``MoNo.Ctrl.Operation`` クラスが動いています。これらについては :doc:`/src/operation/index` を参照して下さい。 Object Picking """""""""""""""" .. code-block:: fsharp cont { let! view, e = con.GetLButtonClick (fun op -> op.Cursor <- Forms.Cursors.Hand) let pick = view.PickAt (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<Point3d>) """""""""""""""""""""""""""""""""""""""""" .. code-block:: fsharp cont { // MoNo.Geometries.Polyline エンティティを生成 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<Triangle3d>) """"""""""""""""""""""""""""""""""""" .. code-block:: fsharp 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<Point3d>) """"""""""""""""""""""""""""""""""" .. code-block:: fsharp 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 """"""" .. code-block:: fsharp 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 """""""""""" .. code-block:: fsharp 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) -------------------------------------------- .. code-block:: fsharp type [)>] Tris3dMenu () = inherit ContextMenuFactory () 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>) の面数を表示するコマンドを定義しています。 インポートファイル形式 (FileImporterSample.fs) --------------------------------------------------- .. code-block:: fsharp type [)>] 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 を実装してファイル形式のキャプションや拡張子(フィルタ)を設定します。