I highly recommend contacting @Karam_Baki, who has done a fair bit of research on this issue and its implications. He has done a lot of work with freeform/directshape/mesh in Dynamo/Revit and was impacted by these changes quite heavily when they first hit. My understanding is he has worked around them where possible and has also had contact with the Autodesk technical team about it.
Thanks for the tag @GavinCrump
I knew this issue will start affecting many people by time…
How should I start…
Well, basically a decision was made to remove an old branch of code (document.import) and replace it with (ShapeImporter) code…
Benefits & Consequences ? well, better Revit performance, consistent import units… but LIMITED range of importable geometry…
Why ? well, the old branch of code did lots of healing and fixing to the geometry prior imporintg it, but has resulted (sometimes) in an undesirable performance impact.
What is healing ? well, basically, the source of troubles in Revit is it’s small geometrical tolerance, around 0.07mm … when a geometry has a surface that has an edge, of this dimension… and that edge is stitched to another one… it cannot simply import it… (Many other complex situations as well)
So healing is basically detecting those errors and re-creating the surfaces and stitching them on the fly…
Healing has so many techniques… (Mentioned later in the post)
Now after Autodesk removed that old branch of code, we can no longer import the wide range of geometry that we could through the API as before…
*Even if you imported (many) forms in older Revit versions, and opened them in 2022, they will disappear, or behave strangely… etc
I tried a lot to convince Revit team by undo of what they did until they make the new code (healable to imported geometry) and has the ability to import same wide range of geometry… but what’s made has been made… and Autodesk is… well Autodesk…
Anyways, to solve your issue, and hopefully many others.
You may try to use the node called “K-Family Import” from Synthesize toolkit package…
I implemented ALL possible healing methods in Dynamo to help fix the geometry prior importing it to Revit…
I tried your SAT file… and it worked, but there’s a catch…
I CANNO GAURENTEE THAT IT STAYS SOLID
When geometry is healed, (Surfaces are re-created) it is not guaranteed that they actually stitch together back into a proper solid… so it depends…
Autodesk’s old code was able to stitch them back into Solids, but in the cost of making Revit somehow ignoring the issues… which of course caused performance problems.
But I really recommend using it within the package to have your inputs ready.
There’s also in extra folder dyn file called Family.SAT Sync… it uses K-Family Insert in a very user-friendly way… that allows you to do a refreshable imports…etc
Code snippets in the following posts, there is a character limit…
Here are some images during the journey of studying this behavior…
You can clearly see after healing, or “Laundering” the geometry in Dynamo… I can get it inside.
# Created By Karam Baki, karam@aecedx.com
def smartFreeForm(geoms,scaleup,doc,centermode=False,category="Generic Models",importinstance=False,singlefinalprocess=False,safemode=False,temp_path=System.IO.Path.GetTempPath(),bypasscheck=False,returnfailures=False):
class WarnSwallowImport(IFailuresPreprocessor):
def PreprocessFailures(self, failuresAccessor):
failuresAccessor.DeleteAllWarnings()
return FailureProcessingResult.Continue
def finalimport(importinstance,geoms,satopt,singlefinalprocess):
randomname = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(5)])
sat_path = '%s%s.sat' % (temp_path, randomname)
dg.Geometry.ExportToSAT(geoms,sat_path)
if importinstance:
imported = doc.Import(sat_path,satopt,None)
element = doc.GetElement(imported)
element.Pinned = False
result = [element]
else:
result = []
shapeImporter = ShapeImporter()
converted = list(shapeImporter.Convert(doc, sat_path))
if doc.IsFamilyDocument:
for e in converted:
try:
result.append(FreeFormElement.Create(doc,e))
except:
pass
else:
form = DirectShape.CreateElement(doc,doc.Settings.Categories.get_Item(category).Id)
try:
form.SetShape(converted)
except:
doc.Delete(form.Id)
imported = doc.Import(sat_path,satopt,None)
element = doc.GetElement(imported)
element.Pinned = False
form = element
result.append(form)
shapeImporter.Dispose()
System.IO.File.Delete(sat_path)
return result
def allisone(testlist):
for t in testlist:
if t != 1:
return False
return True
def tolist(obj1):
if hasattr(obj1,'__iter__'): return obj1
else: return [obj1]
def flatten(x):
result = []
for el in x:
if hasattr(el, "__iter__") and not isinstance(el, basestring):
result.extend(flatten(el))
else:
result.append(el)
return result
def movecenter(doc,elements):
try:
elements = flatten(elements)
pts = []
for e in elements:
try:
bbox = e.get_BoundingBox(None)
pts.append(bbox.Max.ToPoint())
pts.append(bbox.Min.ToPoint())
except:
pass
Xs = []
Ys = []
Zs = []
for pt in pts:
Xs.append(pt.X)
Ys.append(pt.Y)
Zs.append(pt.Z)
bigbox = dg.BoundingBox.ByCorners(dg.Point.ByCoordinates(max(Xs),max(Ys),max(Zs)),dg.Point.ByCoordinates(min(Xs),min(Ys),min(Zs)))
minp = bigbox.MinPoint
maxp = bigbox.MaxPoint
midpoint = dg.Curve.PointAtParameter(dg.Line.ByStartPointEndPoint(minp,dg.Point.ByCoordinates(maxp.X,maxp.Y,minp.Z)),0.5)
mainvec = dg.Vector.ByTwoPoints(midpoint, dg.Point.Origin())
scaledvec = mainvec.Scale(unitconverter(1,doc,False))
for e in elements:
ElementTransformUtils.MoveElement(doc,e.Id,scaledvec.ToXyz())
return mainvec
except:
return None
def unitconverter(value,doc,frominternal=True):
if int(DocumentManager.Instance.CurrentUIApplication.Application.VersionNumber) < 2021:
if frominternal:
value = UnitUtils.ConvertFromInternalUnits(value,doc.GetUnits().GetFormatOptions(UnitType.UT_Length).DisplayUnits)
else:
value = UnitUtils.ConvertToInternalUnits(value,doc.GetUnits().GetFormatOptions(UnitType.UT_Length).DisplayUnits)
else:
if frominternal:
value = UnitUtils.ConvertFromInternalUnits(value,doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId())
else:
value = UnitUtils.ConvertToInternalUnits(value,doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId())
return value
def smartrevitsolid(surfs,doc,importinstance,safemode,geometrymode,disposeinit,temp_path):
def polysurfaceOP(surfaces,chunks=12):
def smartpolysurface(surfaces,times,round=0):
if len(surfaces) > chunks:
if times == 0:
return surfaces
result = []
chopped = [surfaces[x:x+chunks] for x in xrange(0, len(surfaces), chunks)]
for chop in chopped:
try:
joined = dg.PolySurface.ByJoinedSurfaces(chop)
#if round != 0:
# for c in chop:
# c.Dispose()
result.append(joined)
except:
for c in chop:
result.append(c)
times -= 1
round += 1
result = smartpolysurface(result,times,round)
return result
else:
if isinstance(surfaces, dg.PolySurface):
return surfaces
else:
return dg.PolySurface.ByJoinedSurfaces(surfaces)
surfaces = flatten(tolist(surfaces))
n = len(surfaces)
times = 0
while n > 1:
n //= chunks
times += 1
times *= 1.5
times = int(round(times))
joined = smartpolysurface(surfaces,times)
if isinstance(joined, list):
try:
joined = dg.PolySurface.ByJoinedSurfaces(joined)
except:
joined = joined
return joined
def isActuallygood(geomorsat,converted,firstTime,geometrymode,bypasscheck):
if firstTime:
if bypasscheck == False:
originalfaces = []
if isinstance(geomorsat,dg.Solid) or isinstance(geomorsat,dg.PolySurface):
exploded = dg.Geometry.Explode(geomorsat)
for exp in exploded:
originalfaces.append(exp)
if exp.Closed:
originalfaces.append(exp)
elif isinstance(geomorsat,dg.Surface):
if geometrymode == False:
originalfaces.append(geomorsat)
if geomorsat.Closed:
originalfaces.append(geomorsat)
else:
exploded = dg.Geometry.Explode(geomorsat)
for exp in exploded:
originalfaces.append(exp)
if exp.Closed:
originalfaces.append(exp)
elif isinstance(geomorsat,str):
geomsorsat = dg.Geometry.ImportFromSAT(geomorsat)
for geomorsat in geomsorsat:
if isinstance(geomorsat,dg.Solid) or isinstance(geomorsat,dg.PolySurface):
exploded = dg.Geometry.Explode(geomorsat)
for exp in exploded:
originalfaces.append(exp)
if exp.Closed:
originalfaces.append(exp)
if geometrymode == False and doc.IsFamilyDocument:
geomorsat.Dispose()
elif isinstance(geomorsat,dg.Surface):
if geometrymode == False:
originalfaces.append(geomorsat)
if geomorsat.Closed:
originalfaces.append(geomorsat)
else:
exploded = dg.Geometry.Explode(geomorsat)
for exp in exploded:
originalfaces.append(exp)
if exp.Closed:
originalfaces.append(exp)
if geometrymode:
geomorsat = list(geomsorsat)
originalnum = len(originalfaces)
for org in originalfaces:
org.Dispose()
FFfaces = []
for e in converted:
getfaces = e.Faces
for gotten in getfaces:
FFfaces.append(gotten)
FFfacesnum = len(FFfaces)
if FFfacesnum == originalnum:
samefaces = True
else:
samefaces = False
else:
samefaces = True
freeforms = []
if doc.IsFamilyDocument == False and singlefinalprocess and geometrymode == False and samefaces and importinstance == False:
form = DirectShape.CreateElement(doc,doc.Settings.Categories.get_Item(category).Id)
try:
form.SetShape(converted)
freeforms.append(form)
except:
doc.Delete(form.Id)
satopt = SATImportOptions()
freeforms.append(finalimport(True,geomsorsat,satopt,True)[0])
satopt.Dispose()
if geometrymode == False and samefaces and importinstance == False:
for e in converted:
if doc.IsFamilyDocument:
form = FreeFormElement.Create(doc,e)
freeforms.append(form)
elif singlefinalprocess == False:
form = DirectShape.CreateElement(doc,doc.Settings.Categories.get_Item(category).Id)
try:
form.SetShape([e])
freeforms.append(form)
except:
doc.Delete(form.Id)
else:
samefaces = False
if geometrymode:
if isinstance(geomorsat, list):
freeforms = geomorsat
else:
freeforms = [geomorsat]
if len(freeforms) == 0:
return [False]
if firstTime == False:
return freeforms
elif samefaces and firstTime:
return freeforms
else:
return [False]
def isbellowarea(surface):
if round(meternumber(surface.Area),2) < meternumber(0.0025):
return True
else:
return False
def isbellowlength(surface):
edges = surface.Edges
for e in edges:
curvegeom = e.CurveGeometry
length = curvegeom.Length
curvegeom.Dispose()
if length <= 0.0007803686370625:
return True
return False
def meternumber(value):
value = value
return value
def metersurfarea(surface):
area = meternumber(surface.Area)
return area
def almostsame(surf1,surf2,tollerance = 2):
if round(metersurfarea(surf1),tollerance) == round(metersurfarea(surf2),tollerance):
return True
else:
return False
def quickSATcheck(sat_path,customscale=[1,1,1],importinstance=False,firstTime=True,geometrymode=False,bypasscheck=False):
if allisone(customscale) == False:
return [False]
converted = []
shapeImporter = ShapeImporter()
converted = list(shapeImporter.Convert(doc, sat_path))
shapeImporter.Dispose()
if bypasscheck:
cleanconverted = []
for e in converted:
if e.ToString() == "Autodesk.Revit.DB.Mesh":
continue
else:
cleanconverted.append(e)
converted = cleanconverted
else:
for e in converted:
if e.ToString() == "Autodesk.Revit.DB.Mesh":
return [False]
if len(converted) != 0:
converted = isActuallygood(sat_path,converted,firstTime,geometrymode,bypasscheck)
if (bypasscheck and importinstance) or (importinstance and converted[0] != False):
satopt = SATImportOptions()
imported = doc.Import(sat_path,satopt,None)
element = doc.GetElement(imported)
element.Pinned = False
converted = [element]
satopt.Dispose()
if len(converted) == 0:
return [False]
return converted
def batchattempt(temp_path,surfs,importinstance,bypasscheck,scaleup,sendmessage):
randomname = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(5)])
x_sat_path = '%s%s.sat' % (temp_path, randomname)
dg.Geometry.ExportToSAT(surfs,x_sat_path)
if importinstance and bypasscheck:
satopt = SATImportOptions()
imported = doc.Import(x_sat_path,satopt,None)
element = doc.GetElement(imported)
element.Pinned = False
x_finalsurfaces = [element]
satopt.Dispose()
else:
x_finalsurfaces = quickSATcheck(x_sat_path,scaleup,importinstance,True,False,bypasscheck)
System.IO.File.Delete(x_sat_path)
if x_finalsurfaces[0] == False:
return [False]
else:
x_choppedfinal = []
for xf in x_finalsurfaces:
x_choppedfinal.append([xf])
return x_choppedfinal,sendmessage,True
def SurfacesLaundry(surfs,doc,importinstance,geometrymode,temp_path):
#scaleup = round(unitconverter(3.280839895,doc,True),8)
def tolist(obj1):
if hasattr(obj1,'__iter__'): return obj1
else: return [obj1]
def checkcompatible(geoms,customscale=[1,1,1],firstTime=False,geometrymode=True,bypasscheck=False):
randomname = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(5)])
sat_path = '%s%s.sat' % (temp_path, randomname)
if allisone(customscale) == False:
scaledgeoms = geoms.Scale(customscale[0],customscale[1],customscale[2])
geoms = scaledgeoms
dg.Geometry.ExportToSAT(geoms,sat_path)
converted = []
shapeImporter = ShapeImporter()
converted = list(shapeImporter.Convert(doc, sat_path))
shapeImporter.Dispose()
System.IO.File.Delete(sat_path)
for e in converted:
if e.ToString() == "Autodesk.Revit.DB.Mesh":
return [False]
if len(converted) != 0:
converted = isActuallygood(geoms,converted,firstTime,geometrymode,bypasscheck)
if len(converted) == 0:
return [False]
return converted
def revitOP(finalsub,nurbs=None,cleanexplodes=None,customscale=[1,1,1],disposeNurbs=True,geometrymode=True):
if nurbs == None and cleanexplodes == None:
testit = checkcompatible(finalsub,customscale,False,geometrymode)
if testit[0] == False:
raise Exception("Geometry Incompatible With Revit")
else:
if geometrymode == False:
finalsub.Dispose()
return testit
cleanfinalsub = dg.Geometry.Explode(finalsub)
revitgeom = []
for c in cleanfinalsub:
testit = checkcompatible(c,customscale,False,geometrymode)
if testit[0] == False:
c.Dispose()
raise Exception("Geometry Incompatible With Revit")
else:
if geometrymode == False:
c.Dispose()
revitgeom.append(testit)
revitgeom = flatten(revitgeom)
if disposeNurbs:
nurbs.Dispose()
for cleanex in cleanexplodes:
cleanex.Dispose()
finalsub.Dispose()
return revitgeom
def cleanupSurface(surface):
def subtractfunction(nurbs,cleanexplodes):
def loopclean(nurbs,thick,counter):
result = dg.Surface.SubtractFrom(nurbs,thick)[0]
if counter != 0:
nurbs.Dispose()
thick.Dispose()
return result
thickforms = []
for cleanex in cleanexplodes:
safearea = metersurfarea(cleanex)/10
if safearea > meternumber(0.1):
maxthicken = meternumber(0.1)
else:
maxthicken = safearea
thickforms.append(dg.Surface.Thicken(cleanex,maxthicken,True))
counter = 0
for thick in thickforms:
nurbs = loopclean(nurbs,thick,counter)
counter += 1
return nurbs
def tryingforall(surface,nurbs,cleanexplodes,geometrymode):
try:
finalsub = subtractfunction(nurbs,cleanexplodes)
return revitOP(finalsub,nurbs,cleanexplodes,scaleup,True,geometrymode)
except:
try:
finalsub = dg.Surface.Difference(nurbs,cleanexplodes)
return revitOP(finalsub,nurbs,cleanexplodes,scaleup,True,geometrymode)
except Exception as e:
if str(e) == "Cannot difference an empty list" and almostsame(nurbs,surface):
finalsub = nurbs
try:
return revitOP(finalsub,nurbs,cleanexplodes,scaleup,False,geometrymode)
except:
return None,finalsub
else:
return None,finalsub
def gencleanexplodes(subtractnurbs):
if isinstance(subtractnurbs, list):
if len(subtractnurbs) == 0:
return []
else:
explodednurbs = dg.Geometry.Explode(subtractnurbs)
cleanexplodes = []
if safemode:
for ex in explodednurbs:
if ex.Area < meternumber(0.002):
ex.Dispose()
else:
cleanexplodes.append(ex)
else:
cleanexplodes = list(explodednurbs)
subtractnurbs.Dispose()
return cleanexplodes
def easymethod(surface,geometrymode):
try:
nurbs = dg.Surface.ToNurbsSurface(surface)
if almostsame(nurbs,surface):
try:
return True,revitOP(nurbs,None,None,scaleup,True,geometrymode)
except:
return None,nurbs
else:
return None,nurbs
except:
return None,None
def isplanarsurf(s):
norms = [0,0.333,0.666,1]
pts = []
for n in norms:
for n2 in norms:
pts.append(dg.Surface.PointAtParameter(s,n,n2))
plane = dg.Plane.ByBestFitThroughPoints(pts)
status = True
for p in pts:
try:
inter = dg.Geometry.DoesIntersect(p,plane)
if inter == False:
status = False
break
except:
pass
return status
def joincurves(crvs):
def PointsEqual(pt1, pt2):
tolerance = 0.00001
if (math.fabs(pt1.X - pt2.X) < tolerance and
math.fabs(pt1.Y - pt2.Y) < tolerance and
math.fabs(pt1.Z - pt2.Z) < tolerance):
return True
else:
return False
crvs = flatten(tolist(crvs))
crvGroup = [-1 for x in range(len(crvs))]
crvGroup[0] = 0
maxGroupNum = 1
starts = []
ends = []
for crv in crvs:
starts.append(crv.PointAtParameter(0))
ends.append(crv.PointAtParameter(1))
for i in range(len(crvs)):
if (crvGroup[i] == -1):
crvGroup[i] = i
for j in range(len(crvs)):
if (i != j and
(PointsEqual(starts[i], starts[j]) or
PointsEqual(starts[i], ends[j]) or
PointsEqual(ends[i], starts[j]) or
PointsEqual(ends[i], ends[j]))):
if (crvGroup[j] == -1):
crvGroup[j] = crvGroup[i]
elif (crvGroup[i] != crvGroup[j]):
oldNum = crvGroup[j]
for k in range(len(crvs)):
if (crvGroup[k] == oldNum):
crvGroup[k] = crvGroup[i]
groups = []
excluded = []
for n in crvGroup:
innercount = 0
tmp = []
for n2 in crvGroup:
if n == n2 and n2 not in excluded:
tmp.append(crvs[innercount])
innercount += 1
if len(tmp) == 0:
continue
try:
tmp = dg.PolyCurve.ByJoinedCurves(tmp)
groups.append(tmp)
except:
for t in tmp:
groups.append(dg.PolyCurve.ByJoinedCurves([t]))
excluded.append(n)
joinedcurves = groups
return joinedcurves
def trimloopmethod(surface,nurbs,geometrymode):
try:
if surface.Closed:
return [None]
try:
joinedperms = [dg.PolyCurve.ByJoinedCurves(surface.PerimeterCurves())]
except:
joinedperms = joincurves(surface.PerimeterCurves())
finalsub = dg.Surface.TrimWithEdgeLoops(nurbs,joinedperms)
if almostsame(finalsub,surface):
return revitOP(finalsub,None,None,scaleup,True,geometrymode)
else:
return [None]
except:
return [None]
def thickenintersectMethod(surface,nurbs,geometrymode,pointOneMeter):
try:
tmp_thicken = dg.Surface.Thicken(surface,pointOneMeter,True)
isplanar = isplanarsurf(surface)
if isplanar:
tmp_pt = dg.Surface.PointAtParameter(surface,0.5,0.5)
tmp_norm = dg.Surface.NormalAtPoint(surface,tmp_pt)
intersecter = dg.Plane.ByOriginNormal(tmp_pt,tmp_norm)
finalsub = dg.Geometry.Intersect(intersecter,tmp_thicken)
for f in finalsub:
if isinstance(f,dg.Surface):
finalsub = f
break
tmp_thicken.Dispose()
else:
finalsub = dg.Geometry.Intersect(nurbs,tmp_thicken)
for f in finalsub:
if isinstance(f,dg.Surface):
finalsub = f
break
tmp_thicken.Dispose()
try:
return revitOP(finalsub,None,None,scaleup,True,geometrymode)
except:
return None,finalsub
except:
return [None]
def inithickenMethod(surface,nurbs,geometrymode):
try:
safearea = metersurfarea(surface)/10
if safearea > meternumber(0.1):
maxthicken = meternumber(0.1)
else:
maxthicken = safearea
thickeninit = dg.Surface.Thicken(surface,maxthicken,True)
try:
subtractnurbs = dg.Surface.SubtractFrom(nurbs,thickeninit)[0]
except Exception as e:
if "index out of range" in str(e) and almostsame(nurbs,surface):
subtractnurbs = []
elif "index out of range" in str(e):
try:
subtractnurbs = dg.Surface.Difference(nurbs,[surface])
except Exception as e:
if str(e) == "Unable to get FACE from BODY: no faces in BODY":
subtractnurbs = []
thickeninit.Dispose()
cleanexplodes = gencleanexplodes(subtractnurbs)
tryall = tryingforall(surface,nurbs,cleanexplodes,geometrymode)
if tryall[0] != None:
return tryall
else:
return None,tryall[1]
except:
return [None]
def simplesubmethod(surface,nurbs,geometrymode):
try:
try:
subtractnurbs = dg.Surface.Difference(nurbs,[surface])
except Exception as e:
if str(e) == "Unable to get FACE from BODY: no faces in BODY":
try:
try:
return revitOP(nurbs,None,None,scaleup,True,geometrymode)
except:
return None,nurbs
except:
return [None]
cleanexplodes = gencleanexplodes(subtractnurbs)
tryall = tryingforall(surface,nurbs,cleanexplodes,geometrymode)
if tryall[0] != None:
return tryall
else:
return None,tryall[1]
except:
return [None]
easy = easymethod(surface,geometrymode)
pointOneMeter = unitconverter(0.3280,doc,True)
if easy[0] != None:
return easy[1]
elif easy[0] == None and easy[1] == None:
return None
else:
nurbs = easy[1]
failsafesurfs = []
trimloop = trimloopmethod(surface,nurbs,geometrymode)
if trimloop[0] != None:
return trimloop
simple = simplesubmethod(surface,nurbs,geometrymode)
if simple[0] != None:
return simple
else:
try:
failsafesurfs.append(simple[1])
except:
pass
inithick = inithickenMethod(surface,nurbs,geometrymode)
if inithick[0] != None:
return inithick
else:
try:
failsafesurfs.append(inithick[1])
except:
pass
thickeninter = thickenintersectMethod(surface,nurbs,geometrymode,pointOneMeter)
if thickeninter[0] != None:
return thickeninter
else:
try:
failsafesurfs.append(thickeninter[1])
except:
pass
for failsurf in failsafesurfs:
surface = failsurf
easy = easymethod(surface,geometrymode)
if easy[0] != None:
return easy[1]
elif easy[0] == None and easy[1] == None:
return None
else:
nurbs = easy[1]
trimloop = trimloopmethod(surface,nurbs,geometrymode)
if trimloop[0] != None:
return trimloop
simple = simplesubmethod(surface,nurbs,geometrymode)
if simple[0] != None:
return simple
inithick = inithickenMethod(surface,nurbs,geometrymode)
if inithick[0] != None:
return inithick
thickeninter = thickenintersectMethod(surface,nurbs,geometrymode,pointOneMeter)
if thickeninter[0] != None:
return thickeninter
return None
sendmessage = False
is_sat = False
if isinstance(surfs,str) == False:
surfs = flatten(tolist(surfs))
x_scaledown = round(unitconverter(0.3048,doc,False),8)
x_pure = []
for xs in surfs:
if isinstance(xs,dg.Solid) or isinstance(xs,dg.Surface) or isinstance(xs,dg.PolySurface) or isinstance(xs,dg.Cuboid):
if x_scaledown != 1:
x_pure.append(xs.Scale(x_scaledown))
else:
x_pure.append(xs)
surfs = x_pure
bOP = batchattempt(temp_path,surfs,importinstance,bypasscheck,scaleup,sendmessage)
if bOP[0] == False:
pass
else:
return bOP
if isinstance(surfs,str):
if ".sat" in surfs.lower():
finalsurfaces = quickSATcheck(surfs,scaleup,importinstance,True,False,bypasscheck)
if finalsurfaces[0] == False:
surfs = dg.Geometry.ImportFromSAT(surfs)
x_pure = []
for xs in surfs:
if isinstance(xs,dg.Solid) or isinstance(xs,dg.Surface) or isinstance(xs,dg.PolySurface) or isinstance(xs,dg.Cuboid):
x_pure.append(xs)
surfs = x_pure
is_sat = True
else:
choppedfinal = []
for f in finalsurfaces:
choppedfinal.append([f])
return choppedfinal,sendmessage,True
else:
return [],sendmessage
scaledown = 1
else:
scaledown = round(unitconverter(0.3048,doc,False),8)
surfs = flatten(tolist(surfs))
finalsurfaces = []
precheck = []
for s in surfs:
try:
if isinstance(s,dg.Surface):
exploded = dg.Geometry.Explode(s)
if disposeinit:
s.Dispose()
if len(exploded) > 1:
try:
precheck.append(dg.PolySurface.ByJoinedSurfaces(exploded))
except:
for exp in exploded:
precheck.append(exp)
else:
for exp in exploded:
precheck.append(exp)
else:
precheck.append(s)
except:
precheck.append(s)
surfs = precheck
bOP = batchattempt(temp_path,surfs,importinstance,bypasscheck,scaleup,sendmessage)
if bOP[0] == False:
pass
else:
return bOP
for s in surfs:
surface = s
safe = True
if safemode:
if isinstance(surface,dg.Solid) or isinstance(surface,dg.PolySurface):
nicesurfaces = []
explodedcheck = dg.Geometry.Explode(surface)
for explocheck in explodedcheck:
if isbellowarea(explocheck):
safe = False
explocheck.Dispose()
sendmessage = True
else:
nicesurfaces.append(explocheck)
if safe:
for n in nicesurfaces:
n.Dispose()
else:
if isbellowarea(surface):
s.Dispose()
if scaledown != 1 and geometrymode == False:
surface.Dispose()
sendmessage = True
continue
if safe == False:
tmp = []
for mini_s in nicesurfaces:
testit = checkcompatible(mini_s,scaleup,False,geometrymode)
if testit[0] == False:
cleanedup = cleanupSurface(mini_s)
if cleanedup != None:
tmp.append(cleanedup)
else:
tmp.append(testit)
if geometrymode == False and testit[0] != False:
mini_s.Dispose()
if len(tmp) != 0:
finalsurfaces.append(flatten(tmp))
else:
finalsurfaces.append(["FAILED"])
if (disposeinit or is_sat) and geometrymode == False:
s.Dispose()
if scaledown != 1 and geometrymode == False:
if isinstance(surface,list):
for surf in surface:
surf.Dispose()
else:
surface.Dispose()
continue
testit = checkcompatible(surface,scaleup,True,geometrymode)
if testit[0] == False:
isexploded = False
if isinstance(surface,dg.Solid) or isinstance(surface,dg.PolySurface):
surface = list(dg.Geometry.Explode(surface))
isexploded = True
if isexploded:
tmp = []
for mini_s in surface:
testit = checkcompatible(mini_s,scaleup,False,geometrymode)
if testit[0] == False:
cleanedup = cleanupSurface(mini_s)
if cleanedup != None:
tmp.append(cleanedup)
else:
tmp.append(testit)
if geometrymode == False and testit[0] != False:
mini_s.Dispose()
if len(tmp) != 0:
try:
soljoin = polysurfaceOP(flatten(tmp))
try:
sol = dg.Solid.ByJoinedSurfaces([soljoin])
soljoin.Dispose()
except:
sol = soljoin
testit = checkcompatible(sol,scaleup,True,geometrymode)
if testit[0] == False:
sol.Dispose()
finalsurfaces.append(flatten(tmp))
else:
finalsurfaces.append([sol])
except:
finalsurfaces.append(flatten(tmp))
else:
finalsurfaces.append(["FAILED"])
sendmessage = True
else:
cleanedup = cleanupSurface(surface)
if cleanedup != None:
finalsurfaces.append(cleanedup)
else:
finalsurfaces.append(["FAILED"])
else:
finalsurfaces.append(testit)
if (disposeinit or is_sat) and geometrymode == False:
s.Dispose()
if scaledown != 1 and geometrymode == False:
if isinstance(surface,list):
for surf in surface:
surf.Dispose()
else:
surface.Dispose()
return finalsurfaces,sendmessage
forms = []
laundryMain = SurfacesLaundry(surfs,doc,importinstance,geometrymode,temp_path)
result = laundryMain[0]
sendmessage = laundryMain[1]
for rlist in result:
tmp = []
for r in rlist:
tmp.append(r)
if len(tmp) != 0:
forms.append(tmp)
if len(laundryMain) == 3:
return forms,sendmessage,True
else:
return forms,sendmessage
if geoms == [] or geoms == None:
return [],None,False
geometrymode = True
disposeinit = False
TransactionManager.Instance.ForceCloseTransaction()
if importinstance:
trans = Transaction(doc, 'Create ImportInstance')
else:
if doc.IsFamilyDocument:
trans = Transaction(doc, 'Create FreeForm')
else:
trans = Transaction(doc, 'Create DirectShape')
trans.Start()
warnhandle = WarnSwallowImport()
foptions = trans.GetFailureHandlingOptions()
foptions.SetFailuresPreprocessor(warnhandle)
foptions.SetClearAfterRollback(True)
trans.SetFailureHandlingOptions(foptions)
try:
smartrevitsolidMain = smartrevitsolid(geoms,doc,importinstance,safemode,geometrymode,disposeinit,temp_path)
except:
trans.RollBack()
return [],None,False
if len(smartrevitsolidMain) == 3:
trans.Commit()
else:
trans.RollBack()
freeformslists = smartrevitsolidMain[0]
sendmessage = smartrevitsolidMain[1]
result = []
singlelist = []
TransactionManager.Instance.ForceCloseTransaction()
if importinstance:
if singlefinalprocess:
trans = Transaction(doc, 'Create ImportInstance')
else:
trans = Transaction(doc, 'Create ImportInstances')
else:
if doc.IsFamilyDocument:
trans = Transaction(doc, 'Create FreeForms')
else:
trans = Transaction(doc, 'Create DirectShape')
if len(smartrevitsolidMain) == 3:
if singlefinalprocess:
trans = Transaction(doc, 'Move ImportInstance')
else:
trans = Transaction(doc, 'Move ImportInstances')
trans.Start()
satopt = SATImportOptions()
for slist in freeformslists:
tmp = []
tmpexport = []
for s in slist:
if s == "FAILED":
if returnfailures:
tmp.append(None)
continue
compare = s.ToString()
if compare == "Autodesk.Revit.DB.FreeFormElement" or compare == "Autodesk.Revit.DB.Solid" or compare == "Autodesk.Revit.DB.ImportInstance" or compare == "Autodesk.Revit.DB.DirectShape":
tmp.append(s)
else:
if singlefinalprocess:
singlelist.append(s)
else:
tmpexport.append(s)
if len(tmp) != 0:
result.append(tmp)
if len(tmpexport) != 0:
result.append(finalimport(importinstance,tmpexport,satopt,singlefinalprocess))
if len(singlelist) != 0:
result.append(finalimport(importinstance,singlelist,satopt,singlefinalprocess))
satopt.Dispose()
mainvec = None
if len(result) != 0 and centermode:
doc.Regenerate()
mainvec = movecenter(doc,result)
trans.Commit()
if importinstance or (singlefinalprocess and doc.IsFamilyDocument == False):
cleanresult = []
for r in result:
cleanresult.append(r[0])
result = cleanresult
if (singlefinalprocess and importinstance) or (singlefinalprocess and doc.IsFamilyDocument == False):
result = result[0]
return result,mainvec,sendmessage
Okay there’s a limit for replies as well… you may check the full code from the node it self… it is several thousand lines…
If anyone from Autodesk would like to escalate this issue and perhaps give more priority than others… he’s welcome to do so…
Here’s the link of FULL report with many examples and cases and explanations …etc
https://feedback.autodesk.com/project/feedback/view.html?cap=cb0fd5af18bb49b791dfa3f5efc47a72&uf=b41ee1e3f04b4177a0823351348691e7&slsid=4a0d93f403ac4e3787cacc2530e731bf
Oh and it’s status is Closed not submitted to Jira … (I guess means ignored somehow)…
More images of the study
After Healing with Dynamo in Revit 2022:
This issue caused me and still causing lots of troubles… I hope, Autodesk listens and realize what they have caused…
Thanks for the great info dump @Karam_Baki. I’ve run into the character limit a few times - it’s a drag but unfortunately it’s a limit of the forum.
I believe that this is because of the source of the feedback - the beta forum is for issues around new features which stop work or fail to execute - at some point these get closed to migrate the effort to the new builds - it doesn’t mean they . I recommend creating a Dynamo for Revit issue for similar scope in the future - these stay open and are more readily reviewed by non-beta participants.
I have seen a few issues on developer channels around this issue, but the specifics are beyond my skill set. I did raise the issue with the team so hopefully we’ll hear back after the holiday.
EDIT: there is in fact an open Jira ticket (179667) and several related tasks for this issue.
That’s great news, I hope they really keep it as a priority, this problem will just keep getting worse with more customers facing it.
Thank you, @Karam_Baki, for this detailed explanation and the code. They are much appreciated.
It seems this node works again in Revit 2023 but the geometry gets exported at a strange scale. Seems like 3:1.
perhaps a factor of 3.28084? Meters to Ft
Ahh, very likely. Thanks. This might be the issue.
You were right!
Here is a screenshot. I also had to scale around the lowest point of the bounding box of the geometry.
Does the Node FamilyInstance.ByGeometry work again with Revit 2022? I am still getting the error-message:
NewForm_background TypeError: expected Solid, got Mesh
@leonard.moelders - The underlying Revit API (Which this node calls) was improved for Revit 2022.1, which has caused the error you see on the FamilyInstance.ByGeometry
node here.
Inside Revit 2022 and earlier, there was a legacy method used (Which is still used in the Import DWG
feature) that allowed invalid geometry to be imported into Revit. “Invalid” here means that it did not pass Revit’s validation checks, which check for a whole raft of geometric conditions. If invalid geometry is detected in these validation checks, mesh will be imported instead of base geometry.
This was done to build consistency within geometrical import, for things like missing pieces of furniture in one particular view would occur. Previously, it would import everything and you as a user had to manually check if everything was correct. Now, you’ll get a solid import when the geometry is “viable” according to Revit, and a mesh when it is “not viable” as a fallback method.
We can look to improve the error message on the FamilyInstance.ByGeometry
node so that it’s more human-friendly and explains what I’ve mentioned above succinctly, as well as look if it’s possible to allow it to also parse through mesh geometry, not just solids.
“Improved” is an opinion. From the standpoint of a user that used that feature a whole lot - it sure doesn’t feel improved when I have to take the .sat down to Revit 2020 and import into that version, then turn around and bring that resultant family into Revit 2022 and see that it still works and scratch my head thinking “it obviously works in the 2022 engine - so why wont it let me do it…”. Sure feels like “broken” instead of “improved”. I understand the desired to build consistency - but it feels like some of those validity checks might be a tad bit arbitrary since that which worked in many previous versions now does not work. Instead of (or in addition to) improving the error message - why not add in a Bool option on the ImportDWG feature to allow the user to “skip” the validity check (use the old “unimproved” method") if they feel like living dangerously.
Hi @Ben_Osborne - Definitely recognize that “improved” is an opinion here, and that this change can also be perceived as a “regression”, especially given that a lot of the geometry that came through could be effectively correct, even if not in all instances.
We can explore what options we have including what you suggested, but will be somewhat limited to what the Revit API provides and/or we could follow the same path that @Karam_Baki did above with attempted healing methods in his Synthesize Toolkit package, and in particular K-Family Import
node. For the latter, note that we won’t be able to guarantee results with these and it would indeed be a “Living dangerously” situation
@Karam_Baki Is there any documentation on how to use your K-Family Insert - Node
? As my current Code is only working up to Revit 2021 due to the changes in the Import of SATs, I am thinking of using your package. I have a family-template which I use to color deviations for a target-performance comparison. It looks like this:
the familyinstance was assembled from 5 cylindrical solids that where created in dynamo. The red and green colors are handed over via two material-names. The old code iterated through the 5 SATs and imported them as FreeFormElements into the family and assigned the material and at the end it saved the familydocument and created a new familyinstance. Can I rebuild this with your node? I tried but failed unfortunately. My Inputs would be:
5 geometries
1 family name
1 family template
1 family category
1 family subcategory
5 materials (a mix of 2 materials to paint red or green)
I would really appreciate your help
Yes it can do that, but for the materials you’ll need to change an input (VARIsOneMaterial to False)
Then it will create instance (or type parameters based on another input) as “Material 1, Material 2, Material 3…etc”, so you use them in the project.
Other than this, you don’t actually have to feed any more data, just the Dynamo geometry or SAT file, it accepts both.
*Optionally you can change family category, subcategory, family name and material parameter being type or instance.
It’ll detect the appropriate family template, and all other recommended settings.
EDIT, I didn’t find the time to do proper documentation yet, probably sometime after I finish off the headache of Revit 2023
Thanks for your answer. Unfortunately I get this error-message:
Traceback (most recent call last):
File “”, line 2152, in
File “”, line 836, in mainOP
File “”, line 667, in geometryOP
File “”, line 145, in changemat
AttributeError: ‘list’ object has no attribute ‘lower’
This is what my material-input (strings) looks like:
Just input a string, not a list
It will propagate this material name to all of the geometry and then you can use the parameters to change it in Revit directly
EDIT:
I’m thinking of Supporting this, inputing a matching material list, it’s relatively easy to implement, you’ll find it working in 2023, but for now, use a single material as stated above, then change the parameters in Revit to fit your needs
You may also use @Level 1 and input the geometry list and equivalent material list, this will produce a family for each geometry though (Don’t worry about naming, it automatically increments)