development of

Tieghaに関する備忘録とおまけ

【IJCAD】プログレスメーターを表示する

プログレスメーターを表示する

.NET APIプログレスメーターを表示させるには、 ProgressMeter クラスを使用するのですが、IJCAD 2018 までは API に不具合があるのか、プログレスメーターがうまく表示されませんでした。
ただこの問題は IJCAD 2019 では修正されているようです。

サンプルコード

var pm = new ProgressMeter();
pm.Start("プログレスバーサンプル");
pm.SetLimit(100);

for (var i = 0; i <= 100; i++)
{
    System.Threading.Thread.Sleep(10);
    pm.MeterProgress();
}
pm.Stop();

実行結果

IJCAD 2019 では問題なくプログレスメーターを表示できています。 f:id:tknmt:20190527154151g:plain

IJCAD 2018 ではサンプルコードを実行しても何も表示されませんでした。 f:id:tknmt:20190527154246g:plain

【IJCAD】任意の軸のアルゴリズムについて

任意の軸のアルゴリズムとは

任意の軸のアルゴリズムは、特定のオブジェクトで OCS(オブジェクト座標系)を生成するために、CAD の内部で使用されているアルゴリズムです。

例えば、円オブジェクトの OCS が任意の軸のアルゴリズムによって、円の X 軸と Y 軸の方向が決められています。

アルゴリズムの詳細

AutoCAD のヘルプには、任意の軸のアルゴリズムについて次のような感じで説明されています。

  1. オブジェクトの法線ベクトルを N とする。
  2. ベクトル N の X の絶対値と Y の絶対値が、両方とも 1/64 未満の場合は、WCS の Y 軸と法線ベクトル N の外積の値が X 軸になる。 それ以外の場合は WCS の Z 軸と法線ベクトル N の外積の値が X 軸 になる。
  3. 法線ベクトル N と、2 で求めた X 軸のベクトルの外積の値が Y 軸になる。

.NET API だとこんな感じですね。

Vector3d normal = ent.Normal;
Vector3d axisX, axisY;
if (Math.Abs(normal.X) < 1.0 / 64.0 && Math.Abs(normal.Y) < 1.0 / 64.0)
{
    axisX = Vector3d.YAxis.CrossProduct(normal);
}
else
{
    axisX = Vector3d.ZAxis.CrossProduct(normal);
}
axisY = normal.CrossProduct(axisX);

サンプルプロジェクト

任意の軸のアルゴリズムを用いて、円オブジェクト上にカーソルが近づいている時に、円のオブジェクト座標系を表示させすサンプルを作成してみました。

ArbitraryAxisAlgorithmSample.zip - Google ドライブ

実行結果

サンプルを IJCAD 上で動かしてみると次のようになります。


ArbitraryAxisAlgorithmSample

オブジェクトスナップをカスタマイズする

オブジェクトスナップを追加する

.NET API によるカスタマイズの一つとして、CustomObjectSnapMode クラスを使用することで、独自のオブジェクトスナップを追加することができます。

今回は、円オブジェクトに対して特定の角度の円周上点にスナップされる、カスタムオブジェクトスナップを追加してみました。

IJCAD にも CustomObjectSnapMode クラスは 存在していますが、IJCAD ではうまくスナップ機能が有効になりませんでしたので、今回のサンプルは AutoCAD 向けです。
ただ ObjectGRX を使用した場合は、 IJCAD でもオブジェクトスナップのカスタマイズは可能でしたので、そのうち .NET API でも AutoCAD と同じように .NET API でもカスタマイズができるようになると思います。

サンプルコード

using System;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(CustomOSnapSample.CustomOSnap))]
[assembly: ExtensionApplication(typeof(CustomOSnapSample.CustomOSnap))]

namespace CustomOSnapSample
{
    public class CustomOSnap : IExtensionApplication
    {
        /// <summary>スナップされる角度</summary>
        private static double Angle { get; set; }
        /// <summary>グリフの角度</summary>
        private static Vector3d Dir { get; set; }
        /// <summary>表示されるグリフ</summary>
        private static SampleGlyph Glyph { get; set; }
        /// <summary>カスタムオブジェクトスナップの情報</summary>
        private static SampleOSnapInfo Info { get; set; }
        /// <summary>カスタムオブジェクトスナップモード</summary>
        private static CustomObjectSnapMode Mode { get; set; }

