RebroやT-fasでモデリングした3DモデルをRevitMEP要素として変換できたら、と今まで何度も考えました。
例えば、IFC(T-fas)→JSON→Revit
「JSONなどで座標情報などを取得し、Revitで生成できないか?」と思い、Revit APIを使ってIFC→MEP変換アドインに挑戦してみました。
目標
- JSONファイルからダクト情報を読み込み
- ダクト直線部分をRevit上に生成
- 端点接続の自動化も試みる(現状未完成)
IFCデータをJSONファイルとして書き出す
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Xbim.Ifc;
using Xbim.Ifc4.Interfaces;
namespace DuctImporter
{
public class IfcToJsonConverter
{
public static void ConvertIfcToJson(string ifcPath, string outputFolder)
{
// フォルダがなければ作成
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
string jsonPath = Path.Combine(outputFolder, "myDucts.json");
using (var model = IfcStore.Open(ifcPath))
{
var segments = model.Instances.OfType<IIfcFlowSegment>();
var segmentList = new List<Dictionary<string, object>>();
foreach (var seg in segments)
{
string segName = seg.Name != null ? seg.Name.ToString() : "(null)";
string segType = seg.GetType().Name;
// デフォルト値
double width = 500.0;
double height = 500.0;
double diameter = 500.0;
string shape = "Rectangular"; // 丸ダクトなら "Round"
double[] start = { 0, 0, 0 };
double[] end = { 1, 0, 0 };
var psets = seg.IsDefinedBy
.Select(r => r.RelatingPropertyDefinition)
.OfType<IIfcPropertySet>();
foreach (var pset in psets)
{
foreach (var prop in pset.HasProperties.OfType<IIfcPropertySingleValue>())
{
string propName = prop.Name != null ? prop.Name.ToString() : "";
string valueStr = prop.NominalValue?.Value?.ToString() ?? "";
if (string.IsNullOrEmpty(valueStr)) continue;
// 幅・高さ
if (propName.Contains("connection_point_1_size"))
{
var arr = valueStr.Replace("W:", "").Replace("H:", "").Split(',');
if (arr.Length >= 2)
{
double.TryParse(arr[0], out width);
double.TryParse(arr[1], out height);
}
}
// 座標
else if (propName.Contains("connecting_point_1"))
{
var arr = valueStr.Split(',');
if (arr.Length >= 3)
{
start[0] = Convert.ToDouble(arr[0]);
start[1] = Convert.ToDouble(arr[1]);
start[2] = Convert.ToDouble(arr[2]);
}
}
else if (propName.Contains("connecting_point_2"))
{
var arr = valueStr.Split(',');
if (arr.Length >= 3)
{
end[0] = Convert.ToDouble(arr[0]);
end[1] = Convert.ToDouble(arr[1]);
end[2] = Convert.ToDouble(arr[2]);
}
}
// 形状
else if (propName.Contains("Shape") || propName.Contains("Round"))
{
// IFC で丸ダクトなら "Round", 角ダクトなら "Rectangular" を設定
shape = valueStr.ToLower().Contains("round") ? "Round" : "Rectangular";
}
// 丸ダクトなら直径も設定
if (shape == "Round" && propName.Contains("Diameter"))
{
double.TryParse(valueStr, out diameter);
}
}
}
var segDict = new Dictionary<string, object>
{
{ "Name", segName },
{ "Type", segType },
{ "Shape", shape },
{ "Width", width },
{ "Height", height },
{ "Diameter", diameter },
{ "Start", start },
{ "End", end }
};
segmentList.Add(segDict);
}
// JSON に書き出し
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(segmentList, Formatting.Indented));
}
Console.WriteLine($"JSON file generated at: {jsonPath}");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("IFC Path:");
string ifcPath = Console.ReadLine();
Console.WriteLine("Output Folder:");
string outputFolder = Console.ReadLine();
IfcToJsonConverter.ConvertIfcToJson(ifcPath, outputFolder);
Console.WriteLine("Done.");
}
}
}
書き出したJSONの一部(文字化けはRevit読み込み時から)
[
{
"Name": "¼ü",
"Type": "IfcFlowSegment",
"Shape": "Rectangular",
"Width": 500.0,
"Height": 500.0,
"Diameter": 500.0,
"Start": [
-72749.963521,
-2214.907934,
2800.0
],
"End": [
-72749.963521,
-2214.907934,
10000.0
]
},
{
"Name": "¼Ç",
"Type": "IfcFlowSegment",
"Shape": "Rectangular",
"Width": 500.0,
"Height": 500.0,
"Diameter": 500.0,
"Start": [
-81297.086139,
2280.908854,
2318.426936
],
"End": [
-81297.086139,
2280.908854,
790.0
]
},
{
"Name": "¼Ç",
"Type": "IfcFlowSegment",
"Shape": "Rectangular",
"Width": 500.0,
"Height": 500.0,
"Diameter": 500.0,
"Start": [
-72785.282844,
564.054683,
500.0
],
"End": [
-75398.276203,
-2444.681048,
500.0
]
JSONをRevitで読み込み、ダクトを生成
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Mechanical;
using Autodesk.Revit.UI;
using Autodesk.Revit.Attributes;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace DuctImporter
{
[Transaction(TransactionMode.Manual)]
public class Command : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
UIDocument uidoc = commandData.Application.ActiveUIDocument;
Document doc = uidoc.Document;
string jsonPath;
using (OpenFileDialog ofd = new OpenFileDialog())
{
ofd.Filter = "JSON Files (*.json)|*.json";
ofd.Title = "Select JSON file to import ducts";
if (ofd.ShowDialog() != DialogResult.OK)
return Result.Cancelled;
jsonPath = ofd.FileName;
}
try
{
DuctJsonImporter importer = new DuctJsonImporter();
importer.ImportDuctsFromJson(doc, jsonPath);
}
catch (Exception ex)
{
message = ex.Message;
return Result.Failed;
}
return Result.Succeeded;
}
}
public class DuctJsonImporter
{
public void ImportDuctsFromJson(Document doc, string jsonPath)
{
string jsonText = File.ReadAllText(jsonPath);
var ducts = JsonConvert.DeserializeObject<List<DuctData>>(jsonText);
List<Duct> createdDucts = new List<Duct>();
using (Transaction tx = new Transaction(doc, "Import and Connect Ducts"))
{
tx.Start();
// 適当な DuctType, MEPSystemType, Level を取得
DuctType ductType = new FilteredElementCollector(doc)
.OfClass(typeof(DuctType))
.Cast<DuctType>()
.FirstOrDefault();
MEPSystemType systemType = new FilteredElementCollector(doc)
.OfClass(typeof(MEPSystemType))
.Cast<MEPSystemType>()
.FirstOrDefault();
Level level = new FilteredElementCollector(doc)
.OfClass(typeof(Level))
.Cast<Level>()
.FirstOrDefault();
if (ductType == null || systemType == null || level == null)
throw new Exception("Revit 内に DuctType, MEPSystemType または Level が見つかりません。");
// ミリ → フィート換算
double mmToFeet = 0.00328084;
// ① 全ダクト生成
foreach (var ductData in ducts)
{
XYZ start = new XYZ(
ductData.Start[0] * mmToFeet,
ductData.Start[1] * mmToFeet,
ductData.Start[2] * mmToFeet
);
XYZ end = new XYZ(
ductData.End[0] * mmToFeet,
ductData.End[1] * mmToFeet,
ductData.End[2] * mmToFeet
);
Duct duct = Duct.Create(doc, systemType.Id, ductType.Id, level.Id, start, end);
if (ductData.Shape == "Round")
{
duct.get_Parameter(BuiltInParameter.RBS_CURVE_DIAMETER_PARAM)
?.Set(UnitUtils.ConvertToInternalUnits(ductData.Diameter, UnitTypeId.Millimeters));
}
else
{
duct.get_Parameter(BuiltInParameter.RBS_CURVE_WIDTH_PARAM)
?.Set(UnitUtils.ConvertToInternalUnits(ductData.Width, UnitTypeId.Millimeters));
duct.get_Parameter(BuiltInParameter.RBS_CURVE_HEIGHT_PARAM)
?.Set(UnitUtils.ConvertToInternalUnits(ductData.Height, UnitTypeId.Millimeters));
}
createdDucts.Add(duct);
}
// ② 自動接続処理
ConnectNearbyDucts(doc, createdDucts);
tx.Commit();
}
}
/// <summary>
/// 端点が近いダクト同士を自動接続
/// </summary>
private void ConnectNearbyDucts(Document doc, List<Duct> ducts)
{
double tolerance = UnitUtils.ConvertToInternalUnits(5.0, UnitTypeId.Millimeters); // 5mm以内で接続
var endPoints = new List<(Duct duct, Connector connector)>();
// ① 各ダクトの端点コネクタだけ取得
foreach (var d in ducts)
{
var connectors = d.ConnectorManager.Connectors.Cast<Connector>();
var curve = ((LocationCurve)d.Location).Curve;
// 端点に最も近いコネクタのみ抽出
foreach (var c in connectors)
{
if (c.Origin.DistanceTo(curve.GetEndPoint(0)) < tolerance ||
c.Origin.DistanceTo(curve.GetEndPoint(1)) < tolerance)
{
endPoints.Add((d, c));
}
}
}
// ② ペア探索&接続
for (int i = 0; i < endPoints.Count; i++)
{
for (int j = i + 1; j < endPoints.Count; j++)
{
var ep1 = endPoints[i];
var ep2 = endPoints[j];
if (ep1.duct.Id == ep2.duct.Id) continue;
// 距離が許容範囲内なら接続
if (ep1.connector.Origin.DistanceTo(ep2.connector.Origin) < tolerance)
{
try
{
if (!ep1.connector.IsConnectedTo(ep2.connector))
{
ep1.connector.ConnectTo(ep2.connector);
}
}
catch { /* 失敗しても無視 */ }
}
}
}
}
}
public class DuctData
{
public string Name { get; set; }
public string Type { get; set; }
public string Shape { get; set; } // Round or Rectangular
public double Width { get; set; } // 角ダクトの幅
public double Height { get; set; } // 角ダクトの高さ
public double Diameter { get; set; } // 丸ダクトの直径
public double[] Start { get; set; }
public double[] End { get; set; }
}
}
端点接続はまだ完全には動作していませんが、試行として「端点距離が近いダクト同士を接続する」ロジックを組み込んでいます。
- 現状では直線距離が近くても接続されない場合がある
- 曲がりダクトや長距離接続の自動化はまだ課題
元のIFCデータ(T-fasモデル)
JSOONを介して生成したMEPモデル

ジョイントが生成されないため、手動で接続する必要があります。これくらいの量であればすぐに完了しますが、ビル一棟分となるとかなりの手間が必要です。
また、IFC要素にダクトサイズ情報が入っていないため、今回は500角統一で生成する設定にしましたが、一本一本サイズとダクト種類を修正しなおす作業も出てきます。
最初にルール決めをしたうえで、IFCからJSONに送る情報をなるべく多く正確に、Revit側のマッピング設定もJSONの情報と一致していないと自動化は難しいと感じました。
実際にやってみて分かったこと
- JSONを使った一括作図は可能
- 直線ダクト生成はうまくいく
- 端点接続や曲がりダクトはまだ難しい
- 作業効率化には一定の効果あり
今後の課題
- 端点接続の安定化
- 曲がりダクト・エルボ自動生成
- 接続失敗時のログ出力
- IFC→JSON変換時のパラメータ保持
- Revit側のマッピング設定
まとめ
今回の挑戦で、JSONからの直線ダクト生成は現実的に可能と分かりました。
ただし、端点接続や曲がり部分の自動化はまだ課題です。
現状では「自動作図の半自動化」として使うのが現実的です。
