ラベル SVG の投稿を表示しています。 すべての投稿を表示
ラベル SVG の投稿を表示しています。 すべての投稿を表示

2018年2月17日土曜日

「変換くん」にレーザー加工機と通信する機能を追加して「fConnect」なるアプリを作ろうと思うのだけれど、それの要件定義と外部設計を兼ねたスケルトン

「fConnect」というアプリのスケルトン(画面のみで実際の動作が実装されていない)を作りました。
以降、「接続くんスケルトン」とします。

せっかち人のためのダウンロードはこちら → https://goo.gl/HfnvYR

■「fConnect」は何をするものなのか
CAD(など)で作成した平面データを読み込みます。
線色毎に動作条件(順序、出力、速度)を設定します。
レーザー加工機(FABOOL Laser Mini など)にデータを送信して動作させます。
その他、原点復帰や範囲確認など動作上の基本的な動作を加工機に指示します。
以降、「接続くん」とします。
※接続くん : 実際に動作する、これから実装するモジュール
※接続くんスケルトン : 今回の画面動作のみのモジュール

■なぜ「接続くん」を作るのか
当者が求めるのは「できるだけ精度よく」「ベクター加工を行う」となります。レーザー加工機のユーザとしては少し偏っている可能性もあります。
そして、先日、メーカー純正のソフトウェアがアップデートされましたが、当者とはだいぶ異なる方向性でありました。
だったら作れば良いじゃないって自分の中のアントワ.NETがいうので、自分のほしい機能とスペックの段階的な実現を目指すことにしました。

■何を「接続くん」に求めるのか
「操作性」「動作精度」「実装のメンテナンス性」を重要視します。
具体的には「官能的(?)に操作ができ」「加工機の機械的電気的なポテンシャルを引き出し」「状況に応じた改修の容易なプログラムの実装」となります。
最後の項目は内部的な内容です。

■立脚点
当方の現在の環境と内容が、
・JW_CADでデータを作成する
加工したい箇所をJW_CADでDXFファイルに変換する
DXFファイルをInkscapeで開いてSVGファイルに変換する
メーカー純正のソフトウェアに取り込む
カットする
と、なっています。
これが、「接続くん」を使用する場合は。
・JW_CADでデータを作成する
・JW_CADの外部変形を使用して選択範囲のデータを「接続くん」に取り込む
・カットする
と、変更されます。
また、「変換くん」の実装を流用して、「DXFファイル」「SVGファイル」を入力対象に含めるのは比較的容易です。
※ラスター加工の実装は当面予定されないと思われる

■超上流工程的なもの
このドキュメント(?)がある程度、超上流工程の内容になっています。
それ以外は基本的に省略します。
必要になったら遡って作成/追加するかもしれません。

■想定する動作環境
OS : Windows 7 ~ Windows 10
加工機 : メーカー純正の「オフラインバージョン」ソフトウェアが動作する加工機

■想定する開発手法
スパイラル開発を行います。※用語の正確なところは軽くスルーの方向でよろしくです
事前に最終形態と思われる実装内容を仮で定義し、優先度をつけて段階的にかつ長すぎないスパンでリリースを行います。リリースを行うたびに以降の実装内容を検討し、これを繰り返します。

■実装における責任等
フルスピードで実装するのは不可能ですので、最初期バージョンで、夏頃リリース、何かの弾みで5月頃にリリースできるかも知れない、くらいに設定します。
また、なんらかの都合で実装の中断、中止も有りうるものとします。
モジュールのリリース後も責任を持った対応は基本的に期待できないものとします。
リリースにおいて、βリリースとし、ユーザにテスト等の負担とリスクを負ってもらう可能性もあるとします。
もし何らかの協力をいただける場合も、上記不利益の可能性を納得の上のこととします。
(それと・・・、自分の必要とする内容が実現したら急にテンションが下がるとかもありうるのがリスクかも知れません)

■今回のモジュール
「接続くん」のスケルトン(画面のみで実際の動作が実装されていない)になります。
動作のイメージを確認し、共有することができます。
画面操作(クリック、入力、Tab動作)に関しては、実装後の内容とほぼ同じとなります。
一部(Jogの移動量変更など)は多少動作します。(表示の数値の変化など)