        // 初期化処理
        public void Initialize()
        {
            Angle = Math.PI / 4.0;
            Dir = Vector3d.XAxis;
            Glyph = new SampleGlyph();
            Info = new SampleOSnapInfo();
            Mode = new CustomObjectSnapMode("SAMPLEOSNAP", "SAMPLEOSNAP", "Custom object snap sample", Glyph);

            // 円オブジェクトに新しいコールバックを適用する
            Mode.ApplyToEntityType(RXClass.GetClass(typeof(Circle)), new AddObjectSnapInfo(Info.SnapInfoCircle));
        }

        // 終了処理
        public void Terminate()
        {
        }

        // カスタムオブジェクトスナップの有効/無効を切り替える
        [CommandMethod("CUSTOMOSNAP")]
        public void CustomObjectSnap()
        {
            // カスタムオブジェクトスナップが有効かどうか
            if (CustomObjectSnapMode.IsActive("SAMPLEOSNAP"))
            {
                // 有効の場合無効にする
                CustomObjectSnapMode.Deactivate("SAMPLEOSNAP");
            }
            else
            {
                // 無効の場合有効にする
                CustomObjectSnapMode.Activate("SAMPLEOSNAP");
            }
        }

        // スナップされる角度を設定する
        [CommandMethod("SNAPANGLE")]
        public void SnapAngle()
        {
            var ed = Application.DocumentManager.MdiActiveDocument.Editor;
            var opt = new PromptAngleOptions("\n角度を入力");
            opt.DefaultValue = Angle;
            opt.UseDefaultValue = true;
            var res = ed.GetAngle(opt);
            if (res.Status != PromptStatus.OK) return;
            Angle = res.Value;
        }

        /// <summary>カスタムオブジェクトスナップのグリフ</summary>
        public class SampleGlyph : Glyph
        {
            /// <summary>グリフが表示される基準点</summary>
            private Point3d Pt { get; set; }

            public override void SetLocation(Point3d point)
            {
                // 基準点としてスナップ点を取得
                Pt = point;
            }

            protected override void SubViewportDraw(ViewportDraw vd)
            {
                // ビューポートを取得
                var vport = vd.Viewport;
                // 1平方メートルあたりのピクセル数を取得
                var pixels = vport.GetNumPixelsInUnitSquare(Pt);
                // グリフサイズとピクセル数からカメラの高さに合ったグリフの高さを計算
                var glyphHeight = (CustomObjectSnapMode.GlyphSize / pixels.Y) * 2.0;
                // 基準点をWCSからDCSに変換
                var pos = Pt.TransformBy(vport.EyeToWorldTransform);
                // フォントの特性
                var font = new FontDescriptor("Meiryo", false, false, 0, 0);
                // テキストスタイルを作成
                var style = new TextStyle();
                style.Font = font;
                style.TextSize = glyphHeight;
                // テキストのグリフの位置調整用ベクトル
                var vec = (Dir.GetNormal() * glyphHeight / 2.0) + (Dir.GetPerpendicularVector().Negate().GetNormal() * glyphHeight / 2.0);
                // テキストのグリフを表示
                vd.Geometry.Text((pos + vec).TransformBy(vport.EyeToWorldTransform), vport.ViewDirection, Dir, "☜", true, style);
                // 塗りつぶし設定を変更
                vd.SubEntityTraits.FillType = FillType.FillAlways;
                // スナップ位置に黒丸のグリフを表示
                vd.Geometry.Circle(pos, glyphHeight / 4, vport.ViewDirection);
            }
        }

        /// <summary>コールバック用のクラス</summary>
        public class SampleOSnapInfo
        {
            // 円にスナップ
            public void SnapInfoCircle(ObjectSnapContext context, ObjectSnapInfo result)
            {
                // ピックされているオブジェクトを取得
                var circle = context.PickedObject as Circle;
                // オブジェクトが円かどうか
                if (circle == null) return;
                // スナップ点を取得
                var snapPt = circle.GetPointAtParameter(Angle);
                // リザルトにスナップ点を追加
                result.SnapPoints.Add(snapPt);
                // グリフの方向を更新
                Dir = circle.Center.GetVectorTo(snapPt);
            }
        }
    }
}

実行結果


AutoCAD CustomObjectSnap

.NET アセンブリをロードして、ステータスバーから追加したカスタムオブジェクトスナップを有効にすると、円に対して独自のオブジェクトスナップが使用できるようになります。

【IJCAD】ダイナミックブロックのプロパティを変更する

ダイナミックブロックのプロパティを変更する

前回の記事ではダイナミックブロックに設定されているプロパティの取得方法を紹介しましたが、
tknmt.hatenablog.com

