RevitPythonDocs for Dynamo and pyRevit - Request for Feedback

Hello Dynamo Friends :slight_smile:

Going the first steps at creating a collection of python scripts for Dynamo and pyRevit, I´d like to know what are your needs when you are looking for code, classes, methods.

http://www.revitpythondocs.com/

It will for sure get search and filter options, but for now it´s more of creating a good naming convention and finding out what code is really needed. In the following what I have come up with so far and I´m looking forward to your thoughts and hopefully contributions! The code is on Github.

Some rules for the code:

  • Every script is a full working code including imports.
  • Necessary imports only.
  • No inputs from dynamo needed, necessary elements are created in the code.
  • Dynamo code: tabs as indent only.
  • pyRevit code: spaces as indent only.
  • No comments, just clean code.
  • snake_case only, for variables, functions, everything.
  • No “for i in a:”, proper names for everything (except list comprehension).

Code is available in different versions, here are examples to explain the naming convention (which is far from perfect right now!):

Basic:
The most simple representation of a task/method:

import clr
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import Material

doc = DocumentManager.Instance.CurrentDBDocument

material_name = 'material_01'

TransactionManager.Instance.EnsureInTransaction(doc)

material_id = Material.Create(doc, material_name)
material = doc.GetElement(material_id)

TransactionManager.Instance.TransactionTaskDone()

OUT = material

Advanced:
Includes error handling.

import clr
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import Material

doc = DocumentManager.Instance.CurrentDBDocument

material_name = 'material_01'

try:
	TransactionManager.Instance.EnsureInTransaction(doc)
	material_id = Material.Create(doc, material_name)
	material = doc.GetElement(material_id)

	TransactionManager.Instance.TransactionTaskDone()
	OUT = material
except Exception as e:
	TransactionManager.Instance.ForceCloseTransaction()
	OUT = str(e)

Example:
A usecase with a corresponding methods and all available API properties.

import clr
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import Material

doc = DocumentManager.Instance.CurrentDBDocument

def get_fill_pattern(doc, pattern_name):

    fill_patterns = FilteredElementCollector(doc).OfClass(FillPatternElement)
    fill_pattern = next((fp for fp in fill_patterns if fp.Name == pattern_name), None)
    return fill_pattern

def get_appearance_asset(doc, asset_name):

    collector = FilteredElementCollector(doc).OfClass(AppearanceAssetElement)
    for asset in collector:
        if asset.Name == asset_name:
            return asset
    return None

def set_properties(material):

    material.Color = color
    material.Shininess = 0.5
    material.Smoothness = 0.5
    material.Transparency = 10
    material.MaterialClass = 'Concrete'
    material.MaterialCategory = 'Structural'
    material.UseRenderAppearanceForShading = True
    material.AppearanceAssetId = asset.Id

    material.SurfaceForegroundPatternId = fill_pattern.Id
    material.SurfaceForegroundPatternColor = color
    material.SurfaceBackgroundPatternId = fill_pattern.Id
    material.SurfaceBackgroundPatternColor = color
    material.CutForegroundPatternId = fill_pattern.Id
    material.CutForegroundPatternColor = color
    material.CutBackgroundPatternId = fill_pattern.Id
    material.CutBackgroundPatternColor = color

material_name = 'material_01'
pattern_name = '<Solid fill>'
asset_name = 'Cyan'
color = Color(128, 128, 128)

try:
	TransactionManager.Instance.EnsureInTransaction(doc)
	
    fill_pattern = get_fill_pattern(doc, pattern_name)
    if fill_pattern is None:
        raise Exception('Solid fill pattern not found.')

    asset = get_appearance_asset(doc, asset_name)
    if asset is None:
        raise Exception('Appearance asset not found.')

    material_id = Material.Create(doc, material_name)
    material = doc.GetElement(material_id)

    set_properties(material)

    TransactionManager.Instance.TransactionTaskDone()
    result = material

except Exception as e:
    TransactionManager.Instance.ForceCloseTransaction()
    result = str(e)

OUT = result

Function:
The desired task/method as a function, for me the most important code for copy paste purposes.

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import FilteredElementCollector, View, ViewType
import Autodesk
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager

