RRadout_01.py

Python Source icon RRadout_01.py — Python Source, 18 KB (19150 bytes)

File contents

### Revit to Radiance scene file export module
### 2016 Georg Mischler
### 
### Version 0.1 - proof of concept

import sys
import nt as os
import clr
import System
clr.AddReferenceByName("RevitAPI.dll");
clr.AddReferenceByName("RevitAPIUI.dll");

from Autodesk.Revit import *
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Macros import *
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Architecture import TopographySurface
from Autodesk.Revit.UI.Selection import *
from System.Collections.Generic import *
from System.Collections import *
from System import *
from math import *

class ThisApplication (ApplicationEntryPoint):
    ### CONFIGURATION #################################################################
    #
    # Metric or imperial?
    # True  -> Export unit meters
    # False -> Export unit inches
    metric = True
    #
    #
    # Output file normal Geometry
    fname = os.environ['USERPROFILE'] + r'\Desktop\revout.rad'
    #
    # Output file Topography Geometry
    topo_fname = os.environ['USERPROFILE'] + r'\Desktop\revout_topo.rad'
    #
    #
    ### END CONFIGURATION #############################################################

    #region Revit Macros generated code
    def FinishInitialization(self):
        ApplicationEntryPoint.FinishInitialization(self)
        self.InternalStartup()
    def OnShutdown(self):
        self.InternalShutdown()
        ApplicationEntryPoint.OnShutdown(self)
    def InternalStartup(self):
        self.Startup()
    def InternalShutdown(self):
        self.Shutdown()
    #endregion
    def Startup(self): pass
    def Shutdown(self): pass
    
    def ExportTopoToRadiance(self):
        if (self.ActiveUIDocument != None):
            self.__ExportTopoToRadianceImplementation(self.Application, self.ActiveUIDocument.Document)
        else:
            self.__ExportTopoToRadianceImplementation(self.Application, None)    
    def __ExportTopoToRadianceImplementation(self, app, doc):
        context  = _MyTopoExportContext(doc, self.metric, self.topo_fname)
        exporter = CustomExporter(doc, context)
        curview = doc.ActiveView
        exporter.Export(curview)

    def ExportToRadiance(self):
        if (self.ActiveUIDocument != None):
            self.__ExportToRadianceImplementation(self.Application, self.ActiveUIDocument.Document)
        else:
            self.__ExportToRadianceImplementation(self.Application, None)    
    def __ExportToRadianceImplementation(self, app, doc):
        context  = _MyExportContext(doc, self.metric, self.fname)
        exporter = CustomExporter(doc, context)
        curview = doc.ActiveView
        exporter.Export(curview)

    # Transaction mode
    def GetTransactionMode(self):
        return Attributes.TransactionMode.Manual
    # Addin Id
    def GetAddInId(self):
        return '98E9898F-B62F-415A-9AA3-1F705B44EAC0'


class _RadianceBaseExportContext(IExportContext):
    def _make_polygon(self, name, pts):
        sl = ['{name} polygon {name}_{n:06d}'.format(name=name, n=self.cur_n)]
        self.cur_n += 1
        sl.append('0 0 {num}'.format(num=len(pts)*3))
        for pt in pts:
            sl.append('   {pt[0]:.8f} {pt[1]:.8f} {pt[2]:.8f}'.format(pt=pt))
        sl.append('')
        return '\n'.join(sl)

    def _make_ring(self, name, cen, norm, r0, r1):
        s = '''{name} ring {name}_{n:06d}
0 0 8
  {cen[0]:.8f} {cen[1]:.8f} {cen[2]:.8f}
  {norm[0]:.8f} {norm[1]:.8f} {norm[2]:.8f}
  {r0:.8f} {r1:.8f}
'''.format(name=name, n=self.cur_n, cen=cen, norm=norm, r0=r0, r1=r1)
        self.cur_n += 1
        return s

    def _make_cylinder(self, name, start, end, rad):
        s = '''{name} cylinder {name}_{n:06d}
0 0 7
  {start[0]:.8f} {start[1]:.8f} {start[2]:.8f}
  {end[0]:.8f} {end[1]:.8f} {end[2]:.8f}
  {rad:.8f}
'''.format(name=name, n=self.cur_n, start=start, end=end, rad=rad)
        self.cur_n += 1
        return s

    def _norm_name(self, s):
        return s.replace(' ', '_')


