Help with Python Script for Surface Grading

Hello everyone,

I’m working on a Python script in Dynamo for Civil 3D to automate a surface grading task, and I’ve run into a persistent and unusual error. I would be very grateful for any insights the community might have.

The Goal: The script’s purpose is to generate a new, regularized TIN surface within a closed polyline boundary. The new surface must adhere to maximum slope constraints in both the North-South (Y-axis) and East-West (X-axis) directions. The logic involves creating a grid of points within the boundary and calculating the Z-elevation for each point based on its neighbors and the specified slope limits.

The Problem: The script consistently fails with the error: AttributeError: 'Polygon' object has no attribute 'Contains'.

This error occurs on the line where the script checks if a newly generated grid point lies inside the boundary polygon. This is very strange, as standard Autodesk.DesignScript.Geometry.Polygon objects should always have the .Contains() method.

What We Have Already Tried (Extensive Debugging): We have gone through a long troubleshooting process and have already ruled out the most common issues:

  • Input Verification: We confirmed via a separate diagnostic script (type(IN[1]).__name__) that the object being passed into the Python node is indeed a 'Polygon'.
  • Graph Logic: We have tried creating the Polygon input using two different methods:
    1. A custom node (PolyCurveToPolygon from the Arkance Systems package).
    2. A full chain of native Dynamo nodes (PolyCurve.Curves, Curve.StartPoint, List.AddItemToEnd, Polygon.ByPoints, etc.).
    • Both methods produce a valid Polygon in the Dynamo environment, but both result in the same error inside the Python node.
  • Environment Configuration: We have explicitly set the Python engine to CPython3 in Dynamo’s preferences and have restarted Civil 3D multiple times.
  • Package Conflicts: We have tried running the script after activating the “Disable Loading Custom Packages” option and restarting. The error still persists even in this “safe mode” using only native nodes.
  • Script Robustness: The script itself was modified to internally check the input type and handle conversions, but the error remains.

Our conclusion so far is that this points to a corrupted Dynamo installation or a problem with the CPython3 engine’s interface with the Geometry library (ProtoGeometry). Before proceeding with a full software repair, I wanted to ask for your help.

The Script: Here is the final, most robust version of the script we are using:

# Load Dynamo's geometry libraries
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

# 1. GET INPUTS FROM DYNAMO
# =======================================
original_surface = IN[0]
boundary_input = IN[1] # Geometry from Dynamo graph
max_slope_ns = IN[2] 
max_slope_ew = IN[3] 
grid_step = IN[4]         

# 2. MAIN LOGIC
# =======================================

# Safety check to ensure the boundary is a Polygon for the .Contains() method
if type(boundary_input).__name__ == 'PolyCurve':
    boundary_polygon = Polygon.ByVertices(boundary_input.Vertices)
else:
    boundary_polygon = boundary_input

final_points = []
point_dictionary = {}

# Get the BoundingBox from the original input geometry for reliability
bbox = boundary_input.BoundingBox

x_min, y_min = bbox.MinPoint.X, bbox.MinPoint.Y
x_max, y_max = bbox.MaxPoint.X, bbox.MaxPoint.Y

