Revitアドインで計算書作成を効率化する #003

計算書は設計のもとになる重要なものです。

設備設計では計算書の作成は手間がかかるものです。作業のひとつとして建築の部屋情報を取得することも負担です。これまでは建築設計者から各室の面積や天井高など情報をもらったり、建築設計図を見たり、あるいはAutoCADなどでAreaコマンドを使って自分で計測したりしました。建築に変更があると手間がさらにかかり、不整合もありました。

ここではRevitアドインを使って、換気計算書に各室の階、室名称、面積、天井高を自動で取り込みます。Revitの集計表を使った場合との違いも説明します。

換気計算書フォーマットをExcelで用意しておく

あらかじめ計算書を用意しておきます。通常は各社で標準的に持っているものでよいです。ここでは次のようなものを使います。Revitアドインで部屋情報を転記するのは、この黄色い部分です。白の部分は手入力またはExcelの自動計算です。

なお、フォーマットによって部屋情報を入れるところのセルの位置が異なりますので、Revitアドインのコードもそれに合わせて作る必要があります。

Revitモデル

2階建ての簡単な事務所モデルを作りました。1階は次のようなものです。

アドインを実行する

実行すると「Revitから取得した室情報を換気計算書に転記しますか?」という確認が出てから計算書Excelファイルを指定する画面が出ますのでフォーマットのExcelファイルを選びます

実行されるとすぐにモデルから部屋情報が取得されて計算書Excel転記され、完了メッセージが出るのでOKボタンを押すとExcelが起動されて次の計算書が表示されます

手入力の部分を入れると計算書が出来上がります。一人当たりの占有面積はあらかじめ室用途ごとに別ファイルで用意しておいて、それに従ってアドインで自動的に入力することもできるでしょう

ちょっと待て、Revit集計表でもいいんじゃない?

Revit使いの皆さんは集計表で十分でしょとおっしゃるかもしれません。でも部屋の集計表には注意点があります。説明します。

モデルで部屋を割り付けたときに、デフォルトではその高さが適切でないことがあります。デフォルトのままにしておくと左の図の事務室のようになります。部屋を表すところはブルーで天井下で止まっているように見えますが上部の▲マークのところは3500になっています。一方右の図は▲マークをつかんで天井レベルに合わせて手動で下げています。天井は2900ですがこの部屋の数値は微小なところで合致していませんが、まあいいでしょう。

以上のような部屋設定を行ってから部屋集計表を作ってみると次のようになりました。事務室の天井は全て2900で設定していますが上述のように部屋の設定をデフォルトのままにしておくと3500になってしまいます。これが天井高さだと素直に認識してしまうと間違ってしまいます。このようなことから集計表を安易に利用することはリスクがあります

このアドインは天井高さについて独自の工夫をしていますのでこのあと説明します。

コードの説明

 

1つの部屋情報をまとめたRoomInformationクラスを用意しておく

Roomの情報には室名、面積など多種多様なプロパティを持ちますが、それをまとめて保持するためのカスタムなRoomInformationクラスを作ります。これにモデルから取得したデータを入れていきます。換気計算には使わないプロパティもありますが、今後の拡張性のためです

public class RoomInformation
{
	//室名
	public string Name { set; get; }
	//室番号
	public string Number { set; get; }
	//容積
	public double Volume { set; get; }
	//面積
	public double Area { set; get; }
	//室の内法面積
	public double InnerArea { set; get; }
	//要素ID
	public ElementId Id { set; get; }
	//階(レベル)
	public Level Level { set; get; }
	//上記のId
	public ElementId LevelId { set; get; }
	//高さ
	public double Height { set; get; }
	//階の連番情報
	public int LevelSequenceNumber { set; get; }