doc = DocumentManager.Instance.CurrentDBDocument

def get_view_templates():
	views = FilteredElementCollector(doc).OfClass(View)
	templates = []
	for view in views:
		if view.ViewType != ViewType.ThreeD and view.ViewType != ViewType.Schedule and view.ViewType != ViewType.Section and view.IsTemplate == True:
			templates.append(view)
	return templates

templates = get_view_templates()
OUT = templates

designscript:
For dynamo geometry objects:

import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import Solid, Cuboid

def dynamo_solid():
	width = 10.0
	length = 20.0
	height = 15.0
	solid = Cuboid.ByLengths(width, length, height)
	return solid

def faces_from_solid(solid):
    if isinstance(solid, Solid):
        faces = solid.Faces
        return faces
    else:
        return 'Input is not a Dynamo Solid.'

solid_dynamo = dynamo_solid()
faces = faces_from_solid(solid_dynamo)

OUT = faces

Revit:
For Revit geometry objects:

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import XYZ, Line, CurveLoop, Solid, GeometryCreationUtilities
from RevitServices.Persistence import DocumentManager

doc = DocumentManager.Instance.CurrentDBDocument

def revit_solid():
    width = 10.0
    length = 20.0
    p0 = XYZ(0, 0, 0)
    p1 = XYZ(width, 0, 0)
    p2 = XYZ(width, length, 0)
    p3 = XYZ(0, length, 0)
    line1 = Line.CreateBound(p0, p1)
    line2 = Line.CreateBound(p1, p2)
    line3 = Line.CreateBound(p2, p3)
    line4 = Line.CreateBound(p3, p0)
    curveLoop = CurveLoop()
    curveLoop.Append(line1)
    curveLoop.Append(line2)
    curveLoop.Append(line3)
    curveLoop.Append(line4)
    height = 15.0
    solid = GeometryCreationUtilities.CreateExtrusionGeometry([curveLoop], XYZ.BasisZ, height)
    return solid

def faces_from_solid(solid):
    if isinstance(solid, Solid):
        faces = solid.Faces
        return faces
    else:
        return 'Input is not a Revit Solid.'

solid_revit = revit_solid()
faces = faces_from_solid(solid_revit)

OUT = faces

So far, this are my ideas, how about yours?

12 Likes

Love this!

A good boilerplate would make a nice addition. If you get stuff set up for PRs I am happy to contribute my CPython 3 templates (Dynamo Core, Revit, and Civil 3D).

A good addition would be some additional annotations to outline what the various sections are doing, or overly annotated to outline each line.

Also you may want to consider noting the coding standard (ie: PEP), and standardizing script properties.

3 Likes

Great, my focus is on dynamo IronPython 2023 and pyRevit IronPython 2023. As soon as I have set everything up I will fill those two categories with a few hundred scripts and then reach out to everyone who announces his contribution.

I agree that writing self-describing code is a good practice
However, some comments or functions comments like doc-string are a good practice too

sometimes comments are necessary

example NumPy Style Python Docstrings

4 Likes

hi, very good initiative (courage to you),
I will participate when I have a higher level,
quick question in passing what is the role of the Dispose() method present in many classes
Sincerely
christian.stan

In my opinion some degree of comments are necessary. On a project of scope self documenting code is a bit of a myth; without outlining code sections it takes longer to ‘return’ to a previously familiar database than it should. For small projects with profession coders perhaps not. But what percentage of the larger community knows Python at that level? 90%, 70%, 50%, 30% or 10%? If it is anything less than 90 we need to assume that some day there will be someone who needs more breadcrumbs than just clearly stated variables to get that major Revit API change implemented.

And so overly documented code is my preference in the Dynamo/Python scripting community. The annotations take minimal time to add, help with ‘self editing’ and ensure the largest possible portion of the community can resolve issues faster we can’t yet know if, reducing the technical debt on you as a code owner.

1 Like

@c.poupin @jacob.small

I totally agree, however, I could see maintaining commenting standards getting in the way of documenting good processes. In this case, I think code comments should be minimal and focused on generic workflow descriptions rather than code reasoning or in-depth explanations. There should however be attached comments and explanations outside of the code sample as needed. An “annotation” type feature would be nice.

