#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ TactileBIM → FreeCAD import macro ================================= Imports a TactileBIM JSON export (File → Export → JSON in the web app) and materialises the drawing as FreeCAD Arch / Draft objects. Supported entities: - wall → Arch Wall from a Draft Line with Height and Width - line → Draft Line - rect → Draft Rectangle - circle → Draft Circle - polyline → Draft Wire - door → Arch Door placed on the nearest wall - window → Arch Window placed on the nearest wall Units: TactileBIM exports everything in millimetres. FreeCAD defaults to mm, so no conversion is required. Install ------- 1. Save this file under `~/.FreeCAD/Macro/tactilebim_import.FCMacro`. On Windows: `%APPDATA%/FreeCAD/Macro/`. 2. In FreeCAD: Macro → Macros… → Execute `tactilebim_import`. 3. Pick a `.tbim.json` file in the dialog. Tested against FreeCAD 0.21+ (Arch workbench). """ import json import os from PySide2 import QtWidgets # FreeCAD 0.21 uses PySide2 import FreeCAD import FreeCADGui import Draft import Arch DEFAULT_WALL_HEIGHT_MM = 2700 # overridden by entity.height if set def _mm(v): """Return a FreeCAD Quantity in mm. TactileBIM coords are already mm.""" return float(v) def _vec(p): return FreeCAD.Vector(_mm(p["x"]), _mm(p["y"]), 0) def import_wall(doc, ent): start = _vec(ent["start"]) end = _vec(ent["end"]) line = Draft.makeLine(start, end) line.Label = "TactileBIM_wall_line_{}".format(ent.get("id", "")[:6]) height = _mm(ent.get("height", DEFAULT_WALL_HEIGHT_MM)) width = _mm(ent.get("thickness", 200)) wall = Arch.makeWall(line, width=width, height=height, align="Center") wall.Label = ent.get("name") or "Wall" if "layerName" in ent: wall.Label = "{} ({})".format(wall.Label, ent["layerName"]) return wall def import_line(doc, ent): obj = Draft.makeLine(_vec(ent["start"]), _vec(ent["end"])) obj.Label = "Line" return obj def import_rect(doc, ent): origin = _vec(ent["origin"]) pl = FreeCAD.Placement() pl.Base = origin obj = Draft.makeRectangle(length=_mm(ent["width"]), height=_mm(ent["height"]), placement=pl) obj.Label = "Rect" return obj def import_circle(doc, ent): pl = FreeCAD.Placement() pl.Base = _vec(ent["center"]) obj = Draft.makeCircle(radius=_mm(ent["radius"]), placement=pl) obj.Label = "Circle" return obj def import_polyline(doc, ent): points = [_vec(p) for p in ent["points"]] obj = Draft.makeWire(points, closed=bool(ent.get("closed", False))) obj.Label = "Polyline" return obj def import_door(doc, ent, wall_host=None): # Without a host wall, place a generic Arch Door at the position. if wall_host is None: pl = FreeCAD.Placement() pl.Base = _vec(ent["position"]) door = Arch.makeDoor() door.Placement = pl door.Width = _mm(ent.get("width", 900)) door.Height = _mm(ent.get("height", 2100)) door.Label = "Door" return door return Arch.makeDoor(wall_host, width=_mm(ent.get("width", 900)), height=_mm(ent.get("height", 2100))) def import_window(doc, ent, wall_host=None): if wall_host is None: pl = FreeCAD.Placement() pl.Base = _vec(ent.get("start", {"x": 0, "y": 0})) win = Arch.makeWindow() win.Placement = pl win.Width = _mm(ent.get("width", 1200)) win.Height = _mm(ent.get("height", 1400)) win.Label = "Window" return win return Arch.makeWindow(wall_host, width=_mm(ent.get("width", 1200)), height=_mm(ent.get("height", 1400))) _HANDLERS = { "wall": import_wall, "line": import_line, "rect": import_rect, "circle": import_circle, "polyline": import_polyline, "door": import_door, "window": import_window, } def _pick_file(): win = FreeCADGui.getMainWindow() path, _ = QtWidgets.QFileDialog.getOpenFileName( win, "Select TactileBIM JSON export", "", "TactileBIM (*.tbim.json *.json)", ) return path def run(): path = _pick_file() if not path or not os.path.isfile(path): return with open(path, "r", encoding="utf-8") as fh: data = json.load(fh) entities = data.get("entities", []) if not entities: FreeCAD.Console.PrintWarning("TactileBIM: no entities found in {}\n".format(path)) return doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("TactileBIM Import") created = 0 skipped = 0 for ent in entities: etype = ent.get("type") handler = _HANDLERS.get(etype) if not handler: skipped += 1 continue try: handler(doc, ent) created += 1 except Exception as err: FreeCAD.Console.PrintError( "TactileBIM import: {} failed: {}\n".format(etype, err) ) skipped += 1 doc.recompute() if FreeCADGui.ActiveDocument: FreeCADGui.ActiveDocument.ActiveView.fitAll() FreeCAD.Console.PrintMessage( "TactileBIM: imported {} entities, skipped {} (unsupported types)\n".format(created, skipped) ) if __name__ == "__main__": run()