	public RoomInformation(Room room)
	{
		Name = room.get_Parameter(BuiltInParameter.ROOM_NAME).AsString();//room.Nameでもよいが、その場合には室Numberも加わってしまう
		Name = Name.Replace("\n", "");//念のため改行を削除する
		Number = room.Number;
		Volume = room.Volume;
		Area = room.Area;
		Id = room.Id;
		Level = room.Level;
		LevelId = room.LevelId;
	}

モデルに入っているRoomクラスのインスタンスを全部取得します。

「アクティブなビューの取得」では現在ユーザーが開いているビューを取得します。これはそのあとで必要になります。

「カレントフェーズの取得とフェーズフィルター」ではあとでRoom全部を取得するときに現在のフェーズにあったものだけを選ぶためのフィルターです。

「面積がゼロの部屋を除外するフィルター」は面積がゼロ(あるいはゼロにきわめて近いもの)を除外するものです。Revitは一度作成したRoomを設計者が削除した場合でもモデルの中のデータにはそれが残ってしまいます。この削除Roomの面積はゼロで画面上にも表示されません。もしこのフィルターが無いとそういった面積ゼロのRoomも取得してしまいます。

「上記2つのフィルターをANDでつなぐ」では上記2つのフィルターをANDでつないで1つの合成フィルターを作ります。つまり両方の条件を満たすものだけ抽出します

「上記のフィルターをかけて部屋一覧を取得する」ではこの合成フィルターを使ってRoomを取得します。OfCategory(BuiltInCategory.OST_Rooms)でまずは全Roomを取得して、続けてWherePasses(andFilter)でフィルターをかけます。

//アクティブなビューの取得
var activeViewGraphic = commandData.Application.ActiveUIDocument.ActiveGraphicalView;

//カレントフェーズの取得とフェーズフィルター(現在のフェーズに合ったフィルター)
var phaseProvider = new ParameterValueProvider(new ElementId(BuiltInParameter.ROOM_PHASE));
var currentPhase = activeViewGraphic.get_Parameter(BuiltInParameter.VIEW_PHASE).AsElementId();
var phaseRule = new FilterElementIdRule(phaseProvider, new FilterNumericEquals(), currentPhase);
ElementParameterFilter phaseFilter = new ElementParameterFilter(phaseRule);

//面積がゼロの部屋を除外するフィルター。モデルには以前に作成して削除された部屋も残っている。面積がゼロで。それを除外する
ParameterValueProvider areaProvider = new ParameterValueProvider(new ElementId(BuiltInParameter.ROOM_AREA));
ElementParameterFilter areaFilter = new ElementParameterFilter(new FilterDoubleRule(areaProvider, new FilterNumericGreater(), 0, 0.0001));

//上記2つのフィルターをANDでつなぐ
LogicalAndFilter andFilter = new LogicalAndFilter(phaseFilter, areaFilter);

//上記のフィルターをかけて部屋一覧を取得する。
roomCollection = new FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rooms).WherePasses(andFilter);

全部の室情報を解析してRoomInformationインスタンスにセットしていく

複数の部屋があるのでListにしておく

//RoomInFomation型のリストを新規作成。室の情報を保存するためのリスト
listRooms = new List<RoomInformation>();

部屋が多いと時間がかかるのでプログレバー表示

部屋数が100くらいでは数秒で処理が終わりますが、300室とか500室になると時間がかかりますので、全部で何室あって、そのうちの何室が処理されたかプログレスバーを使った方がベターです。そうしないとフリーズしたのと勘違いされます。プログレスバーを表示しながら実行されるメソッドは以下で指定されているようにProgressBarUI_ContentRenderedです。

//これからプログレスバーを表示して時間がかかる処理を行う
//
//全部の室数の取得
int full = roomCollection.Count();
//プログレスバーの表示と時間がかかる処理の実行
ProgressStatusUI progressBarUI = new ProgressStatusUI();
//プログレスバーの大きさ指定
progressBarUI.Width = 450;
progressBarUI.Height = 180;
//プログレスバーが表示されたら実行する関数を指定する。関数ProgressBarUI_ContentRenderedはこの下のほうに有る。
progressBarUI.ContentRendered += ProgressBarUI_ContentRendered;
//プログレスバー表示を中央に。
var desktop = System.Windows.Forms.Screen.PrimaryScreen.Bounds;//デスクトップサイズ
progressBarUI.Top = Convert.ToInt32(((double)desktop.Height - (double)progressBarUI.Height) / 2.0);
progressBarUI.Left = Convert.ToInt32(((double)desktop.Width - (double)progressBarUI.Width) / 2.0);
progressBarUI.ShowDialog();
//ユーザーがキャンセルをしたかどうかをチェック
if (progressBarUI.checkCancel())
{
				//キャンセルした場合はaddinもキャンセル終了
				return Result.Cancelled;
}
//プログレスバーが進みながら関数ProgressBarUI_ContentRenderedが実行される