# Generate the grid coordinates
x_range = range(int(x_min // grid_step), int(x_max // grid_step) + 1)
y_range = range(int(y_min // grid_step), int(y_max // grid_step) + 1)

# Iterate over every point in the grid
for j in y_range:
    y = j * grid_step
    for i in x_range:
        x = i * grid_step
        
        current_point_2d = Point.ByCoordinates(x, y, 0)

        # Check if the point is inside the boundary
        if boundary_polygon.Contains(current_point_2d):
            # If this is the first point, get Z from the original surface
            if not point_dictionary:
                initial_z = 0
                try:
                    initial_z = original_surface.PointAtParameter(x,y).Z
                except:
                    pass
                
                final_point = Point.ByCoordinates(x, y, initial_z)

            # For all other points, calculate Z based on neighbors and slopes
            else:
                neighbor_south = point_dictionary.get((x, y - grid_step))
                neighbor_west = point_dictionary.get((x - grid_step, y))
                
                potential_z_ns = float('inf')
                potential_z_ew = float('inf')

                if neighbor_south:
                    potential_z_ns = neighbor_south.Z + (grid_step * max_slope_ns)
                if neighbor_west:
                    potential_z_ew = neighbor_west.Z + (grid_step * max_slope_ew)
                
                # Choose the most restrictive (lowest) elevation to meet both slope criteria
                new_z = min(potential_z_ns, potential_z_ew)
                
                if new_z == float('inf'):
                   if neighbor_south: new_z = potential_z_ns
                   elif neighbor_west: new_z = potential_z_ew
                   else: new_z = 0

                final_point = Point.ByCoordinates(x, y, new_z)
            
            # Add the new point to our output list and our dictionary for the next iteration
            final_points.append(final_point)
            point_dictionary[(x, y)] = final_point

# 3. SEND OUTPUT TO DYNAMO
# =======================================
OUT = final_points

Environment Details:

  • Software: Autodesk Civil 3D 2024
  • Dynamo Version: Dynamo Core 2.19
  • Python Engine: CPython3

My Question: Has anyone ever encountered a situation where a standard Dynamo geometry object (Polygon) appears to lose its methods (.Contains) when passed into a Python Script node? Is there any known issue with this Dynamo/Civil 3D version that could cause this?

Any ideas would be greatly appreciated. Thank you!

teste - simplificando superficie por declividade.dyn (27.7 KB)
teste de dynamo surface.dwg (3.9 MB)

As the error says, there is no Polygon.Contains() method. Try Polygon.ContainmentTest().

3 Likes

To follow on @zachri.jensen’s point, remember you can always comment out subsequent code and review what you have and what you can do with it.

  1. Triple quote from the failing line to the end of the script which will comment out the active line and all subsequent lines (if your final set of quotes is proceeded with a # then you won’t need to remove it).
  2. Insert a new line before the start of the commented out code.
  3. Type OUT = [thing, thing.__class__, dir(thing)] where thing is the instance of the object which is giving you the issue. In this case you would use boundary_polygon.

This will return the object, what it’s type is, and what constructors, methods, and properties you can do with that object. You can then look for the call you think you should have, or find an alternative. If you want to learn more about a call try adding Class.Method.__doc__ to the end of the OUT line.

2 Likes

Hi,

In the context of constructing a RAG test, I recently created a Python utility class that allows you to obtain more information about the members of a .Net object (including documentation when the XML file exists).

The result is in Markdown format.

Python Class Utils
import sys
import System
import clr
from System.Reflection import BindingFlags

import xml.etree.ElementTree as ET
import os
import re
import difflib
from difflib import get_close_matches

class NetType_Utils:
    @staticmethod
    def get_Type_Infos(obj):
        # sub functions
        def get_docstring(prefix, class_name, member_name=None, use_diff_lib=True):
            if member_name is None:
                doc_key = f"{prefix}:{class_name}"
            else:
                doc_key = f"{prefix}:{class_name}.{member_name}"
            docstring = doc_map.get(doc_key, None)
            if docstring is None:
                docstring = doc_map.get(doc_key.replace(" ", ""), None)
                if docstring is None and use_diff_lib:
                    filter_name_keys = [x for x in doc_map.keys() if x.startswith(f"{prefix}:{class_name}")]
                    best_matches = get_close_matches(doc_key, filter_name_keys, n=2, cutoff=0.9)
                    if best_matches:
                        docstring = doc_map.get(best_matches[0], None)
        
            return docstring if docstring is not None else "No description available."
    
        ############ MAIN ############
        try:
            cls = obj.GetType()
        except:
            try:
                cls = clr.GetClrType(obj)
            except:
                raise Exception("Error, the input object is not a Net Object")
        #
        assembly = System.Reflection.Assembly.GetAssembly(cls)
        doc_xml_path = os.path.splitext(assembly.Location)[0] + ".xml"
        doc_map = {}
        #
        try:
            if os.path.exists(doc_xml_path):
                tree = ET.parse(doc_xml_path)
                root = tree.getroot()
                for member in root.findall(".//member"):
                    member_name = member.get('name')
                    if ".#ctor" in member.get('name'):
                        member_name = member_name.replace(".#ctor", "").strip()
                        if not member_name.endswith(")"):
                            member_name += "()"
                    summary_node = member.find("summary")
                    if member_name and summary_node is not None and summary_node.text:
                        # Clean up whitespace from the docstring
                        doc_map[member_name] = ' '.join(summary_node.text.strip().split())
                        if "System." in member_name:
                            alternative_member_name = member_name.replace("System.", "")
                            doc_map[alternative_member_name] = ' '.join(summary_node.text.strip().split())
        except Exception as e:
            # If XML fails, we can't get docs, but the script can still run
            pass
       
        flags =  BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly
        
        markdown_lines = []
        # Class Name and Docstring
        markdown_lines.append(f"## {cls.FullName}")
        class_doc_key = f"T:{cls.FullName}"
        class_doc = doc_map.get(class_doc_key, "")
        if class_doc:
            markdown_lines.append(f"*{class_doc}*")
        # add inheritance
        inheritance_types = f"- Inheritance Hierarchy : {cls.FullName}"
        current = cls.BaseType
        for _ in range(10):
          if hasattr(current, "FullName") and not inheritance_types.endswith(current.FullName):
              inheritance_types += " -> " + current.FullName
              current = current.BaseType
        #
        markdown_lines.append(inheritance_types)
        markdown_lines.append("") # Add a blank line for spacing
        
        # --- Process constructors ---
        ctor_infos = cls.GetConstructors (flags)
        if ctor_infos:
            markdown_lines.append("### Constructors")
            for ctor_info in sorted(ctor_infos, key=lambda x: x.Name):
                constructor_name = ctor_info.ToString().replace("Void .ctor", cls.FullName).strip()
                docstring = get_docstring("M", constructor_name)
                markdown_lines.append(f"- `{constructor_name}` — {docstring}")
            markdown_lines.append("")
            
        # --- Process Methods ---
        methods = cls.GetMethods(flags)
        if methods:
            markdown_lines.append("### Methods")
            for method in sorted(methods, key=lambda x: x.Name):
                if not method.Name.startswith(("get_","set_", "op_")):
                    b_method_name = method.ToString()
                    return_value = "Void"
                    is_static = "static " if method.IsStatic else ""
                    check_patern = re.match(r"^(.+?)\s(.*\(.*\))$", b_method_name)
                    if check_patern is not None:
                        return_value = check_patern.group(1)
                        method_name = check_patern.group(2)
                    docstring = get_docstring("M", cls.FullName, method_name)
                    markdown_lines.append(f"- `{is_static}{method_name}` → `{return_value}` — {docstring}")
            markdown_lines.append("")
        
        # --- Process Properties ---
        properties = cls.GetProperties(flags)
        if properties:
            markdown_lines.append("### Properties")
            for prop in sorted(properties, key=lambda x: x.Name):
                docstring = get_docstring("P", cls.FullName, prop.Name)
                markdown_lines.append(f"- `{prop.Name}` — {docstring}")
            markdown_lines.append("")
            
        # --- Process Enumerations ---
        if cls.IsEnum:
            enums = cls.GetEnumNames()
            if enums:
                markdown_lines.append("### Enumerations")
                for enum in sorted(enums):
                    docstring = get_docstring("F", cls.FullName, enum)
                    markdown_lines.append(f"- `{enum}`— {docstring}")
                markdown_lines.append("")
        return "\n".join(markdown_lines)

OUT = NetType_Utils

more infos here and here

2 Likes