■目的
基本的には上述の前提で実装を行おうと思いますが、無人の荒野を一人進んでしまうともったいないのでその可能性を下げるのがひとつ大きな目的になります。
また。想定外に大きく多くの要望があれば、全体プランの見直し等の検討もできます。
どうしても取捨や優先順序は発生してしまいますが、コメント欄に記載いただけると幸いです。
(コメント欄、多分、機能する、はず)
場合によってはアンケートを実施することもできるようです。

■動作方法と確認内容
・モジュールをダウンロードします → https://goo.gl/HfnvYR
・zipファイルをダウンロードし解凍します (2018/2/17時点でv1.0.0が最新です)
・exeファイルがひとつありますので、任意の場所に設置します
・アプリを起動します
起動直後の画面です
実質的にこのサイズがディスプレイの最小必要解像度になるかと思います

・メニューバー
シリアルポート関係の項目が設置されると思います
実装上、工夫の余地が少ない部分と思います

・機種選択(加工エリアサイズ指定)と図形描画エリア
機種選択を行うことによって加工エリアサイズを特定します
必要な場合は直接入力できます
図形の描画は長方形などの単純化した図形でも大丈夫なのかもしれません

・加工条件などのエリア
右側の4つのタブで「加工条件」「回転、繰り返し」「加工位置」「加工実行」の操作を行います
基本的にタブの順序で操作することになります
ここの部分の操作性が特に重要と思います
配色にセンス的なものを求めても無い袖は振れないのです
(無い袖は触れないと思ってました。すれ違うときにあたら無い的な)

・加工条件
各色まとめて条件の設定ができます
その後、色個別に条件の設定ができます
図形描画エリアとの間を動かせるようにして、各色の設定項目を横一列に・・・、する??
(現状でも部品の設置は相当複雑になっています)
(実装の確実性を考慮してここでとどめるか、操作性を優先して踏み込むか・・・)
設定されている色はJW_CADの標準色(?)になります
色の関係で初っ端はJW_CADの外部変形のみの対応になるかも・・・
設定値を保存する機能はPhase2以降の実装になると思います

・回転、繰り返し

「回転、反転」はCAD側での編集と管理が簡便になるような気がします
「繰り返し」は似た様な部品をたくさん作るときに便利かなー、と
実装はおそらくPhase3以降になると思います
(こう見ると回転のアイコン酷いな・・)


・加工位置
「移動」or「Jog」でレーザーユニット(?)を動かし、その座標を加工原点(?)に設定します
これはPhase1で必要な機能になります

・加工実行
「通知」と「一時休止(冷却)」はPhase2かPhase3での実装になると思います
「進捗」はPhase2になりそうですがPhase1にできれば入れたいとも思います

■開発環境
以下の環境で実装と動作確認を行う予定です。

--共通--
既定の.NETFramework:v4.5.2
Inkscape:Inkscape 0.91
SVGファイル:Inkscapeで出力した“Inkscape SVGファイル“、Inkscapeで出力した“プレーンSVGファイル“
DXFファイル:JW_CADで出力したDXFファイル
JW_CAD外部変形 : JW_CAD 8.01b

--実装--
OS:Windows 7 Home (64bit)
開発環境 : C# (Visual Studio Community 2017)

--加工動作--
OS:Windows 7 Pro (32bit)
加工機:Fabool Laser Mini 3.5


2018年2月16日金曜日

SVG回りで円弧を平坦化したりDXFから変換したり、追ってはJW_CADと連携したり。変換くん?

「SvgConvForFab」というアプリを作りました。
(多分、バージョンアップか何かで「SvgConv」という名前に変わると思います)
以降、「変換くん」とします。


せっかち人のためのダウンロードはこちら → https://goo.gl/T96fKV

■平坦化とは
円弧(円、円弧、楕円、楕円弧)や曲線を近似の直線群に置き換えることを平坦化と呼ぶようです。少なくともC#のAPI仕様にはそう書いてあった、気がします。