モデルから取得したRoom情報を分析

ProgressBarUI_ContentRenderedメソッドは次のとおりです。色々と処理していますが、要するにRoomInformationクラスのプロパティに値をセットするためのものです。

private void ProgressBarUI_ContentRendered(object sender, EventArgs e)
{
	ProgressStatusUI progressBarUI = sender as ProgressStatusUI;

	if (progressBarUI == null)
		throw new Exception("ステータスバー作成のときにエラーが発生しました");

	int numberOfRooms = roomCollection.Count();
	int roomCount = 1;

	//各室に関してループして分析する
	foreach (Room room in roomCollection.Cast<Room>())
	{
        //1室に対して1つのRoomInformationオブジェクトを新規作成
		//これに各プロパティを入れていく。
        RoomInformation roomInfor = new RoomInformation(room);

		//部屋の面積を内法で計算してセットする。容積から高さを算出するため
		roomInfor.InnerArea = CalculateRoomInnerMenseki(room);

		//容積を内法面積で割ることで高さHeight(平均値)を計算
		//容積は常に正しい値を返すので利用する
		if (roomInfor.InnerArea != 0.0)
		{
			roomInfor.Height = roomInfor.Volume / roomInfor.InnerArea;
		}
		else
		{
			roomInfor.Height = 0.0;
		}

        //RoomInformation型のリストに以上の1室分のデータを追加する
        listRooms.Add(roomInfor);


		/////プログレスバーのステータス更新
		int progressPercent = Convert.ToInt32((double)roomCount / (double)numberOfRooms * 100.0);
		progressBarUI.UpdateStatus(string.Format("処理した室数 {0}", roomCount.ToString()) + "/" + numberOfRooms.ToString(), progressPercent);
		if (progressBarUI.ProcessCancelled)
			break;

        roomCount++;
	}
	//室リストを階順に並べ替える。これは地盤面からの高さを意味するElevationの値によって小さい順に並べ替える。
	listRooms = listRooms.OrderBy(x => x.Level.Elevation).ThenBy(x => x.Number).ToList();

	//階番号を意味するLevel Idカラム(LevelId)を単純な数値に置き換える。
	//階名称を意味するLevelカラム(Level.Name)と階番号の関係を調べる
	//辞書型変数の用意
	Dictionary<string, double> levelTable = new Dictionary<string, double>();
	for (int i = 0; i < listRooms.Count; i++) {
		string levelName = listRooms[i].Level.Name;
		if (!levelTable.ContainsKey(levelName))
		{
			levelTable.Add(levelName, listRooms[i].Level.Elevation);//その階のGLからの高さを値として登録
		}
	}
	//階が低いものから高くなる順に並べる。Valueには上記の処理によってElevationの数値が入っているので小さい順に並べ替える
	levelTable.OrderByDescending(x => x.Value);

	//値を単純な整数化する。例えば地下1階は-1、1階は0、2階は1とする
	//1階の整数は記憶しておく
	//この処理のためにlevelTableをList型に変換する
	List<KeyValuePair<string, double>> levelList = levelTable.ToList();
	//地階の数のカウンター
	int underGroundFloorCount = 0;
	for (int i=0;i<levelList.Count-1;i++)
	{
		if(levelList[i].Value < 0.0)
		{
			//建築基準法の定義に準じてi番目のフロアの階高(法律では天井高さだがそれを計算するのが複雑になるため近似的に計算している)の3分の1以上の高さが地下に埋まっている階を地階とする
			if(Math.Abs(levelList[i+1].Value - levelList[i].Value)/3 <= Math.Abs(levelList[i].Value))
			{
				//この場合にはフロアiは地階であるから地階数カウントを増やす
				underGroundFloorCount++;
			}
		}
	}
	//最初のフロアの階を決める(=地階の数)
	int renumberStart = -1 * underGroundFloorCount;
	//整数で振りなおすためのディクショナリ変数
	Dictionary<string, int> newLevelTable = new Dictionary<string, int>();
	foreach (KeyValuePair<string, double> data in levelTable)
	{
		//1階がゼロになるように番号を振りなおしたものを新規で作る
		newLevelTable.Add(data.Key, renumberStart);
		renumberStart++;//階があがるごとに1増やす。1階はゼロになるようになっている
	}

	//データテーブルのlistRoomsのLevelSequenceNumber番号を設定する
	for (int i = 0; i < listRooms.Count; i++)
    {
		listRooms[i].LevelSequenceNumber = newLevelTable[listRooms[i].Level.Name];
	}

	//プログレスバーを閉じる
	progressBarUI.Close();
}

