Next→
08/04 大規模外部変形への道 〜 第4回 線の操作
今回は、ここまで作成してきたクラスを発展させながら、直線の交点で線分を切断する外部変形を作成したいと思います。
まず、線同士の交点は簡単な方程式の解で求められます。 行列を用いて計算した方がスマートですが、とりあえずここでは傾きaと切片bからの解法で求めます。
class JwLine
: (略)
def intersection( other )
dx1 = @from.x - @to.x
dy1 = @from.y - @to.y
dx2 = other.from.x - other.to.x
dy2 = other.from.y - other.to.y
if dx1 == 0.0
if dx2 == 0.0
point = nil
else
x = @from.x
a2 = dy2 / dx2
b2 = other.to.y - other.to.x * a2
y = a2 * x + b2
point = JwPoint.new( x, y )
end
else
a1 = dy1 / dx1
b1 = @to.y - @to.x * a1
if dx2 == 0.0
x = other.from.x
y = a1 * x + b1
point = JwPoint.new( x, y )
else
a2 = dy2 / dx2
b2 = other.to.y - other.to.x * a2
x = - ( b1 - b2 ) / ( a1 - a2 )
y = a1 * x + b1
point = JwPoint.new( x, y )
end
end
point
end
end
ここで
JwPoint.new(x, y) と記述しましたが、実はこのままではエラーが発生します。 点クラスの書しかメソッドを変更して、文字列と数値の両方を受け取れるようにします。
class JwPoint
: (略)
def initialize( *args )
if args.length > 0
if args[0].is_a? String
params = args[0].split
@x = params[1].to_f
@y = params[2].to_f
elsif args[0].is_a? Float
@x = args[0]
@y = args[1]
end
else
@x = 0.0
@y = 0.0
end
end
: (略)
end
線クラスに追加した交差点は、単なる方程式の解なので、線同士が平行でない限りは交差していなくても、延長線上の交点の座標を返します。 交差する直線を操作するには、直線同士が交差しているかどうかの判定が必要になります。
こちらのアルゴリズムを参考に実装してみました。
class JwLine
: (略)
def intersect?( other )
intersect = ((@from.x - @to.x) * (other.from.y - @from.y) +
(@from.y - @to.y) * (@from.x - other.from.x)) *
((@from.x - @to.x) * (other.to.y - @from.y) +
(@from.y - @to.y) * (@from.x - other.to.x))
if intersect < 0.0
intersect = ((other.from.x - other.to.x) * (@from.y - other.from.y) +
(other.from.y - other.to.y) * (other.from.x - @from.x)) *
((other.from.x - other.to.x) * (@to.y - other.from.y) +
(other.from.y - other.to.y) * (other.from.x - @to.x))
end
( intersect < 0.0 ? true : false )
end
: (略)
end
最後に変更したクラスを利用して外部変形を作成します。 外部変形では、直線同士を交差判定します。 交差が見つかったら、交差した線のコピーを作成します。 コピーの作成には、 .clone メソッドを使用します。 これは ruby の全てのクラスで利用できます。
元の線とコピーの端点のそれぞれを交差した点に変更します。 交差した点への変更もコピーを使用します。 そのまま
line1.from = point; line2.from = pointとすると、line1 の始点座標を変更した場合に line2 の始点座標も変わってしまいます。 これは line1 と line2 が同じ実体を参照してしまうためにおこります。
require 'JwPlotFile.rb'
plot = JwPlotFile.new
plot.read
jwc = open( 'jwc_temp.txt', 'w' )
begin
jwc.print "hd\n"
line_count = plot.lines.length - 1
i = 0
while i <= line_count - 1 do
j = i + 1
while j <= line_count do
if plot.lines[i].intersect? plot.lines[j]
point = plot.lines[i].intersection( plot.lines[j] )
plot.lines.push plot.lines[i].clone
plot.lines.last.from = point
plot.lines[i].to = point.clone
plot.lines.push plot.lines[j].clone
plot.lines.last.from = point.clone
plot.lines[j].to = point.clone
end
j += 1
end
line_count = plot.lines.length - 1
i += 1
end
for line in plot.lines
line.draw jwc
end
ensure
jwc.close
end
あとは、バッチファイルを作成して JWW 側から呼び出して試してみてください。
goto exe
:exe
copy jwc_temp.txt temp.txt > nul
ruby -Ks JwSample3.rb jwc_temp.txt
08/01 大規模外部変形への道 〜 第3回 線を表現する
今回は線を表現するクラスを作成します。 Jw_cad の線を構成する要素は、始点・終点・レイヤ・線色・線種・線幅があります。
ここで、始点・終点の座標には、XY座標を直接持たせるのではなく、JwPoint クラスを利用します。こうすることで、点に対する動作をどこからでも同様に利用することが出来るようになります。 このように階層的にクラスを設計するのがポイントです。
require 'JwPoint.rb'
class JwLine
attr :from, true
attr :to, true
attr :color, true
attr :style, true
attr :thick, true
attr :layer, true
def initialize( *args )
@from = JwPoint.new
@to = JwPoint.new
if args.length > 0
if args[0].is_a? String
params = args[0].split
@from.x = params[0].to_f
@from.y = params[1].to_f
@to.x = params[2].to_f
@to.y = params[3].to_f
end
end
end
def draw( out )
out.printf "ly%s\n", @layer if @layer != nil
out.printf "lc%d\n", @color if @color != nil
out.printf "lt%d\n", @style if @style != nil
out.printf "lw%d\n", @thick if @thick != nil
out.printf "%.10f %.11f %.10f %.11f\n", @from.x, @from.y, @to.x, @to.y
end
def move( offset_x, offset_y )
@from.move( offset_x, offset_y )
@to.move( offset_x, offset_y )
end
end
次に線を扱えるように JwPlotFile クラスを修正します。
require 'JwPoint.rb'
require 'JwLine.rb'
class JwPlotFile
attr :positions, true
attr :points, true
attr :lines, true
def initialize
@points = Array.new
@positions = Hash.new
@lines = Array.new
end
def read
while ARGF.gets
chomp!
case $_
: (中略)
when /^lc([0-9]+)/
line_color = $1.to_i
when /^lt([0-9]+)/
line_style = $1.to_i
when /^\s*[0-9\.\-e]+/
@lines.push JwLine.new( $_ )
@lines.last.color = line_color
@lines.last.style = line_style
@lines.last.thick = width
@lines.last.layer = layer
end
end
end
end
最後に、このクラスを利用して線と点を同時に移動する外部変形を作成します。
require 'JwPlotFile.rb'
plot = JwPlotFile.new
plot.read
jwc = open( 'jwc_temp.txt', 'w' )
begin
jwc.print "hd\n"
for item in plot.points + plot.lines
item.move plot.positions[1].x, plot.positions[1].y
item.draw jwc
end
ensure
jwc.close
end
Ruby の配列クラスでは、+を利用することで簡単に配列同士を連結できます。 また変数の中身が線と点などとクラスが違っていても、同じ名前のメソッドがあれば、同じループ変数で処理できます。
goto exe
:exe
copy jwc_temp.txt temp.txt > nul
ruby -Ks JwSample2.rb jwc_temp.txt
07/31 大規模外部変形への道 〜 第2回 外部変形への第1歩
前回解説したプログラムは、それ単体では全く動作しません(すいません)。 今回は、一応外部変形の骨格を作成していきたいと思います。
まず、前回のソースを JwPoint.rb として保存してください。 (前回掲載したソースには不具合があります。7/30 修正分を御利用ください。)今回はまず、外部変形全体を扱うためのクラスを定義します。
require 'JwPoint.rb'
class JwPlotFile
attr :positions, true
attr :points, true
def initialize
@points = Array.new
@positions = Hash.new
end
def read
while ARGF.gets
chomp!
case $_
when /^pn([0-9]+)/
point_color = $1.to_i
width = 0
when /^lw([0-9]+)/
width = $1.to_i
when /^ly([0-9a-fA-F])/
layer = $1
when /^pt/
@points.push JwPoint.new( $_ )
@points.last.color = point_color
@points.last.diameter = width if width != nil
@points.last.layer = layer
when /^hp([0-9]+)/
@positions[ $1.to_i ] = JwPoint.new( $_ )
end
end
end
end
まず、前回定義した JwPoint クラスを require メソッドで取り込みます。
次に JwPlotFile クラスの初期化で、実点を格納する配列 @points と指示点を格納する連想配列 @positions を定義しておきます。
実際に jwc_temp.txt を読み込む read メソッドでは、1行ずつ読み込みながら、実点を示す pt 〜 が出てきたところで、JwPoint クラスを変数として生成しながら配列に追加していきます。
ここで、JwPoint.new を実行すると、JwPoint クラスで定義した initialize メソッドが実行される点に注意してください。
連想配列の説明に関しては、以前の講座でもしましたが、Jw_cad の指示点は番号が連続でない場合もありますので、配列ではなく連想配列にしました。
以上のコードを JwPlotFile.rb として保存した上で、実際の外部変形呼び出し部分を記述します。
require 'JwPlotFile.rb'
plot = JwPlotFile.new
plot.read
jwc = open( 'jwc_temp.txt', 'w' )
begin
jwc.print "hd\n"
for point in plot.points
point.move plot.positions[1].x, plot.positions[1].y
point.draw jwc
end
ensure
jwc.close
end
このように処理の流れが、配列のループ〜移動〜書出しのように整然と記述できます。 これを JwSample1.rb として、バッチファイルから呼び出しします。
goto exe
:exe
copy jwc_temp.txt temp.txt > nul
ruby -Ks JwSample1.rb temp.txt
ここまでの流れでは、こんなに簡単な処理になんだか複雑なことをするような印象を受けたのではないでしょうか? この手法は、プログラムの複雑さが増すごとに効果が高くなり、また一度書いたコードの再利用が簡単になるので、慣れるに従って開発の手間が省かれていきます。
07/25 大規模外部変形への道 〜 第1回 点を表現する
複雑なプログラムを開発する第一歩は、データや関数の表現をなるべく統一的に扱うことで、読みやすさを向上し、プログラムが煩雑になるのを防ぐことにあります。
Ruby では、
[対象データ].[動作]のように主語〜述語の並びに記述する方法が可能なので、違うタイプのデータに対して、同じ機能は同じ関数名(メソッド)を利用するといった統一的な操作が可能です。
具体的に、線・点・円を一定量移動するプログラムは:
line.move offset_x, offset_y
point.move offset_x, offset_y
circle.move offset_x, offset_y
のように記述できます。
では、まず点のデータを表現します。 Jww の点データはご存知のとおり、座標・レイヤ・点色・点の大きさがあります。 データ表現にはクラスという単位を利用します。
class JwPoint
attr :x, true
attr :y, true
attr :layer, true
attr :color, true
attr :diameter, true
end
このように点のクラスを記述して、その要素を attr(アトリビュート)で定義します。 attr 行の末尾の true は、クラスの外部から要素データを書き換え可能にするかどうかのフラグです。 false にすると読取専用になります。
次に点データに対する動作を記述していきます。 動作の記述には、メソッドを使いますが、class定義の中に記述すると、点データのみに対するメソッドとして定義されますので、冒頭に解説したとおり、同じ名前のメソッドを違うクラスに記述できるようになります。
ここでは、jwc_temp.txt からのデータ設定・移動・書出しの3つの動作を記述します。
class JwPoint
: (中略)
def initialize( *args )
if args.length > 0
if args[0].is_a? String
params = args[0].split
@x = params[1].to_f
@y = params[2].to_f
end
else
@x = 0.0
@y = 0.0
end
end
def draw( out )
out.printf "ly%s\n", @layer if @layer != nil
out.printf "pn%d\n", @color if @color != nil
out.printf "lw%d\n", @diameter if @diameter != nil
out.printf "pt %.10f %.11f\n", @x, @y
end
def move( offset_x, offset_y )
@x += offset_x
@y += offset_y
end
end
ここで、クラス内で attr で定義した変数(メンバ)にアクセスするには、冒頭に @ をつけて記述します。 また後述しますがデータ設定の initialize メソッドは特殊で、クラスを変数として定義するときに暗黙に呼び出されます。
07/25 大規模外部変形への道 〜 序章
外部変形は、実はシンプルなものが汎用性が高く、使用頻度も高いというのが定説です。
しかし、機能としてのシンプルさとプログラムの複雑さは決してイコールではありません。 プログラムが大規模かつ複雑になると例え単独の開発者といえども、そのメインテナンスは容易ではありません。
プログラムの複雑性を改善するには:
1. 読みやすさ
2. 再利用の容易さ
の2点が重要です。 読み易さは、修正・変更の容易さにつながりますし、再利用性の容易さは次の外部変形の開発の時間を大幅に短縮してくれます。
幸いにして、以前外部変形講座でとりあげた Ruby というスクリプト言語は設計も新しく、大規模なプログラミングの複雑性を改善する様々な仕組が実装されています。
今後、不定期ではありますが、Ruby の仕組を利用して外部変形を作成する際のフレームワークを作成していこうと思います。