■どういう動作をするのか
SVGファイル(XMLベースのベクターなんとか。Wiki)の「パス」の円弧を平坦化します。ベジエ曲線など、対応していないアレもあります。
DXFファイル(AutoCADのデータ形式とかなんとか。Wiki)はSVGに変換してから平坦化を行ってSVGファイルにしています。

■背景
格安のレーザー加工機を持っています。Fabool Laser Mini といいます。すでに1年以上、可用性と精度の向上が趣味になってしまっています。
(精度に関しては「加工機」という言葉から想定するものと自分的には隔たりがあったのですが、値段が値段ですし、いじることでいろいろ分かりますし、ひとつの趣味としてもまあありなんじゃないかと思っています。購入当初の目的がどんどん先延ばしではありますが)
いろいろクリアしてきましたが、現状では加工機ソフトウェアの曲線の処理がネックとなってきました。だったら、自前で事前に平坦化すればよくなくなくなくない?、って感じです。

加工機ソフトウェアとSVGファイルとDXFファイルとJW_CADとYシャツと私
加工機ソフトウェアの入力がSVGファイルなので、変換くんの出力はSVGファイルになります。
変換くんの入力(1)はSVGファイルです。加工機ソフトウェアの入力がSVGファイルなので、まずは押さえておきます。
変換くんの入力(2)はDXFファイルです。AutoCADを使っている人は多分InkscapaeでSVGファイルに変換していると思いますので少し手間が省けます。
(Inkscapeの起動が重くてですね・・・。あと、Windows版の場合、動作があちこちWindows的で無いというかなんというかであれですよ)
変換くんの入力(3)はJWW(JW_CADのファイル。Wiki)、といいたいところなのですが、バイナリの入出力がめんどく感じたのと、「外部変形」というものを知って、実装するのも使うのも良い感じポイのでこれにしようと思うそうしよう。

■将来的には加工機との通信を行いたい
変換くんが、SVGファイル、DXFファイル、JW_CAD外部変形、すべてに対応できた場合、入力側の実装は十分と思います。(なんせ自分が使っているのがJW_CADで、DXFファイルを入力に含めたのはほとんど行きがけの駄賃だったりしますです。JW_CADのJWWファイルをSVGにするには、JW_CADでDXFファイルを出力→InkscapeでSVGに変換、っていうなんともはぶられている感満載の手順が必要だったりします)
そいで、加工機へ動作指示をするのは、「シリアル通信で」「Gコードぽいなにか」を送ればよいらしいです。冷却休止とか進捗メールとか動作制御での精度向上とかVVVFインバータ鳴り響かせるとか、実装の手間の割りに実現できることが山盛りなのでどうにか実現したいです。
(名前も考えて有ってですね、fConnect、っつーんです。かっこいいっしょ)

■効果
変換を行わなかった場合(F384、100%。直径10mm)

変換を行った場合(F384、100%。直径10mm)
左:開始点と思われる箇所でぶれている
右:誘導に接線を追加したところスムースに

円を加工したら曲線が滑らかになりました。が、一部がいびつになっています。
接線を追加して動きを誘導したところ、きれいな円になりました。

レーザーユニットを左から見たところ
光学キャリアのローレットの高さがレーザーユニットの上下方向の重心
Y軸が動く度に右回り、左回りと動いている模様

どうやら、レーザーユニットがX軸を軸にしてぶれて(回転して)います。
これに関してはハードウェア的な変更か、制御的な変更を行わないと改善しないと思われます。
実際にはポツンと円を加工することは少ない(?)ので、写真ほど顕著ではないと思われる
カットで「一部が抜けていない」現象はこれが原因なのでは無いかと思われる
すなわち、この現象が解決できれば実質的に加工動作のスピードアップになる

■変換くんの使い方
・モジュールをダウンロードします → https://goo.gl/T96fKV
・zipファイルをダウンロードし解凍します (2018/1/28時点でv1.2.0が最新です)
・exeファイルがひとつありますので、任意の場所に設置します
・アプリを起動します
・SVGファイル or DXFファイル をウィンドウにドロップします