上記の処理の中で次の部分はちょっと珍しい工夫は天井高さの計算です。さきほど説明したようにRevitに部屋の高さを聞くと実際とは違う場合があります。そこで容積÷内法面積(じゃないとダメです)によって平均天井高さを計算してそれを天井高さにしています。Revitの容積はいつも正しい値を出してくれるので信頼できます。

面積ならRoomのプロパティから素直に取得すればいいじゃないかと思うかもしれませんが、ユーザー設定によって壁芯で計算する場合もあるので簡単にいきません。内法面積をどのように計算するかポイントです次のCalculateRoomInnerMensekiが独自の内法計算メソッドです

//部屋の面積を内法で計算してセットする。容積から高さを算出するため
roomInfor.InnerArea = CalculateRoomInnerMenseki(room);

//容積を内法面積で割ることで高さHeight(平均値)を計算
//容積は常に正しい値を返すので利用する
if (roomInfor.InnerArea != 0.0)
{
	roomInfor.Height = roomInfor.Volume / roomInfor.InnerArea;
}

内法面積計算の実際

内法面積計算はCalculateRoomInnerMenseki(room)です。結構複雑なのですが、行っていることは、部屋を構成している壁を全部取得して、その開始点の座標、終了点の座標を取得、それらの点を部屋を上から見たときに反時計回りに追いかけて行って、各2点ごとにベクトルの外積を計算して全部足し算すると部屋の面積になります。

図解

面積計算にベクトルの外積を利用します。下図のピンクの面積Sはベクトルa、bの外積で求められます。S/2は三角形abcの面積です。axbで計算したときにはSはZ軸の正の方向を向きます。しかしbxaの外積のとき、大きさは同じSですがZ軸の負の方向を向きます。このプラス、マイナスの面積を利用します。

下のように青い部屋があるとします。どこか任意の点を0とします。Revitの基準点でOKです。点1へのベクトルと点2へのベクトルの外積を計算して半分にすると三角形012の面積が得られます。これは①+㋐です。方向は+Z方向です。同様に2,3の面積②+㋑が得られます。こうして点5.6までいくと、面積の合計は①+②+③+④+⑤+㋐+㋑+㋒+㋓+㋔になります。

最後に6と1の外積を計算すると061の三角形の面積ー(㋐+㋑+㋒+㋓+㋔)になります。5までの外積は相手のベクトルが左側にありましたが、6の相手のベクトル1は右側にあるため結果はマイナスになるのです。

つまり部屋を左回りに一周して計算すると
①+②+③+④+⑤+㋐+㋑+㋒+㋓+㋔ー(㋐+㋑+㋒+㋓+㋔)
=①+②+③+④+⑤
となって部屋の面積が計算できます。

計算順序は必ず左周りです。Revitは部屋の壁を左回りに保持しているようなので、面積内部計算もおそらくこの原理を使っていると思います。

面積の求め方は意外に奥が深く、ご興味ある方は書籍:「面積」とは何か(小山拓輝著、技術評論社)がおすすめです。