class _MyTopoExportContext(_RadianceBaseExportContext):
    def __init__(self, doc, metric, fname):
        self.cur_n = 0
        self.cur_mat = ''
        self.doc = doc
        self.fname = fname
        self.cancelled = False
        if metric:
            self.xforms = [Transform.Identity.ScaleBasis(0.3048)]
        else:
            self.xforms = [Transform.Identity]
    def Finish(self):
        self.f.write('\n# End of Revit Topography Mesh Export\n')
        self.f.close()
    def Start(self):
        self.f = open(self.fname, 'w')
        self.f.write('# Radiance Scene File\n')
        self.f.write('# Revit Topography Mesh Export\n')
        self.f.write('# Original File: %s\n\n' % self.doc.PathName)
        return True
    def IsCanceled(self):
        if self.cancelled:
            self.f.write('\n# Export cancelled\n\n')
        return self.cancelled
    def OnDaylightPortal(self, node): pass
    def OnElementBegin(self, id):
        el = self.doc.GetElement(id)
        if isinstance(el, TopographySurface):
            matpar = el.Parameter['Material']
            if matpar and matpar.HasValue:
                matid = matpar.AsElementId()
                matel = self.doc.GetElement(matid)
                if matel:
                    self.cur_mat = matel.Name
                elif el.Category and el.Category.Material: # assume by category
                    self.cur_mat = el.Category.Material.Name
            return RenderNodeAction.Proceed
        return RenderNodeAction.Skip
    def OnElementEnd(self, node):
        self.cur_mat = ''
    def OnFaceBegin(self, node): return RenderNodeAction.Skip
    def OnFaceEnd(self, node): pass
    def OnInstanceBegin(self, node): return RenderNodeAction.Skip
    def OnInstanceEnd(self, node): pass
    def OnLight(self, node): pass
    def OnLinkBegin(self, node): return RenderNodeAction.Skip
    def OnLinkEnd(self, node): pass
    def OnMaterial(self, node): pass
    def OnPolymesh(self, node):
        xf = self.xforms[-1]
        if self.cur_mat:
            name = self._norm_name('mesh_topography+++' + self.cur_mat)
        else:
            name = 'mesh_topography'
        fs = node.GetFacets()
        points = node.GetPoints()
        self.f.write('\n# Polymesh Topography with %d facets\n' % node.NumberOfFacets)
        for facet in fs: # XXX xform!
            pl = (xf.OfPoint(points[facet.V1]),
                xf.OfPoint(points[facet.V2]),
                xf.OfPoint(points[facet.V3]))
            self.f.write(self._make_polygon(name, pl))
    def OnRPC(self, node): pass
    def OnViewBegin(self, node): return RenderNodeAction.Proceed
    def OnViewEnd(self, node): pass