DXFファイルの場合、注意メッセージが表示されます

DXFファイルの場合、Inkscapeのダイアログが表示されます
「A4 サイズに自動調整する」のチェックは必ず外してください

・元のファイルと同じディレクトリにファイルが生成されます
ふたつ生成された場合、ファイル名の長い方が使用するファイルです

・およそ、96~128等分されます

・加工結果
上:変換なし
下:変換あり
(穴の直径は、1mm、5mm、10mm)


※DXFファイルを変換するにはInkscapeがインストールされている必要があります (Inkscape 0.91を推奨)
※そういえばWindows Defenderにウィルス検知されたと連絡を貰ったのですが、結局良く分からず。ウィルスは実装していません、って言っても潔白の証明にはならないんですよねぇ

■留意点
・留意事項のほとんどはアプリを動作させたときに表示されます。
・変換後のSVGファイルのサイズは最大で40倍ほどになります。これに起因してかFaboolソフトウェアの動作が不安定になることがありました。加工を行っているときは目を離さないで下さい。
何回も加工していると画面が表示されなくなる現象がたまに出てました(データ例:変換前200KB、変換後1.2MB)。加工する度にChromeを起動し直すようにしたら発生が無くなりました。また、プロジェクトに保存して使用するとさらに安定動作しました。加工機への送信処理よりも、SVGファイルの取り込みにリソースを必要としているようです
・なんらかの損害が発生しても責任は負いかねます。(一度だけ、暴走してぶつけました)
※加工機の動作上の仕様は知らずにアプリを実装しています。何らかの変化により効果が失われることあります。また、タイムリーに改修できない可能性もあります。

■開発環境
以下の環境で実装と動作確認を行っています。

--アプリ動作--
OS:Windows 7 Home (64bit)
既定の.NETFramework:v4.5.2
Inkscape:Inkscape 0.91
SVGファイル:Inkscapeで出力した“Inkscape SVGファイル“、Inkscapeで出力した“プレーンSVGファイル“
DXFファイル:JW_CADで出力したDXFファイル


--加工動作--
OS:Windows 7 Pro (32bit)
Faboolソフトウェア:オフラインバージョン
加工機:Fabool Laser Mini 3.5


※2018/1/28のFacebookへの投稿を再編集

2018年2月10日土曜日

C#で、JW_CADの外部変形用のファイル(JWC_TEMP.TXT)からSVG生成用のGraphicsPathを生成する

JW_CADには「外部変形」という機能があります。
JW_CADが指定のデータを一時ファイルに出力し、ユーザが機能を拡張できる仕組みです。

外部変形の使い方
JWのCADの使い方ー 外部変形プログラム

一時ファイルの仕様はこちらの
DAISY DAISY.. [CAD]
このあたり
jwc_temp.txtの解説(Jw_win)#単線
を参考にさせてもらいました。


コードは概要は下記のようになります。

・ほとんど置き換えているだけで特殊な処理は必要ありませんでした

・Y座標の上下が異なる(JW_CADは左下が原点)のでそこは反転しています

・参考にさせていただいた上記URLには、正円弧に関して
語尾の「1」と「0」は、それぞれ偏平率と軸角。正円弧の場合は「1」と「0」固定でよい
と記載がありましたが、私の環境(8.01b)では
正円弧を作成した後に回転すると末尾の「0」が「90」など回転させた数値になる
となりました

・「曲線」で作成された図形はJW_CADの内部では直線群として扱われているようです

・結果、「単線」「正円」「正円弧、楕円、楕円弧」の3種類のみの処理となりました

ということで、すっきりとした実装になりました。


※「jwwTempItem」はJWC_TEMP.TXTの情報を保持する自作オブジェクトクラス
※GraphicsPath : System.Drawing.Drawing2D.GraphicsPath
※「単線」は単純に座標を移しているだけなので割愛
※90DPIのSVGにするために、別途「(float)((float)90 / (float)25.4)」を乗じています