A good commenting standard has to be written once. A common shared repository requires more manual review of each PR if there isn’t any such standard to be had.

2 Likes

Self documentation can be done quite easily with openAI API or ChatGPT (with a little verification anyway :joy: )

example with doc-string addition

4 Likes

I guess I’m also imagining people just “posting” their written code. Which will not be the case. Everything would be rewritten, including comments, so that’s not a huge ask.

I guess it becomes a question of requirement. When would code require comments? To what extent? Who would dictate that? Will this have any community management functionality that would allow certain users to comment on existing documentation or make suggestions? (@gerhard.p, these are mostly directed at you I guess.)

Always.

Only half kidding there.

As a thought exercise, go find the first excel calculation tool you ever built for MEP loads - yes that one which you did a year out of school. Now edit it to use the new codes. Time yourself on how long it takes.

Now imagine doing that if you had notes on what each reference was among all those formulas.

@Nick_Boyts I think I have sayd everything

:grinning:

Jokes aside, my intent is to create a collection of pretty small tasks and examples how the Revit API works with python, to somehow fill the hole from RevitApiDocs. My intent is not to share scripts with hundreds of lines that do many tasks. So commenting complex parts should not be necessary because there are none. I´m working pretty much with KI and I´m tired of getting everything commented, so if someone wants comments, put the code in any KI.
So I have a pretty casual approach on this topic and see two cases where comments are OK.

  1. A user doesn´t understand a code and asks for adding comments. We add comments.
  2. A contributor want´s to share a code and thinks there is such a complex part in there that no one will understand. We add comments.

In short: If it´s so complex that a KI can´t comment it, we comment it. But I don´t see this for this project.
Maybe this is better discussed with an example?