今回は、ダイナミックブロックのプロパティの値を変更する方法について紹介しようと思います。 といっても前回の記事で値を取得する時に使用したValueプロパティに値を割り当てるだけですが…

サンプルコード

[CommandMethod("SAMPLECMD")]
public void SampleCommand()
{
    var db = Application.DocumentManager.MdiActiveDocument.Database;
    var ed = Application.DocumentManager.MdiActiveDocument.Editor;
    var res1 = ed.GetEntity("\nダイナミックブロックを選択");
    if (res1.Status != PromptStatus.OK) return;

    // トランザクションを開始
    using (var tr = db.TransactionManager.StartTransaction())
    {
        // 選択したダイナミックブロックを開く
        using (var blockRef = tr.GetObject(res1.ObjectId, OpenMode.ForWrite) as BlockReference)
        {
            // ブロック参照が開かれたかどうか確認
            if (blockRef == null) return;
            
            // ダイナミックブロックかどうか確認
            if(blockRef.IsDynamicBlock)
            {
                // ダイナミックブロックに含まれるプロパティを取得
                var properties = blockRef.DynamicBlockReferencePropertyCollection;
                foreach(DynamicBlockReferenceProperty prop in properties)
                {
                    // 距離1プロパティかどうか確認
                    if(prop.PropertyName == "距離1")
                    {
                        // プロパティが読み書き可能かどうか確認
                        if(prop.ReadOnly == false)
                        {
                            var res2 = ed.GetDistance("\n距離を入力");
                            if(res2.Status == PromptStatus.OK)
                            {
                                // 入力した距離を設定
                                prop.Value = res2.Value;
                            }
                        }
                    }
                }
            }
        }
        //トランザクションをコミット
        tr.Commit();
    }
}

サンプル図面

ダイナミックブロック.dwg - Google ドライブ

※サンプルコードについての補足

サンプル図面のダイナミックブロックには、直線状パラメータによるストレッチしか設定されていないので、サンプルコードでは入力された距離の値を割り当てています。
ただ、値を割り当てる際に注意点が必要で、直線状パラメータであれば距離を示す値を、回転パラメータであれば角度を示す値を、可視性のパラメータであれば現在の可視性の状態を示す値を設定しなければなりません。

実行結果

サンプル図面に対してサンプルコードのコマンドを実行すると、次のような結果になります。 f:id:tknmt:20190520162928g:plain

【IJCAD】ダイナミックブロックのプロパティを取得する

ダイナミックブロックのプロパティを取得する

IJCAD の .NET API を使用して、ダイナミックブロックに設定されているプロパティを取得したいと思います。 f:id:tknmt:20190523144126p:plain

ダイナミックブロックのプロパティとは、ダイナミックブロックに設定されているパラメータセットの名前や、現在設定されている値などです。

サンプルコード

[CommandMethod("SAMPLECMD")]
public void SampleCommand()
{
    var db = Application.DocumentManager.MdiActiveDocument.Database;
    var ed = Application.DocumentManager.MdiActiveDocument.Editor;

    var res = ed.GetEntity("\nダイナミックブロックを選択");
    if (res.Status != PromptStatus.OK) return;

    // トランザクションを開始
    using (var tr = db.TransactionManager.StartTransaction())
    {
        // 選択したダイナミックブロックを開く
        using (var blockRef = tr.GetObject(res.ObjectId, OpenMode.ForRead) as BlockReference)
        {
            // ブロック参照が開かれたかどうか確認
            if (blockRef == null) return;
            
            // ダイナミックブロックかどうか確認
            if(blockRef.IsDynamicBlock)
            {
                // ダイナミックブロックに含まれるプロパティを取得
                var properties = blockRef.DynamicBlockReferencePropertyCollection;
                foreach(DynamicBlockReferenceProperty prop in properties)
                {
                    // プロパティ名を出力
                    ed.WriteMessage($"\nプロパティ名 : {prop.PropertyName}");
                    // プロパティの値を出力
                    ed.WriteMessage($"\n値 : {prop.Value}");
                }
            }
        }
    }
}

サンプル図面

ダイナミックブロック.dwg - Google ドライブ

※サンプルコードについての補足

プロパティ名を取得するには、DynamicBlockReferenceProperty クラスの PropertyName プロパティを使用して、プロパティの値を取得するには Value プロパティを使用します。

実行結果

サンプル図面に対してサンプルコードのコマンドを実行すると、次のようにIJCADのコマンドラインにプロパティの情報が出力されます。

f:id:tknmt:20190520153956p:plain

