AutoLISP入門講座

AutoLISP入門講座-2 基礎編

AutoLISPの世界に入る

ここから、AutoLISPプログラムのはじまりです。
まず、AutoLISPの世界をコマンドを使いながらのぞいてみます。

AutoLISPの世界を覗いてみる

AutoCADで、円・直線・矩形を書く。できればいろいろなレイヤーで。

コマンドラインに、

command:
command: (entsel) 

と書き、エンター。
すると、

command:
select object: 

と聞いてきました。(当方の使っているCADが英語版なので英語になっちゃってます。すみません)
何か図形を選択してみる。
すると

command:
Select object: (<Entity name: 7ef7f068>  (11014.2 9982.11 0.0)) 

と返ってきました。ほんのひとつの単語を打ち込んだだけで、AutoCADが
図形を選択:という見慣れた言葉でユーザーに選択を促し、ユーザーが応え、それに対し返事をするという会話ができました。
私が始めてこれをやったときは、けっこう感動しました。
さらにコマンドを加えます。

command:
command: (entsel "図形を選択してくれ:")

とコマンドラインに書いて、図形を選択。すると、

command:
図形を選択してくれ:(<Entity name: 7ef7f068> (11292.2 10028.4 0.0)) 


となりました。
entsel とはユーザーに図形選択を促すコマンドで、エンティティーをセレクトする、という意味でエントセルと読みます。
それに対しAutoCADからの返事は、「選択された図形の図形名称」と「クリックされた点」の2つとセットにしたものです。
このようなセットのことをリストと呼びます。
<Entity name: 7ef7f068>というのが図形名、(11292.2 10028.4 0.0)というのがユーザーがクリックした点の座標です。
オプションとして、選択を促すメッセージを、今回のように変更できます。
「クリックされた点」は、クリックした点そのものであって、スナップ効果は考慮されていません。ですので、ここでの座標は図形上にのっていません。というようなこともヘルプに書かれています。

さらにコマンドを加えて、

command:
command: (car (entsel "図形を選択してくれ:"))


として、図形を選択。すると

command:
図形を選択してくれ:<Entity name: 7ef7f068>


となり、図形名だけが表示されました。

carとは、リストのうち、一番最初の要素だけを抜き取って表示させるコマンドです。なぜcarというのか、よくわかりません。

ですので、(<Entity name: 7ef7f068> (11292.2 10028.4 0.0)) というリストから、最初の要素である<Entity name: 7ef7f068>だけが抜き出されて返ってきたわけです。
ここからがいよいよ面白いところです。さらにコマンドを加えます。

command:
Command: (entget(car(entsel "図形を選択してくれ:")))


として、図形を選択。すると

command:
図形を選択してくれ:((-1 . <Entity name: 7ef7f068>) (0 . "LWPOLYLINE")
(330 . ) (5 . "86") (100 . "AcDbEntity") (67 . 0) (410 .
"Model") (8 . "51_zatsu_A") (370 . 40) (100 . "AcDbPolyline") (90 . 4) (70 . 1)
(43 . 0.0) (38 . 0.0) (39 . 0.0) (10 10247.0 12595.2) (40 . 0.0) (41 . 0.0) (42
. 0.0) (10 13255.4 12595.2) (40 . 0.0) (41 . 0.0) (42 . 0.0) (10 13255.4
10052.1) (40 . 0.0) (41 . 0.0) (42 . 0.0) (10 10247.0 10052.1) (40 . 0.0) (41 .
0.0) (42 . 0.0) (210 0.0 0.0 1.0))


AutoLISPがデータの羅列を返してきました。
これは、さっき適当に描いたRECTANGLEの図形が持つ、DXF情報です。
AutoCADが何をどうやって図形を描いているのか知りませんが、このデータが図形そのものであるとするならば、AutoCADの中味に近い部分にアクセスできたわけで、なんとも面白いことです。
AutoCADユーザーーのおそらく99%の人は、こういったデータに触れることはないと思いますが、もったいないことだと思います。