// --正円の生成 ->
GraphicsPath path = new GraphicsPath();
float x = jwwTempItem.Center.X - jwwTempItem.Radius;
float y = (jwwTempItem.Center.Y * (float)-1.0) - jwwTempItem.Radius;
float w = jwwTempItem.Radius * (float)2.0;
float h = jwwTempItem.Radius * (float)2.0;
path.AddEllipse(x, y, w, h);
// <-正円の生成 --


// -- 円弧、楕円、楕円弧の生成 ->
GraphicsPath path = new GraphicsPath();
float x = jwwTempItem.Center.X - jwwTempItem.Radius;
float y = (jwwTempItem.Center.Y * (float)-1.0) - (jwwTempItem.Radius * jwwTempItem.AspectRatio);
float w = jwwTempItem.Radius * (float)2.0;
float h = jwwTempItem.Radius * (float)2.0 * jwwTempItem.AspectRatio;
float startAngle = jwwTempItem.StartAngle * (float)(-1.0);
float sweepAngle = (jwwTempItem.EndAngle - jwwTempItem.StartAngle) * (float)-1.0;
if (jwwTempItem.EndAngle <= jwwTempItem.StartAngle)
{
    sweepAngle = (jwwTempItem.EndAngle + 360 - jwwTempItem.StartAngle) * (float)-1.0;
}
path.AddArc(x, y, w, h, startAngle, sweepAngle);

// 回転
Matrix matrix = new Matrix();
matrix.RotateAt(jwwTempItem.ShaftAngle * (float)(-1.0), new PointF(jwwTempItem.Center.X, jwwTempItem.Center.Y * (float)-1.0));
path.Transform(matrix);
// <- 円弧、楕円、楕円弧の生成 --



JW_CADで外部変形の範囲選択を行ったところ

変換後のSVGファイルをInkscapeで開いたところ

うぇーい

2018年2月1日木曜日

C#で、SVGのpathの円弧データをデコードする

Fabool Laser Mini が参照している SVGデータ の要素(?)は、すべて「path」になっています。(の様です)

この内容を扱うには座標情報(?)をデコード(?)する必要があるのですが、円弧、、、
(円弧には、円、円弧、楕円、楕円弧が含まれます。のようです)

10分でわかるSVG 基礎編 (2/5)

なんだこれ・・・

中心座標が無いんだ。。。円弧と楕円弧の場合、角度情報も無いんだ。。。ほとんど暗号解析やんか?どうすんのこれ?

SVG pathの楕円を描くコマンドが使いにくい

先人がぼやいているし


2、3日悩みましたね。できるのかよー、って

できませんでした。ティへぺろ

そして、Svg.dll ってライブラリを、てかこれもドキュメントが見つけられなくて、2回くらいスルーしてましたががが。
変数名とかと型名を見ながらデバッグで状況を探った結果、
「System.Drawing.Drawing2D.GraphicsPath」を取得することに成功っっ

下のコードは、上記の後に、平坦化(直線群に変換)して、それの一覧を取得まで行っています。
「PathArcAnal」は自作の処理クラス、「PathArcItem」は自作のオブジェクトクラスです。