距離1 がダイナミックブロックに設定されている直線状パラメータで、現在の値は 50 とコマンドラインに出力されています。 Origin というのが上記の直線状パラメータの原点なのですが、値がありません。
AutoCAD では、直線状パラメータの原点の座標値をここで取得できるのですが、IJCAD で取得できないのは API のバグだと思います。

【IJCAD】データベース内のオブジェクトを選択する

ObjectGRX の強み

前回の記事ではカスタマイズするなら .NET API を使用すべきと主張しましたが、今回のは ObjectGRX のあるクラスを使用したオブジェクト選択のサンプルを紹介します。

データベースから選択セットを作成する

IJCAD の ObjectGRX には OdDbSelectionSet というクラスが存在していまして、このクラスを使用することで、データベースから選択セットを作成できます。

OdDbSelectionSet クラスは Teigha という IJCAD の DWG を読み書きするために採用しているエンジンの、ソフトウェアライブラリ特有のクラスなので 、AutoCAD の ObjectARX には存在しないと思います。

OdResBuf クラスは resbuf 構造体と同じような機能を持ったクラスで、OdDbSelectionSet::select メソッドで選択セットフィルタを設定する際に使用します。このクラスも ObjectARX には存在しないと思います。

サンプルコード

オブジェクトを全て選択するが、円以外はフィルタリングして、円のみを選択して色を変更しています。

#include "DbSSet.h"
void SampleCommand()
{
    OdDbDatabase* db = oddbHostApplicationServices()->workingDatabase();

    OdResBufPtr filter = OdResBuf::newRb(OdDb::kDxfStart, _T("CIRCLE"));
    OdDbSelectionSetPtr sset = OdDbSelectionSet::select(db, filter);
    OdDbObjectIdArray ids = sset->objectIdArray();

    for each (OdDbObjectId id in ids)
    {
        OdDbEntity* ent;
        oddbOpenOdDbEntity(ent, id, OdDb::kForWrite);
        ent->setColorIndex(1);
        ent->close();
    }
}

実行結果

サンプルコードの処理を実行したら次のような感じになります。 f:id:tknmt:20190517162951g:plain

例えば readDwgFile メソッドを使用して、メモリ上にロードしたデータベース内に対して、特定のオブジェクトのみ選択したいときに役に立つかもしれません。

IJCADのアプリケーション開発は.NET APIでやるべき

IJCADのカスタマイズするために、LISP、COM、ObjectGRX、.NET API の 4 つが API が主に使用されています。

ちょっとしたマクロのようなカスタマイズを行うのであれば、LISP や COM も選択肢に入りますが、新しくIJCAD用のアプリケーション開発を始める場合や、AutoCADや他の互換CAD製品からIJCADに乗り換える際にアプリケーションの移植を場合は、元のAPIが何であれ .NET APIを使用して開発を進めるべきです。

まず IJCAD の、各 API についてのメリットとデメリットを簡単に紹介しますが、ここで挙げた内容はあくまで個人的な見解です。

 

LISP(AutoLISP、VisualLISP、DCL)

  • メリット
    • メモ帳があれば開発が進められコンパイル作業が必要無い
    • バージョンアップ毎に再開発する必要が無い
    • AutoCADや他の互換CADでそのままLISPファイルが使用できたりする
  • デメリット
    • エラーの検出が実行するまでわからない
    • デバッグできないのでエラーチェックに手間がかかる
    • IJCADの外部へのアクセスは限定的
    • LISP関数で提供されていない部分のカスタマイズはできない

■ COM(ActiveX オートメーション)

  • メリット
    • 初心者でも扱いやすい VBA で開発を進められる
    • 他のCOMを利用したソフトウェアと連携できる
    • 例外処理を使用できる
  • デメリット
    • 大量のデータを扱う場合は処理が遅くなる
    • IJCADの64bit版では使えない
    • プロジェクトファイルはAutoCADや他の互換CADと互換性が無い

■ ObjectGRX

  • メリット
    • 事実上IJCADで可能なことは全ての部分でカスタマイズが可能
    • MFCMicrosoft Foundation Class)が利用できる
    • 処理が最も高速>
    • カスタムオブジェクトなど機能自体の拡張が可能
    • とにかく、やろうと思えば何でもできる
  • デメリット
    • 使用する開発言語(C++)の難易度や学習コストが高い
    • ポインタを扱うためバグがあるとIJCADが異常終了する
    • Visual Stadio 2010 が必要
    • コンパイルが必要