What I´m more worried about is, is it really necessary to make so many different versions of the same script/task. Will they really be used? I´m not quiet sure how other people work and what they need. I`ll pretty much just use the versions with function, and while I want my functions to return empty lists if no elemets are found, maybe others want error messages?

2 Likes

I think that’s going to be the hard part: keeping the content useful enough to warrant sharing, but simple enough to not need comments. It can certainly be done, but people are always going to want more. Scope creep could be a huge issue here or it may not if you stick to a solid, simple approach. Sounds like you already have answers for a lot of these questions though. I’m excited to see where this goes. Already told the rest of my team to keep an eye on it. :blush:

2 Likes

@jacob.small

Is there an example in the currently uploaded scripts that you can use to show what comments you would add? So we all know what exactly we are talking about.

To me this indicates that the intent is to show users how to build their own stuff from these as samples. And so the ‘target audience’ is less skilled users who are learning the basics of a concept, meaning there will be knowledge gaps for Python, Dynamo, and the Revit API concurrently. And so we can start to identify locations where comments are handy rather quickly. I’ll start before a single line of code is written: let’s say you have a task where you need both the Dynamo’s geometry classes and the geometry classes in Revit.DB (fairly common for me anyway).

Starting with the standard boiler plates by copy pasting content from a Revit automation boilerplate and a Dynamo boilerplate would produce this:

clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

But that means we have conflicting classes for Line, Surface, Plane, Curve… pretty much everything geometry related other than XYZ and Coordinate System if memory serves.

So we might modify the boilerplate to read something like this:

clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript import Geometry as DG
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

It’s a subtle change, but one which will make that code much more difficult to port code from script to another if they didn’t have the same alias for Dynamo’s Geometry namespace, or if the alias was something other than DG.

Now everyone in the thread so far understands why my boilerplate would have this. If we put that in front of 90% of the users would they understand why? My gut says not so much. I see “why is my python not working” where the resolution is fixing import configurations once or twice a month. Now that is not a lot and I might be an outlier, but to me it’s enough that it’s worth teaching the why not just providing a solution, and so I utilize something like this for my boilerplate:

clr.AddReference('ProtoGeometry') #add the Dynamo geometry library to the CLR
from Autodesk.DesignScript import Geometry as DG #add the Dynamo geometry class using the alias DG, to ensure no overlap between class calls in Revit or Dynamo
clr.AddReference('RevitAPI') #add the Revit API to the CLR
from Autodesk.Revit.DB import * #import the entirety of the Revit.DB API into the Dynamo environment

Admittedly that is likely more than the project intends which is fine - there is value in any degree of information shared even if it doesn’t upskill consumers directly. So for the degree of commentary provided, the Python samples on the Revit API docs site might be a good starting point. The “SetParameterByName” code is a fairly good balance between what a novice trying to learn would need and something easy to reuse for someone more seasoned.

5 Likes

Interesting note: Excluding the Autodesk provided code samples and function call documentation, there are more Python examples on the Revit API docs site than C# examples… Is the ‘gap’ the lack of python syntax as shown here, or in the full class examples as shown here? Both are insufficient to directly utilize as functional code even in C#, but if I understand the gap better I can try to lobby for more of the documentation people need directly from experts inside the factory.

1 Like

My only request would be to use a formatter, such as black (personal fave), to keep presentation and readability consistent. Unfortunately black is a bit tricky with python 2.

I do think a minimal amount of docs is useful, especially if the code is doing something that isn’t self evident in the code itself, however the naming of variables, functions, classes and arguments goes a long way to eliminating confusion. We don’t need the epic doc strings that some python modules now have, I do think it would be funny to start a doc string with In this doc string I will explain…

1 Like

I reached out to Gui Talarico (RevitAPIdocs), we agreed that this is better created as a stand alone project instead of integrating this to the already existing code area of APIdocs.

Revit API Docs - Code Samples

Also asked for permission to follow the naming convention and so I just registered the domain

www.RevitPythonDocs.com

and am now starting to code the page. This will take some time^^

Great example Jacob! This is for sure worth a comment!

I also found an example i commented, a code for substracting a high number of solids:

import Autodesk.DesignScript.Geometry as geom

def subtract_solids(main_solid, solids_to_subtract, group_size=20):
    """
    To ensure a successful run Solid.DifferenceAll is not performed on all solids at once but with solid groups. 
    Failing groups will perform Solid.Difference for every single solid. 
    Adjust group size to your needs.
    """
    if main_solid is None or main_solid.Volume <= 0:
        return "Invalid main solid."

    solid_groups = [solids_to_subtract[i:i+group_size] for i in range(0, len(solids_to_subtract), group_size)]
    
    for group in solid_groups:
        valid_group = [s for s in group if s is not None and s.Volume > 0]
        try:
            main_solid = geom.Solid.DifferenceAll(main_solid, valid_group)
        except Exception:
            # If DifferenceAll fails, subtract solids one by one
            for solid in valid_group:
                try:
                    main_solid = geom.Solid.Difference(main_solid, solid)
                except Exception:
                    continue
            
    return main_solid  

main_solid = IN[0]
solids_to_subtract = IN[1]

result_solid = subtract_solids(main_solid, solids_to_subtract)

OUT = result_solid

The script collection should serve beginners and advanced users. There will for sure be a section for lessons and examples for beginners! There can be a very long explanation about proper importing. But there won´t be over annotation in every single script of the “standard section”.

I think the few scripts I uploded already are OK without annotations.

Annotations are OK if something extraordinary is happening that needs annotating. Larger scripts that are more likely to need annotations can have one for sure, maybe making an own category for longer scripts?!

I´m changing filename convention to camelCase for the task.

So instead of

set_material_properties_example_function_ironpython_2023.py
solid_faces_function_designscript_ironpython_2023.py

I´m going for

SetMaterialProperties_example_function_ironpython_2023.py
SolidFaces_function_designscript_ironpython_2023.py

What will be displayed on the page as

Set Material Properties (example, function)
Solid Faces (function, designscript)

Ideally variables should be lowerCamelCase, where the first letter isn’t capitalized. This helps users learn C# (which should be the eventual capability goal for everyone), and will help in porting code between platforms.

UpperCamelCase should be utilized for classes. This also follows C# coding standards.

Snake_Case I am moot to, but again to me it would be lower_Snake_Case rather than Upper_Snake_Case. :slight_smile:

All of that said we are at a personal preference rather than a ‘you should do it this way’. Today alone I had to work with all of the above in various projects (I rarely get to pick).

1 Like