具体的なコードは次のとおりです。こまめにコメント書いたのでわかると思います。

		private double CalculateRoomInnerMenseki(Room room)
		{
			//この関数から返す値。内法面積
			double result = 0.0;
			//室面積がゼロのものは、一旦削除された室なので、無視する。リターンする
			if (room.Area < 0.0001) return result;

			//次のバウンダリを取得するためのオプションを作る
			SpatialElementBoundaryOptions opts = new SpatialElementBoundaryOptions();
			//バウンダリの要素、つまりほとんど壁だと想定しているもの。(壁ではないものも可能性としてある)
			IList<IList<BoundarySegment>> loops = room.GetBoundarySegments(opts);
			
			//バウンダリ(面積を構成する複数の境界線)を分析する
			//一室分の壁の座標を保存するための配列, 一応100個用意する
			double[,] points = new double[100, 100];

			foreach (IList<BoundarySegment> loop in loops)
			{
				//座標をいくつ使ったかのインデックス。初期化
				int pointIndex = 0;
				//endPointを一時的に記憶するための変数。初期化
				XYZ lastEndPoint = null;

				//一つのループについて詳しく分析する
				foreach (BoundarySegment boundarySegment in loop)
				{
					//壁のエレメントID
					ElementId idWall = boundarySegment.ElementId;
					//壁(roomに面する仕上げ面でZ軸はフロアレベルになっている)の線分の開始点と終了点を調べる(これは壁の内法に沿った線分である。
					//本プログラムは直線の壁しか計算できない)もし曲線があると直線で計算してしまうので誤差が出る
					//バウンダリセグメントから接している部分の壁のCurveを取得する
					Curve curve = boundarySegment.GetCurve();
					Arc arc = null;
					try {
						arc = (Arc)curve;
					}catch(Exception e)
                    {
						string dummy = e.Message;
                    }
					XYZ startPoint = curve.GetEndPoint(0);
					XYZ endPoint = curve.GetEndPoint(1);

					//スタートポイントの座標を面積計算のために格納しておく。反時計回りに格納する必要がある。
					points[0, pointIndex] = startPoint.X;
					points[1, pointIndex] = startPoint.Y;
					//endPointは一時的に覚えて置く。最後に使う
					lastEndPoint = endPoint;
					//pointIndexを増やしておく
					pointIndex += 1;

				}//1つのループ(いくつかの壁でかこまれた1つのエリア)の処理の終わり
				 //1つのループごとに、その面積を計算して加減算する。

				 //スタートポイントの座標を面積計算のために格納しておく
				 //データのポイントは反時計回りに格納する必要がある。最後はスタートポイントになっていること。!!
				points[0, pointIndex] = lastEndPoint.X;
				points[1, pointIndex] = lastEndPoint.Y;
				//pointIndexを増やしておく
				pointIndex += 1;

				//roomの内法面積を計算して加算する。例えば室の内部に独立柱があるときは
				//マイナスの面積がtakakukeiMenseki関数から返されるので差し引かれることになる
				result += TakakukeiMenseki(points, pointIndex);

			}//全部のループの処理の終わり

			return result;
		}
    private double TakakukeiMenseki(double[,] points, int n)
    {
        //この面積計算方法は2次元上の多角形の面積を計算する「くつひも公式」を使用している。
        //2つのベクトルを2辺とするひし形の面積はベクトルの外積の大きさになるということに基づいている。
        //pointsは、例えば四角形の頂点A,B,C,Dが半時計周り並ばなければいけない。
        //最後にAの座標に戻るように付け加えて、次のような配列になるようにする
        // [Ax, Bx, Cx, Dx, Ax]
        // [Ay, By, Cy, Dy, Ay]
        //ポイント数nは横軸の要素数で、上記の例だと5になる

        //ポイント数nが3個の場合には円柱など円形を意味していることが考えられる
        //1番目と2番目のポイントは円の左端と右端を意味しているので面積を計算して、マイナスで返す
        //0.25は半径を意味する1/2を2乗しているものを集約した
        if (n == 3)
        {
            return Math.PI * ((points[0, 1] - points[0, 0]) * (points[0, 1] - points[0, 0])) * (-0.25);
        }

        //多角形の面積
        double result = 0.0;
        for (int i = 0; i < n - 1; i++)
        {
            result += points[0, i] * points[1, i + 1] - points[1, i] * points[0, i + 1];
        }
        result *= 0.5;
        //面積を返す
        return result;
    }

あとはExcelの指定の位置にデータを入れていくだけです。

まとめ

AutoCADなど従来のものは”図形”でしかありません。それが壁なのか、設備機器なのか、認識していません。

Revitはデータの塊です。このデータを引き出して活用すれば人間が目で拾っていたデータが非常に効率的に代替えできます。とくに、何度も建築プランが変更されて設備が追従するときには効率が何倍にもなります。

コード全体はこちらをご覧ください

githubはこちら。https://github.com/katsumikawasaki/RoomInfo.git

(ご注意)ここに掲載したコードはひとつの技術的な解法を示すサンプルです。実践的な用途に耐えうる堅牢性や品質等は備えていません。コードの実行結果について当組織は一切責任を負いませんので、参照、利用はご自身の責任でお願いします。

書籍もぜひご覧ください


コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です