entgetとは、エンティティーをゲットする、ということで、エントゲットと読みます。
(entget 図形名) という形で使うことにより、図形の内部情報をゲットしてきてくれます。
”図形名”が目的語(引数)になると決められていますので、carを使って、余計な座標情報が入ってこないようにしたわけです。
このように、図形には一つ一つ<Entity name: 7ef7f068>というような図形名が与えられていて、この図形名を使いながら図形を扱います。

このままだと読みづらく、かついちいちクリックするのが面倒くさいので、eData という変数にこの情報を丸ごと代入したのち、改行しながら表示させます。変数ですので名前は何でも良いですが、LINE などのAutoCADが使っている言葉は避けなければいけません。まず変数に代入。

command:
command: (setq eData (entget(car(entsel "図形を選択してくれ:"))))


setqとは、(setq AAA BBB)の形で、AAAにBBBを代入する、というコマンドです。セットキューと読みます。
次に改行表示させるために、次のように書きます

Command:
Command: (mapcar 'print eData)(princ)

すると

Command:
Command: (mapcar 'print eData)(princ)
(-1 . <Entity name: 7ef7f068>)
(0 . "CIRCLE")
(330 . )
(5 . "85")
(100 . "AcDbEntity")
(67 . 0)
(410 . "Model")
(8 . "21_RC")
(370 . 40)
(100 . "AcDbCircle")
(10 7809.79 7674.96 0.0)
(40 . 2547.4)
(210 0.0 0.0 1.0)


というように、一つ一つの要素が改行表示され、分かりやすくなりました。
これは円をクリックしたときのDXF情報です。
ここには、レイヤー、色、線種、線の太さ、中心の座標、半径、など円自身が持つ情報が全て含まれています。
よく見ると、21_RC などのように当方が設定したレイヤーの名称が含まれているのが分かります。
AutoLISPとは、簡単に言えば、このデータをとにかくこねくり回すことなのです。

DXFデータを覗いてみる

いろいろな図形のDXFデータにアクセスしてみます。
次の短いコマンドをコピーし、コマンドラインにペーストします。

(mapcar 'print (entget (car (entsel))))(princ)

いろいろな図形をクリックすると、そのDXFデータが表示されます。

DXFデータ

線分のDXFデータを例に、各項目の意味を書き添えます。
各項目の最初の番号はAutoCADに決められた番号で、例えば、8番であれば、
”後に続く文字が必ずレイヤーを示す”と決められています。

(-1 . )	図形名
(0 . "LINE")				図形の種類
(330 . )*不明*使わないです
(5 . "2E")				ハンドル名
(100 . "AcDbEntity")			オブジェクトの種類、みたいなもの
(67 . 0)				0ならモデルにあることを示し、1ならペーパー空間
(410 . "Model")				図形の存在するレイアウトの名前
(8 . "0")				レイヤー
(62 . 1)				色番号。256もしくは省略がByLayer、0がByBlock
					マイナスならOFF状態
(6 . "Continuous")			線種。省略されていればByLayer
(370 . 9)				線の太さ。100倍された値が表示されるようである。
(100 . "AcDbLine")			オブジェクトの種類、みたいなもの
(10 139.448 118.297 0.0)		開始点の座標
(11 276.378 203.415 0.0)		終了点の座標
(210 0.0 0.0 1.0)			図形が存在する平面の法線ベクトル
	ベクトル0,0,1が0,0,-1だったりすると裏返っていることになり、
	時計・半時計が逆になったりします。楕円・楕円弧では重要になる

もうひとつ、円のDXFデータです。線分と違う部分だけ、コメントを書きます。

(-1 . )
(0 . "CIRCLE")
(330 . )
(5 . "76")
(100 . "AcDbEntity")
(67 . 0)
(410 . "Model")
(8 . "0")
(100 . "AcDbCircle")
(10 217.297 124.702 0.0)			中心の座標
(40 . 48.6309)					半径
(210 0.0 0.0 1.0)

こういうように、線分、ポリライン、円、円弧、楕円、寸法、文字、マルチテキスト、ビューポート、
ブロックなど、全てのオブジェクトがDXFデータを持っています。
とても面白い仕組みだと思いませんか。

データを取り出すには

これらDXFデータから知りたい情報、例えば円の中心の座標を知りたいのであれば、

(setq Data0 (entget (car (entsel))))

でまずデータ全体をData0という変数にいれ

(setq Data1 (assoc 10 Data0)

で(10 217.297 124.702 0.0)というリストを取り出しData1にいれ

(setq Data2 (cdr Data1))

で、(217.297 124.702 0.0) という座標を取り出しData2に入れる。という手順を踏みます。
実際にはこの手順はあまりに頻出するので

(setq ObjectName (car entsel))
(setq Data2 (cdr (assoc 10  (entget ObjectName))))

と2段階ぐらいにして書いたり、それも面倒くさければ、

(setq ObjectName (car entsel))
(setq Data2 (ObjDxf 10  ObjectName))

というように、ObjDxf という自分の関数を作っておき、文字入力の数を減らしたりすることもできます。

練習プログラム その1

プログラムの一連の流れを大まかに把握するために、単純なプログラムを、何回かに分けて作成します。
プログラムの中味は、次のような3部構成になることがほとんどです。すなわち

(defun c:Program_Name( / aa bb cc)

	ユーザーのCAD設定を記憶するパート

	*************************************************

	1 ユーザーに図形を選択させたり、数値を入力させたりする、情報収集パート

	2 AutoLISPが計算する、計算パート

	3 AutoLISPに何かを描かせたり、変更させる、作図パート

	*************************************************

	ユーザーのCAD設定を元に戻すパート
)

ユーザーのCAD設定を記憶したり、元に戻したりというのは、すでに準備編で書きました。
いわば儀式のようなものであり、一旦、作ってしまえば、そんなに変えることはありません。
誰が作っても内容も似たようなものになると思います。
情報収集 → 計算 → 作図
という流れを念頭において、次のようなプログラムを作ります。

【仕様】 線分をガサっと選択すると、その線分の中点に、半径100の円を作図する。
最初にプログラム例を掲載します。次に、説明をしていきます。
目で追うだけで、なんとなく雰囲気が分かるように、ベタベタな書きかたをします。
保存したフォルダは必ずAutoCADのオプションでパスを通してください。

(defun test( / ObjNameL i  ObjSet Data Pt10 Pt11 MidPt MidPtL item)
	;ユーザーのCAD設定を記憶するパート(毎回同じ)*********************
	(load "A_Utility")
	(setq *error* *myerror*)
	(A_start)
	;ユーザーが図形を選択するパート**********************
	(while (= ObjSet nil)
		(setq ObjSet (ssget '((0 . "LINE"))))
	)
	;選択した図形セットから図形名のリストを作る
	(setq 	i 0)
	(setq m (sslength ObjSet))
	(repeat m
		(setq ObjNameL (cons  (ssname ObjSet i) ObjNameL))
		(setq i (1+ i))
	)
	(princ "n ObjNameL :  ")(princ ObjNameL)
	;端点の座標を取り出し、中点を計算するパート**********************
	(foreach item ObjNameL
		(setq Data (entget item))
		;一方の点の座標
		(setq Pt10 (cdr (assoc 10 Data)))
		(princ "n Pt10 :  ")(princ Pt10)

		;もう一方の点の座標
		(setq Pt11 (cdr (assoc 11 Data)))
		(princ "n Pt11 :  ")(princ Pt11)

		;中点を求める
		(setq MidPt (list
					(* 0.5 (+ (car Pt10)(car Pt11)))
					(* 0.5 (+ (cadr Pt10)(cadr Pt11)))
				)
		)
		(princ "n MidPt :  ")(princ MidPt)
		;結果の座標をMidPtLというひとつのリストにどんどんつなげていく
		(setq MidPtL (cons  MidPt MidPtL))
		(princ "n MidPtL :  ")(princ MidPtL)
	)
	;結果をもとに作図するパート**********************
	(setvar "OSMODE" 0)		;Oスナップを解除しておく
	(foreach item MidPtL
		(command "._circle" item 10)
	)
	;ユーザーのCAD設定を復元するパート(毎回同じ)*********************
	(A_end)
	(setq *error* nil)
	(princ)
)
;ロードしたら実行されるようにしておく
(test)

上のプログラムの中に説明を書き加えます。とても簡単なプログラムですが、選択→計算→作図と全部揃っているので、流れがよくわかると思います。

(defun test( / ObjNameL i  ObjSet Data Pt10 Pt11 MidPt MidPtL item)
	(load "A_Utility")

A_Utilityというのは、下に出てくる(A_start)と(A_end)という関数を書き込んだファイルです。あとで、(A_start)と(A_end)を呼び出すので、呼び出す前にロードする必要があります。
ょっと特殊な記述です。こうしておくと、エラーでプログラムが終わった場合にも、A_Utilityにある、*myerror*というルーチンを読ませることができます。
私もよくわからずに、誰かの書いたサンプルのまねをしました。

	(A_start)

(A_start)というのは、現在のOスナップ設定や、現在画層を一時的に変数に格納しておくルーチンでA_Utilityに書きました。

ユーザーが図形を選択するパート

ssgetというコマンドは、ガバっと図形を選択させるコマンドです。
‘((0 . “LINE”)))という記述を加えることにより、LINEしか選択されないようにフィルターをかけることができるという、すごいコマンドですssget によって得られるものは、選択セットと呼ばれる、いくつかの図形の集まりです。
変数ObjSetには、”選択セットの名前”しか入っておらず、中味が何なのかを知るには、別のコマンドが必要です。
また、ここでは、ひとつでも図形を選択しないと先に進まないように、(while (= ObjSet nil)で囲んでいます。ObjSet がnilの間は、いつまでもグルグル回れ、ということです。

(while (= ObjSet nil)
		(setq ObjSet (ssget '((0 . "LINE"))))
	)

選択した図形セットから図形名のリストを作るめでたく図形が選択されたら、その選択セットに含まれる図形名を取り出して、ObjNameLというリストの中に格納します。
カウンターの初期値を0にします。

	(setq 	i 0)

sslength で図形セットの中に何個の図形があるかを調べmに格納します。

	(setq m (sslength ObjSet))

m回繰り返します

	(repeat m

ssname はi番目の図形の図形名を取り出す関数です。
consというのは新しい要素を古いリストの頭にくっつける、という関数。
ここはちょっと分かりにくいのですが、ObjNameL という関数の頭に、どんどん図形名をくっつけていっています。

	(setq ObjNameL (cons  (ssname ObjSet i) ObjNameL))

カウンターをプラス1します。

		(setq i (1+ i))
	)

出来上がった図形名のリストをコマンドラインに打ち出して、目で確認します。
ここで列挙される図形名たちが、これから計算の対象となる図形たちです。

	(princ "n ObjNameL :  ")(princ ObjNameL)

端点の座標を取り出し、中点を計算するパート

(foreach で、ObjNameLの中の要素のそれぞれに対し、何かの操作を行います。
repeatと似たようなものですが、カウンター代わりの変数が不要なので、ちょっと楽です。
item という変数が、ObjNameLというリストの中を要素に順番に成り代わってくれます。

	(foreach item ObjNameL

engetで要素のDXF情報を取り出し、Dataに保存します

		(setq Data (entget item))

一方の点の座標
直線の端点は、一方がDXFの10番、もう一方が11番と決まっているのでまず、10番の方を取り出します。
assocでひっかけて、cdrで頭を飛ばし、座標だけの状態にしてからPt10に代入。

		(setq Pt10 (cdr (assoc 10 Data)))

一応、画面で結果確認。

		(princ "n Pt10 :  ")(princ Pt10)

もう一方の点の座標11番の方を取り出します。
assocでひっかけて、cdrで頭を飛ばし、座標だけの状態にしてからPt11に代入。

		(setq Pt11 (cdr (assoc 11 Data)))

一応、画面で結果確認。

		(princ "n Pt11 :  ")(princ Pt11)

中点を求める

2点間の中点とは、
(X座標を足して2で割ったもの, Y座標を足して2で割ったもの)
なので、その通りに式で書いて、MidPtに代入。

	(setq MidPt (list
			(* 0.5 (+ (car Pt10)(car Pt11)))
			(* 0.5 (+ (cadr Pt10)(cadr Pt11)))
		)
	)

一応、画面で結果確認。

	(princ "n MidPt :  ")(princ MidPt)

結果の座標をMidPtLというひとつのリストにどんどんつなげていく
さて、ここで、中点の座標がすでに分かったので、直接円を作図してもよいのですが、ここは一旦、全ての結果をひとつのリストにまとめるという手順をかませたいと思います。
先ほどと同じように、consコマンドで、次々に数珠繋ぎにして中点リストを作ります。

		(setq MidPtL (cons	MidPt MidPtL))

一応、画面で結果確認。

	(princ "n MidPtL :  ")(princ MidPtL)
	)

結果をもとに作図するパート

commandを使って作図する場合は、Oスナップのモードをゼロにしておかないとあらぬところに作図したりしてしまいます。

	(setvar "OSMODE" 0)		;Oスナップを解除しておく

再びforeach関数で、中点リストのそれぞれの要素を中心とした、半径10の円を書きます。

	(foreach item MidPtL
		(command "._circle" item 10)
	)

これで作図が完了しました。

ユーザーのCAD設定を復元するパート(毎回同じ)

	(A_end)
	(setq *error* nil)
	(princ)
)
;ロードしたら実行されるようにしておく
(test)

関数

練習プログラムにそって、出てくる関数を順番にみていきます。そのなかで関連する関数を紹介し、なるべくグループで説明します。

defun デファン ( defun AAA ( BB CC / DD EE FF ))

define function (関数を定義する)の略で、defun、というわけです。

(defun AAA ( BB CC / DD EE FF )
 プログラムの中味
 プログラムの中味
 プログラムの中味
)

というように使います。
AAA には好きな関数名を入れます。
特別な決まりがあり、c:AAAというように、c:を付けると、AutoCADのコマンドラインに”AAA”と打ち込んで、直接、プログラムを呼び出すことができます。
AutoLISP関数から一歩飛び出し、AutoCAD関数に格上げされたような感じです。
c:を付けずに AAAのままの場合、AutoLISPの中でしか使えませんので、AutoLISPのファイルの中に
(AAA)
と書いて呼び出すか、コマンドラインにカッコ付きで(AAA)と書いて呼び出す必要があります。もっとも、その前に、ロードといって、ファイル自体をAutoCADに読ませておかなくてはなりません。ロードさせるには、そのLISPファイルをAutoCAD上にドラッグさせればよいです。
BB CC は引数を記述するところ。その関数が引数を必要としないなら、空欄とし、必要とするならその引数を並べて記述します。引数とは、英語で言う目的語のようなものです。

(defun test (bb cc / dd)
	(setq dd (* bb cc))
	dd
)

この例は、aa bbという2つの数値を与えれば、それらを掛け算して、ccという数値を返すコマンドです。
使うときには、
(test 10 20)
というように使います。
引数の数が足りなくて、
(test 10)
などとしてしまうと、引数が足りません!と怒られ、エラーとなります。
また、c:AAAのように、AutoCAD関数に格上げを行った場合は、この引数はなしでなければなりません。
そういえば、trimや、copyなど、AutoCADの関数で引数を必要とする関数はひとつもありませんね。
AutoLISPを自作していると、何度も使う関数がどんどん増えてきます。
例えば、

  1. 直線と点の距離を求める関数
  2. 線分と線分の交点を求める関数
  3. 点 点 レイヤー で線分を作成する関数
  4. 文字列をぶつ切りにしてリストにする関数
  5. 逆にリストをつなげて文字列にする関数

などなどです。
DD EE FFの部分は、その関数の中でだけ使う、使い捨て変数を列記します。
ここに書かれた使い捨て変数は、関数の終了と同時に、すべてnilにリセットされます。
nilというのは、何にも無し。0ですらない、無の状態です。
プログラムを何度も書き直している途中は良いのですが、ほぼ完成した頃には、全ての変数をここに列記します。
こうすることで、同じ変数名を別のプログラムで使用したときの不具合を防止します。
ここに意図的に記述しない変数は、グローバル変数と呼ばれ、プログラムが終了してもnilにリセットされませんので、別の関数でその変数を再利用することができます。プログラムが増えてくるとグローバル関数も増えてくると思われますが、何がグローバル変数なのか、しっかり覚えておく必要があります。

よくある失敗

使い捨て関数は、私も、プログラムが完成するまで、ほとんど書きません。
それは、ほとんどの関数が

(setq aaa (なんちゃらかんちゃら))

というように、強制的に値を入力されることがほとんどなので、それ以前に何の値が入っていようが、関係ないのです。ところが、ひとつだけ必ず失敗することがあります。

(setq theList (cons newAtom theList))

のように、自分自身に何かを加えていくような場合です。
これだと、どんどん、どんどん変数の中味が長くなって、とんでもないことになります。
なので、cons やappendを使って、自分自身に連結するときは、たとえプログラム作成中であっても、使い捨て変数として宣言してリセットをかけるか、もしくは明示的に
(setq theList nil)
と直前に入れるようにします。

ssget

ssgetはユーザーに図形を選択させる重要コマンドです。フェンス選択などの選択オプションや、選択する図形の種類を限定できるフィルターなど、機能満載で、結構すごいコマンドです。

  1. 特定の種類の図形だけを選択させる
    (ssget ‘((0 . “DIMENSION”))))
  2. 特定の種類の図形だけを選択させる(OR)
    (ssget ‘((-4 . “<OR”)(0 . “ARC“)(0 . “CIRCLE“)(0 . “INSERT“)(0 . “LEADER“)(-4 . “OR>”)))
    もしくは
    (ssget ‘((0 . “ARC,CIRCLE,INSERT,LEADER”))
    だけでも動きます。
  3. 特定の種類の図形だけを選択させる(AND)
    複合図形かつ(66 . 1)(=属性定義)だけを選択する場合
    (ssget ‘((-4 . “<AND”)(0 . “INSERT”)(66 . 1)(-4 . “AND>”)))
  4. フィルターに”*”が使える
    “*line”というようにして、LINE, LWPOLYLINE, POLYLINE, XLINEを選択させることもできます。
    (ssget ‘((0 . “*LINE”)))
  5. 図面全体の中から選択
    存在するビューポートオブジェクトを全て選択させる
    (ssget “x” ‘((0 . “VIEWPORT”)))
  6. あらかじめ点を収納しておき、クロスで選択
    (ssget “_C” Pt1 Pt2 ‘((0 . “LINE”)))

mapcar と lambda

難しいといわれるmapcarとlambdaという関数の使い方です。
いくらググっても、わかりやすく説明している文書が無く、当然、書籍にも記載が無く、私も理解にはすごく苦労しました。
しかし、一旦理解すると、非常に便利な関数です。プログラミングの行数が省けるだけでなく、おそらく処理スピードも、ほんのちょっとあがるのではないか、と思います。

■mapcarとlambdaの効用
mapcarとlambdaは、たとえると、「5個の小麦粉から5個のケーキを連続的に作る」という関数です。
そのケーキの作り方を説明する関数部分がlambdaです。
似たような繰り返し動作をする関数に、foreach や repeat 関数がありますが、これらに比べ、行数がかなり減ることが多いです。


■実際の書き方
実際の関数の形はこうなります。例として、1 5 7 3 8という各数字に5を加える操作です。
(setq MyList (mapcar ‘(lambda(x)(+ x 5))(list 1 5 7 3 8 )))
→(6 10 12 8 13)
これはこのように読むことができます。
(setq ケーキ5セット (mapcar ‘(lambda(x)(ケーキのレシピをxを使って説明))(小麦粉5セット)))

いくつかのポイントがあります。
まず、mapcarとlambdaは5つの具から5つの成果品を作る関数であって、5つの具から1個の成果品を作る関数ではない。つまり材料が 5つあれば、結果も5つです。それがリストにひとまとまりになって返ってきます。なので、1 5 7 3 8を全部加えた数を知りたい、というときはmapcarとlambdaではなく、applyという関数を使います。この点が、混同しやすい点です。
(setq MyList (mapcar ‘(lambda(x)( までは考えずにとにかく書く。
‘(lambdaのように、アポストロフィがついていて、この意味は何だろう、と気になって調べたりするとドツボはまります。
ちなみに式の中で使う変数が小麦粉だけ、というように1種類ならlambda(x)ですが、「小麦粉と卵」というように2つの場合はlambda(x y)と書くだけです。
その後に続くのは、ケーキのレシピ=「実行する関数」ですが、何行にわたってもOK。とにかく、xを使った形で、普通のLISP形式で式を並べればよい。途中にif文があろうが、cond文があろうがかまいません。
最後に、計算の材料となる(list 1 5 7 3 8 )を続けるのですが、そのまえに、括弧がきちんとあっているかが重要です。つまり、計算の材料となる(list 1 5 7 3 8 )を続ける前に、‘(lambdaに対応する括弧を終わらせなくてはならないということです。普通は、LISPエディターや、秀丸などのエディターを使ってプログラムを書くので、いちいち括弧の数を数える人はいないと思いますが、(+ x 5))の最後の括弧は、‘(lambdaの括弧がハイライトされるまで入力させないといけません。mapcarとlambda式のエラーはほとんど、ここの括弧の不一致で起きます。
最後にやっと計算の材料となる(list 1 5 7 3 8 )を書き、あとは、(setq に対する括弧がハイライトされるまで括弧を必要なだけ入れて終わり。

■難しい2重mapcar
L1= (((A (2 3)) (A (3 5))) ((B (1 3)) (B (7 9)) (B (5 1))) ((C (4 7)) (C (4 1))))
というリストがあったとして、その頭の部分だけを取り出して、((A A)(B B B)(C C))というリストを得たいとき、これはかなり頭がごちゃごちゃになるのですが、2重mapcarが出てきます。
まず((A (2 3)) (A (3 5)))というところだけに着目し、ここから(A A)を取り出す式を考え、それを全体にmapcarさせるという手順になります。
x=((A (2 3)) (A (3 5)))から(A A)を取り出すには
(mapcar ‘car x)
これを全体に繰り返すので
(mapcar ‘(lambda(x) (mapcar ‘car x)) L1)
となります。

■さらに難しい2重mapcar
L1=(((1 2) (2 3)) ((3 5) (6 2)) ((3 6) (3 1)))
というような6個の点の座標が、変則的なリストになったものがあったとして、これらの点全部に対し、何かの操作をしたいときはこうなります。
簡単のため、点を中心に円を書く関数を(MyCircle 点)で定義されているものとすると、
(mapcar ‘(lambda (y) (mapcar ‘(lambda(x) (MyCircle x)) y )) L1)
となります。しかし、ここまでくるとなかなか直感では書くことができず、かなり試行錯誤が必要になります。
うまく動いたときは、どこかにメモっておいたほうがいいですね。

quoteの使い方

■ ‘(アポストロフィ)による、quoteの使い方
例として
L1=(1 2 3)
L2=(4 5 6)
のときに、それぞれの要素同士を掛け合わせる関数は
(mapcar ‘(lambda(x y)(* x y)) ‘(1 2 3) ‘(4 5 6))
(mapcar ‘(lambda(x y)(* x y)) L1 L2)
となります。このように、関数部分(* x y)が、”*”という関数とx、yという変数以外に何も無い場合に限って、
(mapcar ‘* L1 L2)
と言うように書き換えることができます。
ほかには
(mapcar ‘(lambda(x)(car x)) L1) は (mapcar ‘car L1)
(mapcar ‘(lambda(x)(print x)) L1) は (mapcar ‘print L1)
lambdaを理解したいときに、’(quote)を使った式とlambdaを使った式を同時に見せられてしまうと、非常に混乱するのですが、私はあくまでlambda(x)(xの式)を最初に理解し、そのあとで特殊な場合にのみ’(quote)を使った式を使える、と理解したほうがわかりやすいと思います。(’(quote)には、「その後に続く文字を評価せずにそのまま引用する」みたいな意味があるようですが、そんな説明を聞いてもさっぱりわからないです。)
特に
(mapcar ‘print List)
は、各要素を、改行しながらコマンドラインに表示してくれるので、ややこしいリストを確認したいときには、とても重宝します。

とにかくパターン化


AutoLISPを何個も作っていくと気づくのですが、図形を選択→プログラム実行のパターンには数種類しかありません。
とにかくパターンをたくさんつくって、あとはそれのコピペをする。それが早道です。
下記が図形選択におけるパターンです。

  1. ssgetやentselでObjを指定し、プログラムを実行して終わるパターン
  2. ssgetやentselでObjectをため込んで、右クリックされたら、プログラムを実行するパターン
  3. Entselで一つ選択されるごとにProcedureを実行し、右クリックされるまで続くパターン
  4. Entselで図形を1個選択した瞬間にプログラムを実行して終わるパターン

;1 ssgetやentselでObjを指定し、プログラムを実行して終わるパターン
(defun c:Test1( / ObjSet ObjNameL i)
	(load "A_Utility")
	(setq *error* *myerror*)
	(A_start)

	(setq ObjSet nil)
	(while (= ObjSet nil)
	(setq ObjSet (ssget '((-4 . ""))))
	)
	;選択セットから図形名のリスト作成
	(setq i -1)
	(repeat (sslength ObjSet)
		(setq ObjNameL (cons  (ssname ObjSet (setq i (1+ i))) ObjNameL))
	)
	(Procedure1)

	(A_end)
	(setq *error* nil)
	(princ)
)

;*****************************************************************
;2 ssgetやentselでため込んで、右クリックされたら、プログラムを実行するパターン
(defun c:Test2( / ObjNameL Flag ObjType)
	(load "A_Utility")
	(setq *error* *myerror*)
	(A_start)

	(while (null Flag)      ;FlagがTになるまで続く
		(setvar "ERRNO" 0)
		(setq ObjName nil ObjType nil)
		(setq ObjName (car (entsel  "n 文字を選択:")))
		(if ObjName (setq ObjType (A_dxf 0 ObjName))(setq Flag nil))
		(cond	((or (= ObjType "TEXT")(= ObjType "MTEXT"))
					(princ ObjType)
					(setq ObjNameL (append ObjNameL (list ObjName)))
				)
				((= (getvar "ERRNO") 52)
					(Procedure1)
					(setq Flag T)
				)
		)
	)

	(A_end)
	(setq *error* nil)
	(princ)
)
;****************************************************************
;3 Entselで一つ選択されるごとにProcedureを実行し、
;右クリックされるまで続くパターン
(defun c:Test3( / Flag ObjName ObjType )
	(load "A_Utility")
	(setq *error* *myerror*)
	(A_start)

	(while (null Flag)      ;FlagがTになるまで続く
		(setvar "ERRNO" 0)
		(setq ObjName nil ObjType nil)
		(setq ObjName (car (entsel  "n 文字を選択:")))
		(if ObjName (setq ObjType (A_dxf 0 ObjName))(setq Flag nil))
		(cond	((= ObjType "TEXT")(Procedure2))
				((= ObjType "MTEXT")(Procedure3))
				((= (getvar "ERRNO") 52)(setq Flag T))
		)
	)

	(A_end)
	(setq *error* nil)
	(princ)
)
;********************************************************************
;4 Entselで図形を1個選択した瞬間にプログラムを実行して終わるパターン
(defun c:Test4()
	(load "A_Utility")
	(setq *error* *myerror*)
	(A_start)

	(setq Flag nil)
	(while (null Flag)      ;FlagがTになるまで続く
		(setq ObjName nil ObjType nil)
		(setq ObjName (car (entsel  "n ポリラインを選択:")))
		(if ObjName (setq ObjType (A_dxf 0 ObjName))(setq Flag nil))
		(if (= ObjType "LWPOLYLINE")(setq Flag T))
	)

	(Procedure4)

	(A_end)
	(setq *error* nil)
	(princ)
)
;*******************
(defun Procedure1()
	(princ "nObjNameL  : ")(princ ObjNameL)
)
;*******************
(defun Procedure2()
	(princ "n Procedure2")
)
;*******************
(defun Procedure3()
	(princ "n Procedure3")
)
;*******************
(defun Procedure4()
	(princ "n Procedure4")
)
(princ)

■空振りに対しての対処
entselで図形を選択するとき、ユーザーが図形選択に失敗し、空振りした場合にもエラーにならず、プログラムが継続するようにすることで、ユーザーが格段に使いやすくなります。
ユーザーは普段は速いスピードで操作をしていることが多く、空振りする事が多いので、そのたびにエラーになっていたのでは、作業効率が落ちてしまいます。
上記テンプレートの3番と4番では、空振りに対する対処をしています。(もっときれいな方法があるかもしれません)

 

コメント

この記事へのコメントはありません。

TOP