class _MyExportContext(_RadianceBaseExportContext):
    def __init__(self, doc, metric, fname):
        self.fname = fname
        if metric:
            self.xforms = [Transform.Identity.ScaleBasis(0.3048)]
        else:
            self.xforms = [Transform.Identity]
        self.cylhalfs = []
        self.RND = 10
        self.cancelled = False
        self.doc = doc
        self.cur_level = 'No-Level'
        self.cur_family = ['']
        self.cur_type = ''
        self.cur_n = 0
    def Finish(self):
        self.f.write('\n# End of Revit Geometry Export\n')
        self.f.close()
    def Start(self):
        self.f.write('# Radiance Scene File\n')
        self.f.write('# Revit Geometry Export\n')
        self.f.write('# Original File: %s\n\n' % self.doc.PathName)
        self.f = open(self.fname, 'w')
        return True
    def IsCanceled(self):
        if self.cancelled:
            self.f.write('\n# Export cancelled\n\n')
        return self.cancelled
    def OnDaylightPortal(self, node): pass
 
    def _get_element_level(self, el):
        level = self.doc.GetElement(el.LevelId)
        if level:
            return level.Name
        if hasattr(el, 'Host') and isinstance(el.Host, Level):
            return el.Host.Name
        # let's just hope this key is not locale dependent.
        ref_lev = el.Parameter['Reference Level']
        if ref_lev and ref_lev.HasValue:
            return self.doc.GetElement(ref_lev.AsElementId()).Name
        

    def OnElementBegin(self, id):
        try:
            el = self.doc.GetElement(id)
            self.cylhalfs.append({})
            level = self._get_element_level(el)
            if level:
                self.cur_level = level
            else:
                self.cur_level = 'Level-XXX'

            cur_family = ''
            if hasattr(el, 'Symbol'):
                sym = el.Symbol
                if sym and sym.Family:
                    cur_family = sym.Family.Name
            self.cur_family.append(cur_family)
            self.cur_type = el.Name
            self.f.write('\n# %s // %s // %s\n' % (self.cur_level, self.cur_family, el.Name))
            return RenderNodeAction.Proceed
        except:
            self._show_exc()

    def OnElementEnd(self, id): 
        el = self.doc.GetElement(id)
        chl = self.cylhalfs.pop()
        # write the partial cylinders that were not matched
        for hck, (ch, matid) in chl.items():
            self._writeCylinderSegments(ch, matid)
        self.cur_family.pop()
        self.cur_level = 'No-Level'

    def _make_name(self, matid):
        mat = self.doc.GetElement(matid)
        if self.cur_family[-1]:
            l = [self.cur_level, self.cur_family[-1]]
        else:
            l = [self.cur_level, self.cur_type]
        if mat:
            l.append(mat.Name)
        s = '+++'.join(l)
        return self._norm_name(s)

    def _writeMesh(self, mesh, matid):
        mesh = mesh.Transformed[self.xforms[-1]]
        name = self._make_name(matid)
        sl = []
        for i in range(mesh.NumTriangles):
            t = mesh.Triangle[i]
            s = self._make_polygon(name, (t.Vertex[0], t.Vertex[1], t.Vertex[2]))
            sl.append(s)
        self.f.write('\n### Mesh Face Triangles\n')
        self.f.write(''.join(sl))
        self.f.write('\n')

    def _almost_equal(self, x, y):
        return round(x, self.RND) == round(y, self.RND)

    def _loop_to_segments(self, loop):
        segs = []
        for edge in loop:
            c = edge.AsCurve().CreateTransformed(self.xforms[-1])
            cpts = c.Tessellate()
            segs.append(list(cpts))
        for i in range(len(segs)-1):
            # some edges may be backwards
            pl0 = segs[i]
            pl1 = segs[i+1]
            if pl0[-1].IsAlmostEqualTo( pl1[0]):
                continue
            if pl0[-1].IsAlmostEqualTo(pl1[-1]):
                pl1.reverse()
                continue
            if pl0[0].IsAlmostEqualTo(pl1[0]):
                pl0.reverse()
                continue
            if pl0[0].IsAlmostEqualTo(pl1[-1]):
                pl0.reverse()
                pl1.reverse()
                continue
        return segs


    def _write_polygon(self, f, matid):
        name = self._make_name(matid)
        edges = f.EdgeLoops[0]
        loop = edges.ForwardIterator()
        segs = self._loop_to_segments(loop)
        pts = []
        for ptl in segs:
            pts.extend(ptl[:-1])
        if f.EdgeLoops.Size > 1: # holes
            last = pts[-1]
            for i in range(1, f.EdgeLoops.Size):
                loop = f.EdgeLoops[i]
                hpts = []
                hsegs = self._loop_to_segments(loop)
                for hptl in hsegs:
                    hpts.extend(hptl[:-1])
                hpts.append(hpts[0])
                hpts.reverse()
                hpts.append(last)
                pts.extend(hpts)                
        self.f.write(self._make_polygon(name, pts))

    def _write_circle(self, f, matid):
        name = self._make_name(matid)
        xf = self.xforms[-1]
        curve = f.EdgeLoops[0][0].AsCurve().CreateTransformed(self.xforms[-1])
        irad = 0.0
        if f.EdgeLoops.Size == 2:
            curve2 = f.EdgeLoops[1][0].AsCurve().CreateTransformed(self.xforms[-1])
            irad = curve2.Radius
        s = self._make_ring(name, curve.Center, curve.Normal, irad, curve.Radius)
        self.f.write(s)


    def _is_circle(self, f):
        loops = f.EdgeLoops
        if loops.Size not in [1,2]:
            return False
        loop = loops[0]
        if loop.Size != 2:
            return False
        c0 = loop[0].AsCurve()
        c1 = loop[1].AsCurve()
        if not isinstance(c0, Arc) or not isinstance(c1, Arc):
            return False
        if not self._almost_equal(c0.Radius, c1.Radius): 
            return False
        if loops.Size == 2: # inner loop for ring
            loop2 = loops[1]
            if loop2.Size != 2:
                return False
            c02 = loop2[0].AsCurve()
            c12 = loop2[1].AsCurve()
            if not isinstance(c02, Arc) or not isinstance(c12, Arc):
                return False
            if not self._almost_equal(c02.Radius, c12.Radius): 
                return False
            if not c0.Center.IsAlmostEqualTo(c02.Center): 
                return False
        return True

    def _is_HalfCylinder(self,f):
        loop = f.EdgeLoops[0]
        if loop.Size != 4:
            self.f.write('\n# XXX Cylinder with weird number of edges #%d\n' % loop.Size)
            return 'weird'
        curves = []
        lines = []
        for i in range(loop.Size):
            c = loop[i].AsCurve()
            if isinstance(c, Arc):
                curves.append(c)
            elif isinstance(c, Line):
                lines.append(c)
            else:
                self.f.write('\n# XXX Cylinder with weird edge type #%d: "%s"\n' % (i, str(c.GetType())))
                return 'weird'
        if not len(curves) == len(lines) == 2:
            return False
        c = curves[0]
        if (round(c.GetEndParameter(1) - c.GetEndParameter(0), self.RND) * 2
                == round(c.Period, self.RND)):
            return True
        return False


    def _writeCylinder(self, cyl, matid):
        name = self._make_name(matid)
        arcs = self._get_cyl_arcs(cyl)
        xf = self.xforms[-1]
        s = self._make_cylinder(name=self._make_name(matid),
            start=xf.OfPoint(arcs[0].Center),
            end=xf.OfPoint(arcs[1].Center),
            rad=xf.OfVector(cyl.Radius[0]).GetLength())
        self.f.write(s)

    def _get_cyl_arcs(self, cyl):
        loop = cyl.EdgeLoops[0]
        c0 = loop[0].AsCurve()
        if isinstance(c0, Arc):
            c1 = loop[2].AsCurve()
        else:
            c0 = loop[1].AsCurve()
            assert(isinstance(c0, Arc))
            c1 = loop[3].AsCurve()
        return c0, c1

    def _get_cyl_segments(self, cyl):
        loop = cyl.EdgeLoops[0]
        segs = self._loop_to_segments(loop)
        c0 = loop[0].AsCurve()
        if isinstance(c0, Arc):
            s0 = segs[0]
            s1 = segs[2]
        else:
            s0 = segs[1]
            s1 = segs[3]
        return s0, s1
        
    def _writeCylinderSegments(self, cyl, matid):
        name = self._make_name(matid)
        segs = self._get_cyl_segments(cyl)
        pts0 = segs[0]
        pts1 = segs[1]
        sl = []
        llen = pts0.Count
        if pts0.Count != pts1.Count:
            llen = min((pts0.Count, pts1.Count))
            self.f.write('# XXX Segment count differs: %d %d\n' % (pts0.Count, pts1.Count))
        for i in range(llen-1):
            s = self._make_polygon(name,
                (pts0[i], pts0[i+1], pts1[llen-i-2], pts1[llen-i-1])                )
            sl.append(s)
            self.cur_n += 1
        self.f.write('\n### Cylinder Segments\n')
        self.f.write(''.join(sl))
        self.f.write('\n')

    def _get_hcsig(self, cyl):
        # unique signature for the location and size of a cylinder
        # we use this to reunite half shells
        # XXX Should we transform them to world first ?!?
        # XXX Since this works on a per-family instance basis,
        # XXX that may not actually be necessary.
        arcs = self._get_cyl_arcs(cyl)
        orig = arcs[0].Center
        end = arcs[1].Center
        rad = cyl.Radius[0].GetLength()
        return (round(rad, self.RND),
            round(orig[0], self.RND),
            round(orig[1], self.RND),
            round(orig[2], self.RND),
            round(end[0], self.RND),
            round(end[1], self.RND),
            round(end[2], self.RND))

    def OnFaceBegin(self, node):
        try:
            f = node.GetFace()
            ref = f.Reference
            matid = f.MaterialElementId
            if isinstance(f, CylindricalFace):
                hcres = self._is_HalfCylinder(f)
                if hcres is True:
                    hck = self._get_hcsig(f)
                    oh = self.cylhalfs[-1].get(hck)
                    if oh:
                        del self.cylhalfs[-1][hck]
                        self._writeCylinder(f, matid)
                    else:
                        self.cylhalfs[-1][hck] = (f, matid)
                elif hcres == 'weird':
                    # unexpected properties for a cylyndrical face
                    self._writeMesh(f.Triangulate(), matid)
                else:
                    self._writeCylinderSegments(f, matid)
            elif isinstance(f, PlanarFace):
                if self._is_circle(f):
                    self._write_circle(f, matid)
                else:
                    self._write_polygon(f, matid)
            else:
                self._writeMesh(f.Triangulate(), matid)
            return RenderNodeAction.Skip
        except:
            self._show_exc()

    def _show_exc(self):
        # Otherwise we'll only see the traceback to where C# called Python
        ei = sys.exc_info()
        sl = [str(ei[1])]
        tb = ei[2]
        while tb:
            s = '%s Line: %d' % (tb.tb_lasti, tb.tb_lineno)
            sl.append(s)
            tb = tb.tb_next
        ss = '\n'.join(sl)
        TaskDialog.Show('Traceback', ss)

    def OnFaceEnd(self, node): pass

    def OnInstanceBegin(self, node):
        self.xforms.append(self.xforms[-1].Multiply(node.GetTransform()))
        return RenderNodeAction.Proceed
    def OnInstanceEnd(self, node):
        self.xforms.pop()

    def OnLight(self, node): pass
    def OnLinkBegin(self, node):
        TaskDialog.Show('OnLinkBegin', str(node))
        return RenderNodeAction.Proceed
    def OnLinkEnd(self, node): pass
    def OnMaterial(self, node): pass
    def OnPolymesh(self, node): pass # seperate function above
    def OnRPC(self, node): pass
    def OnViewBegin(self, node): return RenderNodeAction.Proceed
    def OnViewEnd(self, node): pass

by Andy McNeil last modified Feb 29, 2016 12:18 PM