/// 円弧を変換する
private string[] ConvArc(string dValue, DValueTypes dValueType, SValueTypes sValueType)
{
    // 引数の確認
    if (string.IsNullOrEmpty(dValue) == true) { return null; }
    if (sValueType == SValueTypes.Delete) { return null; }   // 無いはず

    // 処理クラスの生成(と解析)
    PathArcAnal pathArcAnal = new PathArcAnal(dValue, dValueType);

    // 解析結果の確認
    if (pathArcAnal.Result == false) { return null; }

    // svg.dllの処理クラスの生成
    Svg.SvgPath sPath = new Svg.SvgPath();

    // 座標で回る
    PointF lastPos = pathArcAnal.StartPos;
    float minRadius = float.MaxValue;
    foreach (PathArcItem pathArcItem in pathArcAnal.PathArcItems)
    {
        // svg.dllの円弧要素の生成
        SvgArcSegment arcSegment = new SvgArcSegment(lastPos,
                                                        pathArcItem.Radius.X, pathArcItem.Radius.Y,
                                                        pathArcItem.Angle,
                                                        pathArcItem.Size,
                                                        pathArcItem.Sweep,
                                                        pathArcItem.Pos);

        // 追加する
        sPath.PathData.Add(arcSegment);

        // 座標の保存
        lastPos = pathArcItem.Pos;

        // 最小半径の保持
        if (pathArcItem.Radius.X < minRadius) { minRadius = pathArcItem.Radius.X; }
        if (pathArcItem.Radius.Y < minRadius) { minRadius = pathArcItem.Radius.Y; }
    }

    // マトリクス(変換を指示?)の生成
    System.Drawing.Drawing2D.Matrix matrix = new System.Drawing.Drawing2D.Matrix();

    // 平滑化(直線で細分化)する
    float coeff = (float)0.01;
    if (sValueType == SValueTypes.Smooth)
    {
        coeff = (float)0.001;
    }
    else if (sValueType == SValueTypes.Rough)
    {
        coeff = (float)0.1;
    }
    sPath.Path.Flatten(matrix, (float)(minRadius * coeff));

    // 戻り値の生成
    List<string> ret = new List<string>();

    // 座標群で回る
    PointF startPoint = PointF.Empty;
    PointF lastPoint = PointF.Empty;
    foreach (PointF p in sPath.Path.PathData.Points)
    {
        // 最初の座標の確認
        if (startPoint.IsEmpty || lastPoint.IsEmpty)
        {
            // 保持して抜ける
            startPoint = p;
            lastPoint = p;
            continue;
        }

        // 文字列を組み立てて追加する
        ret.Add("m " + lastPoint.X.ToString() + "," + lastPoint.Y.ToString() + " " + (p.X - lastPoint.X).ToString() + "," + (p.Y - lastPoint.Y).ToString());

        // 保持する
        lastPoint = p;
    }

    // 閉じる確認
    if (pathArcAnal.IsZ == true && startPoint.Equals(lastPoint) == false)
    {
        //-- 閉じる指定があり、実際には閉じていないとき
        // 文字列を組み立てて追加する
        ret.Add("m " + lastPoint.X.ToString() + "," + lastPoint.Y.ToString() + " " + (startPoint.X - lastPoint.X).ToString() + "," + (startPoint.Y - lastPoint.Y).ToString());
    }

    // 返す
    return ret.ToArray();
}


※動作しているコードの該当箇所をそのままコピーしました

2018年1月31日水曜日

C#で、SVGファイルのXMLを操作する

SVGはXMLベースのベクターデータ用フォーマットで業界標準(?)らしい。

自分が必要なのはレーザー加工用の情報が入っている「path」って要素で、円弧などもこちらで定義している。
これの編集と追加をしたかったのだけれど、面倒そうなのでもういっそ文字列操作で済まそうかと思ったが、調べてみたら意外とすっきりXmlパーサで実装できました。変な制限がつかないで済んでえがった。

※A
まず、なんだかよく分かっていないけれど、「http://www.w3.org/2000/svg」文字列をマネージャ経由で渡す必要がある。
そして、XPathというらしいけれど「/a:svg/a:g/a:path」みたいに直接パスを指定してXmlNodeListを取得できる。(「a」は名前空間?)

※B
CloneNode(…)でコピーでき、現在位置に対してInsertBefore(…)やInsertAfter(…)で挿入できる。
現在位置もNodeで普通のリスト要素のように保持できたり。


// ログ出力クラス
private LogUtil logUtil = null;