.NET API

  • メリット
    • 使用する開発言語(C# または VB.NET)の学習コストが比較的低い
    • .NET Frameworkが持つ豊富な機能を利用することができる
    • IJCADのほぼすべての機能をカスタマイズ可能
    • .NET APIを使用したカスタマイズに関するドキュメントが豊富
    • 開発環境となるVisualStadioバージョンに依存しなくなる
  • デメリット
    • 現時点手は他の開発言語よりも不具合が目立つ
    • コーディング次第でメモリリークが発生してしまうリスクがある
    • コンパイルが必要

 

.NET API のメリットとデメリットは、もう少し掘り下げていきたいと思います。

 

使用する開発言語(C# または VB.NET)の学習コストが比較的低い

.NET APIで使用する開発言語は C# または VB.NET を使用します。 C#VB.NET はどちらも、初心者向けの学習参考書や、ネットのドキュメントが多く存在していて、初めてプログラミング言語にさわる方でも取り組みやすい開発環境です。

また VB.NETC# は業務システム開発や、各種プラットフォーム上のアプリケーション開発にも使用される開発言語なので、決して習得して損のない開発言語です。

NET Frameworkが持つ豊富な機能を利用することができる

コマンド内で表示させるダイアログなどは、.NET Frameworkに含まれるグラフィカルユーザーインターフェイスAPIWindows Forms)を利用することができ、C# ではさらにWPFWindows Presentation Foundation)も利用することができます。

他にもジェネリックLINQ動的言語ランタイムなど、.NET Framework の機能を活用することができます。

IJCADのほぼすべての機能をカスタマイズ可能

VBALISP では IJCAD 上でカスタマイズできる範囲が限定されていていますが、カスタムオブジェクトの定義などを除き、ほぼすべての範囲をカスタマイズ可能です。

ObjectGRX であれば、IJCAD の全ての範囲のカスタマイズが可能になりますが、やはりC++C#VB.NET よりも、開発言語としての学習コストや難易度が高いため、IJCAD 自体を拡張するようなカスタマイズでない限りは、.NET API のカバー範囲で十分開発を進めていけます。

.NET APIによるカスタマイズに関するドキュメントが豊富

IJCAD の開発者向けのドキュメントはまだまだ不足していますが、それでも VBALISP などの他の開発言語と比べて、.NET API のドキュメントの方が多く用意されています。

また、IJCAD の .NET APIAutoCAD .NET API と高い互換性をもっている為、AutoCAD 向けにコーディングされたサンプルコードが IJCAD のコーディングの参考になります。

開発環境となる Visual Stadio バージョンに依存しなくなる

アセンブリの開発に必要な .NET Framework のバージョンが4.0と記されていますが、実際には4.0以降のバージョンであれば問題なく開発が行えるます。

その為、.NET Framework 4.0 をサポートした Visual Stadio 2010 以降であれば、どのVisual Stadio のバージョンを使用しても開発を進めることができます。

 

他の開発言語よりも不具合が目立つ

.NET API によるカスタマイズが IJCAD に導入されたのは IJCAD 2015 からで、比較的新しい開発言語のため、他の開発言語よりも API に不具合が目立つ傾向にあります。

ですが、バージョンのアップデート毎に確実にAPIの品質は良くなっていると思われます。

コーディング次第でメモリリークが発生してしまうリスクがある

.NET API による開発でも、一部のクラスの扱いを間違えるとメモリリーク発生します。

これは、.NET API のクラスは ObjectGRX のクラスををラップして公開している為、CLR(Common Language Runtime)の管理外の部分もあり、一部のクラスは適切に解放処理を行わない場合メモリリークが発生して、Windowsが終了するまでメモリを確保した状態になってしまいます。

コンパイルが必要

VBALISPと違い、コーディングが完了したら即実行というわけにはいかず、プロジェクトのビルドを実行してソースコードコンパイルする必要があります。

設計やコーディングにミスがあり修正を行った場合はその都度コンパイルしなければなりません。

 

■ 総括

IJCAD の .NET API には不具合が目立つというデメリットがありますが、デメリットが気にならなくなるほどにメリットが多く、これからIJCAD 用のアプリケーションの開発や移植を行う場合は、.NET API を使用するべきです。

LISP は覚えてしまえばちょっとしたことが楽にできますが、やはりエラーチェックがとにかく面倒で、VBA は 64bit 版の IJCAD で使用できない時点で選択する余地は無いです。

※ただし、C++ を習得済み、またはC++を挫折せず習得する自信があるのであれば、ObjectGRX を使用した開発が最適解です。

ただ今後のメンテナンスの事を考えると、引き継がせる相手はそれなりに優秀な人材であることと、否が応でも C++ を叩き込む必要があります。