/// 要素を解析し変換する
public System.Xml.XmlDocument ConvSvg(System.Xml.XmlDocument xmlDoc)
{
    // 引数の確認
    if (xmlDoc is null) { return null; }

    try
    {
        // -- ※A ->
        // 名前空間マネージャの生成
        System.Xml.XmlNamespaceManager nsmgr = new System.Xml.XmlNamespaceManager(xmlDoc.NameTable);
        nsmgr.AddNamespace("a", "http://www.w3.org/2000/svg");

        // 「path」の取得
        System.Xml.XmlNodeList nodeList = xmlDoc.SelectNodes("/a:svg/a:g/a:path", nsmgr);
        // <- ※A --

        // 取得の確認
        if (nodeList is null || nodeList.Count <= 0)
        {
            this.logUtil.WriteLog("パス(path) が取得できませんでした。", LogUtil.LogLevel.Error);
            this.logUtil.WriteLog(" → オブジェクト は予め パス(path) に変換する必要があります", LogUtil.LogLevel.Info);
            this.logUtil.WriteLog(" → /svg/g/path が存在するか確認してください", LogUtil.LogLevel.Info);
            return null;
        }

        // 取得したパスで回る
        foreach (System.Xml.XmlNode node in nodeList)
        {
            // 図形情報(d属性?)の取得
            string dValue = null;
            try
            {
                dValue = node.Attributes["d"].Value;
            }
            catch (Exception)
            {
                dValue = null;
            }
            if (string.IsNullOrEmpty(dValue) == true) { continue; }

            // 図形情報を確認して図形の種類を取得する
            DValueTypes dValueType = this.GetDValueType(dValue);

            // style属性(「;」「:」区切り。strokeが線色?)の取得
            string sValue = null;
            try
            {
                sValue = node.Attributes["style"].Value;
            }
            catch (Exception)
            {
                sValue = null;
            }

            // style属性を確認して処理の種類を取得する
            SValueTypes sValueType = this.GetSValueType(sValue);

            // 線色による分岐
            if (sValueType == SValueTypes.Delete)
            {
                // 削除する
                node.ParentNode.RemoveChild(node);
                continue;
            }

            // 図形種類による分岐
            string[] dValues = null;
            if (dValueType == DValueTypes.LineRelative || dValueType == DValueTypes.LineAbsolute)
            {
                dValues = this.ConvLine(dValue, dValueType, sValueType);
            }
            else if (dValueType == DValueTypes.ArcRelative || dValueType == DValueTypes.ArcAbsolute)
            {
                dValues = this.ConvArc(dValue, dValueType, sValueType);
            }
            else
            {
                this.logUtil.WriteLog("図形の種類の特定に失敗しました。", LogUtil.LogLevel.Error);
                this.logUtil.WriteLog(" → 現バージョンでは「円弧(a)」「移動(m)で定義の直線」のみ対応しています。(「直線(l)で定義の直線」は非対応)", LogUtil.LogLevel.Info);
                this.logUtil.WriteLog(" → " + dValue, LogUtil.LogLevel.Info);
                continue;
            }

            // 処理結果の確認
            if (dValues is null || dValues.Length <= 0) { continue; }

            // -- ※B ->
            // 変換結果で回る
            System.Xml.XmlNode lastNode = node;
            for (int i = 0; i < dValues.Length; i++)
            {
                if (i == 0)
                {
                    //-- 1件目の場合
                    // 置き換える
                    node.Attributes["d"].Value = dValues[i];
                    continue;
                }

                //-- 以下、2件目以降の場合

                // コピーして置き換える
                System.Xml.XmlNode newNode = node.CloneNode(true);
                newNode.Attributes["id"].Value += "_" + (i + 1).ToString().PadLeft(3, '0');
                newNode.Attributes["d"].Value = dValues[i];
                node.ParentNode.InsertBefore(newNode, lastNode);

                // 現在の位置を保存する
                lastNode = newNode;
            }
            // <- ※B --
        }
    }
    catch (Exception ex)
    {
        // ログ出力
        this.logUtil.WriteLog(LogUtil.LogLevel.Delimiter);
        this.logUtil.WriteLog(ex.ToString(), LogUtil.LogLevel.Fatal);
        this.logUtil.WriteLog(LogUtil.LogLevel.Delimiter);
        this.logUtil.WriteLog("変換処理で例外が発生しました。", LogUtil.LogLevel.Fatal);

        // 戻り値のクリア
        xmlDoc = null;
    }

    // 返す
    return xmlDoc;
}


※動作しているコードの該当箇所をそのままコピーしました