Python Script using Alignment.PointLocation() with ref parameters (Usually in C# or .Net)

Hi,

I’m trying to figure out why the .PointLocation() method in the Alignment Class doesn’t return the intended values
( Help )
Is it because ref variables in parameters aren’t working the same way in Python ?

As of now; I only keep on having the values when I initiliaze the variables.

Here’s a paste of my code (Line 241) :

import clr
import time
import unicodedata  # For normalizing special characters

# Add references to AutoCAD and Civil 3D assemblies
clr.AddReference('AcMgd')
clr.AddReference('AcCoreMgd')
clr.AddReference('AcDbMgd')
clr.AddReference('AecBaseMgd')
clr.AddReference('AecPropDataMgd')
clr.AddReference('AeccDbMgd')

# Import necessary namespaces
import Autodesk
from Autodesk.AutoCAD.ApplicationServices import *
from Autodesk.AutoCAD.DatabaseServices import *
from Autodesk.Civil.ApplicationServices import *
from Autodesk.Civil.DatabaseServices import *
from Autodesk.Aec.DatabaseServices import *
import math
import System.Reflection

# Input from Dynamo
doc = IN[0]
global easting_out
global northing_out
global bearing

# Function to get bearing in radians
def get_bearing(pipe):
    start_point = pipe.StartPoint
    end_point = pipe.EndPoint
    dx = end_point.X - start_point.X
    dy = end_point.Y - start_point.Y
    return math.atan2(dy, dx)

# Function to convert radians to degrees
def rad_to_deg(radians):
    return radians * 180 / math.pi

# Function to normalize angle to [0, 360)
def normalize_angle(angle_deg):
    normalized = angle_deg - 360 * math.floor(angle_deg / 360.0)
    return normalized if normalized >= 0 else normalized + 360  # Ensure positive result

# Function to inspect object properties
def list_properties(obj):
    props = obj.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
    return [prop.Name for prop in props]

# Function to normalize description for consistent matching
def normalize_description(desc):
    if desc:
        # Normalize Unicode characters (e.g., "É" to "E") and convert to uppercase
        return unicodedata.normalize('NFKD', desc).encode('ASCII', 'ignore').decode('ASCII').upper()
    return ""

# Function to determine if structure is connected at the beginning or end of a pipe
def is_connected_at_start(structure, pipe):
    struct_loc = structure.Location
    dist_start = ((pipe.StartPoint.X - struct_loc.X)**2 + (pipe.StartPoint.Y - struct_loc.Y)**2)**0.5
    dist_end = ((pipe.EndPoint.X - struct_loc.X)**2 + (pipe.EndPoint.Y - struct_loc.Y)**2)**0.5
    return dist_start < dist_end  # True if closer to start, False if closer to end

# Function to extract the angle from COUDE description
def extract_coude_angle(desc):
    try:
        # Split the description (e.g., "COUDE 45") and get the angle part
        angle_str = desc.split()[-1]  # Last part should be the angle (e.g., "45")
        return float(angle_str)  # Convert to float (e.g., 45.0, 22.5)
    except (IndexError, ValueError) as e:
        # Default to 0 if parsing fails
        return 0.0

# Main processing
output = []

try:
    active_doc = Application.DocumentManager.MdiActiveDocument
    db = active_doc.Database
    civil_doc = CivilApplication.ActiveDocument
    network_ids = civil_doc.GetPipeNetworkIds()

    # Lock the document for writing
    doc_lock = None
    try:
        doc_lock = active_doc.LockDocument()
        output.append("Document locked for writing")

        # Use a single transaction for all networks
        with db.TransactionManager.StartTransaction() as t:
            for network_id in network_ids:
                try:
                    pipe_network = t.GetObject(network_id, OpenMode.ForRead)
                    structure_ids = pipe_network.GetStructureIds()
                    pipe_ids = pipe_network.GetPipeIds()

                    # Map structures to connected pipes
                    structure_to_pipes = {struct_id: [] for struct_id in structure_ids}
                    for pipe_id in pipe_ids:
                        pipe = t.GetObject(pipe_id, OpenMode.ForRead)
                        start_struct_id = pipe.StartStructureId
                        end_struct_id = pipe.EndStructureId
                        if not start_struct_id.IsNull and start_struct_id in structure_to_pipes:
                            structure_to_pipes[start_struct_id].append(pipe)
                        if not end_struct_id.IsNull and end_struct_id in structure_to_pipes:
                            structure_to_pipes[end_struct_id].append(pipe)

                    # Process each structure
                    for struct_id in structure_ids:
                        structure = t.GetObject(struct_id, OpenMode.ForRead)
                        output.append(f"Structure ID {struct_id} type: {structure.GetType().FullName}")
                        if not isinstance(structure, Autodesk.Civil.DatabaseServices.Structure):
                            output.append(f"Object with ID {struct_id} is not a Structure")
                            continue

                        output.append(f"Properties of Structure ID {struct_id}: {list_properties(structure)}")
                        raw_desc = structure.Description if structure.Description else ""
                        desc = normalize_description(raw_desc)  # Normalize and uppercase the description
                        output.append(f"Structure {struct_id} raw description: {raw_desc}")  # Log raw description
                        output.append(f"Structure {struct_id} normalized description: {desc}")  # Log normalized description
                        connected_pipes = structure_to_pipes.get(struct_id, [])
                        pipe_count = len(connected_pipes)
                        output.append(f"Structure {struct_id} has {pipe_count} connected pipes")

                        bearings = []
                        if pipe_count > 0:
                            for pipe in connected_pipes:
                                bearing = get_bearing(pipe)
                                bearings.append(bearing)
                                output.append(f"Pipe bearing for Structure {struct_id}: {rad_to_deg(bearing)} degrees")
                        else:
                            output.append(f"Structure {struct_id}, Warning: No connected pipes, rotation will be set to 0° if applicable")

                        new_rotation = None
                        if desc == "VANNE" and pipe_count == 2 and abs(bearings[0] - bearings[1]) < 0.01:
                            new_rotation = bearings[0]
                        elif desc == "VANNE" and pipe_count != 2:
                            new_rotation = 0.0  # Default rotation if pipe count is not 2
                            output.append(f"Structure {struct_id}, VANNE has {pipe_count} pipes (expected 2), setting rotation to 0°")
                        elif desc == "T" and pipe_count == 3:
                            if abs(bearings[0] - bearings[1]) < 0.01 and abs(bearings[0] - bearings[2]) > 0.01:
                                new_rotation = bearings[2]
                            elif abs(bearings[1] - bearings[2]) < 0.01 and abs(bearings[0] - bearings[1]) > 0.01:
                                new_rotation = bearings[0]
                            elif abs(bearings[0] - bearings[2]) < 0.01 and abs(bearings[1] - bearings[0]) > 0.01:
                                new_rotation = bearings[1]
                        elif desc == "T" and pipe_count != 3:
                            new_rotation = 0.0  # Default rotation if pipe count is not 3
                            output.append(f"Structure {struct_id}, T has {pipe_count} pipes (expected 3), setting rotation to 0°")
                        elif desc in ["BOUCHON", "REDUIT", "MANCHON"] and pipe_count in [1, 2]:
                            if pipe_count == 1:
                                pipe = connected_pipes[0]
                                is_start = is_connected_at_start(structure, pipe)
                                bearing = bearings[0]
                                if desc == "BOUCHON":
                                    new_rotation = bearing if is_start else bearing - math.pi  # -180° if at end
                                    output.append(f"Structure {struct_id}, BOUCHON connected at {'start' if is_start else 'end'}, setting rotation to {'bearing' if is_start else 'bearing - 180°':} {rad_to_deg(new_rotation)} degrees")
                                else:
                                    new_rotation = bearing  # Default for REDUIT/MANCHON
                            elif abs(bearings[0] - bearings[1]) < 0.01:
                                new_rotation = bearings[0]
                        elif desc in ["BOUCHON", "REDUIT", "MANCHON"] and pipe_count not in [1, 2]:
                            new_rotation = 0.0  # Default rotation if pipe count is not 1 or 2
                            output.append(f"Structure {struct_id}, {desc} has {pipe_count} pipes (expected 1 or 2), setting rotation to 0°")
                        elif desc == "POTEAU INCENDIE" and pipe_count == 1:
                            new_rotation = bearings[0]  # Align with the pipe bearing directly
                            output.append(f"Structure {struct_id}, POTEAU INCENDIE setting rotation to pipe bearing: {rad_to_deg(new_rotation)} degrees")
                        elif desc == "POTEAU INCENDIE" and pipe_count != 1:
                            new_rotation = 0.0  # Default rotation if pipe count is not 1
                            output.append(f"Structure {struct_id}, POTEAU INCENDIE has {pipe_count} pipes (expected 1), setting rotation to 0°")
                        elif desc in ["COUDE 11", "COUDE 22.5", "COUDE 45", "COUDE 90"] and pipe_count == 2:
                            pipe0, pipe1 = connected_pipes
                            struct_loc = structure.Location
                            dist_start0 = ((pipe0.StartPoint.X - struct_loc.X)**2 + (pipe0.StartPoint.Y - struct_loc.Y)**2)**0.5
                            dist_end0 = ((pipe0.EndPoint.X - struct_loc.X)**2 + (pipe0.EndPoint.Y - struct_loc.Y)**2)**0.5
                            pipe0_is_start = dist_start0 < dist_end0
                            dist_start1 = ((pipe1.StartPoint.X - struct_loc.X)**2 + (pipe1.StartPoint.Y - struct_loc.Y)**2)**0.5
                            dist_end1 = ((pipe1.EndPoint.X - struct_loc.X)**2 + (pipe1.EndPoint.Y - struct_loc.Y)**2)**0.5
                            pipe1_is_start = dist_start1 < dist_end1
                            incoming_bearing = bearings[0] if pipe0_is_start else bearings[1]
                            outgoing_bearing = bearings[1] if pipe0_is_start else bearings[0]

                            output.append(f"Structure {struct_id}, Incoming bearing (degrees): {rad_to_deg(incoming_bearing)}")
                            output.append(f"Structure {struct_id}, Outgoing bearing (degrees): {rad_to_deg(outgoing_bearing)}")

                            # Calculate the angle difference (normalized to [-180°, 180°])
                            angle_diff = normalize_angle(rad_to_deg(outgoing_bearing) - rad_to_deg(incoming_bearing))
                            if angle_diff > 180:
                                angle_diff -= 360  # Normalize to [-180°, 180°]
                            elif angle_diff < -180:
                                angle_diff += 360

                            # Check for near-straight pipes
                            coude_angle = extract_coude_angle(desc)  # Extract angle (e.g., 11, 22.5, 45, 90)
                            if abs(angle_diff) < 1.0:  # Near-straight (less than 1° difference)
                                new_rotation = incoming_bearing
                                output.append(f"Structure {struct_id}, {desc} pipes are near-straight (angle diff {angle_diff}°), setting rotation to incoming bearing: {rad_to_deg(new_rotation)} degrees")
                            else:
                                if angle_diff < 0:  # Outgoing turns left (counterclockwise)
                                    new_rotation = incoming_bearing + math.pi - math.radians(coude_angle)  # +180° - COUDE angle
                                    output.append(f"Structure {struct_id}, {desc} outgoing goes left, setting rotation to incoming + 180° - {coude_angle}°: {rad_to_deg(new_rotation)} degrees")
                                else:  # Outgoing turns right (clockwise)
                                    new_rotation = outgoing_bearing - math.radians(coude_angle)
                                    output.append(f"Structure {struct_id}, {desc} outgoing goes right, setting rotation to outgoing - {coude_angle}°: {rad_to_deg(new_rotation)} degrees")

                            # Force a 1° change if rotation matches current, with guard clause
                            current_rotation = structure.Rotation
                            current_deg = normalize_angle(rad_to_deg(current_rotation))
                            new_deg = normalize_angle(rad_to_deg(new_rotation))
                            if abs(current_deg - new_deg) < 1.0:  # Use degrees for comparison
                                new_rotation += math.radians(1)  # Add 1 degree
                                output.append(f"Structure {struct_id}, Forcing rotation change by +1° due to match with current rotation")
                        elif desc in ["COUDE 11", "COUDE 22.5", "COUDE 45", "COUDE 90"] and pipe_count != 2:
                            new_rotation = 0.0  # Default rotation if pipe count is not 2
                            output.append(f"Structure {struct_id}, {desc} has {pipe_count} pipes (expected 2), setting rotation to 0°")
                            
                        elif desc.upper().find("PUISARD") != -1:
                            output.append(f"Structure {struct_id} matched as PUISARD-type structure with description: {raw_desc}")
                            alignment_id = getattr(structure, 'RefAlignmentId', None)  # Get the alignment ID
                            output.append(f"Structure {struct_id}, RefAlignmentId: {alignment_id}")  # Debug alignment ID
                            if alignment_id and not alignment_id.IsNull:
                                alignment = t.GetObject(alignment_id, OpenMode.ForRead)
                                struct_loc = structure.Location  # Get the structure's 3D point (X, Y, Z)
                                easting = struct_loc.X  # Use X as Easting
                                northing = struct_loc.Y  # Use Y as Northing
                                tolerance = 10000.0  # Tolerance in drawing units (e.g., 5 meters or 5 feet)

                                try:
                                    # First, project the structure's coordinates onto the alignment to get station and offset
                                    station = structure.Station
                                    offset = structure.Offset                                    
                                    output.append(f"Structure {struct_id}, Projected Station: {station}, Offset: {offset}")

                                    # Now use the station and offset to get the point and bearing
                                    easting_out = 0.0
                                    northing_out = 0.0
                                    bearing = 0.0
                                    # easting_out, northing_out, bearing = alignment.PointLocation(station, offset, tolerance, easting_out, northing_out, bearing)
                                    # easting_out, northing_out, bearing = alignment.PointLocation(station, offset, tolerance)
                                    alignment.PointLocation(station, offset, tolerance, easting_out, northing_out, bearing)
                                    output.append(f"Structure {struct_id}, Computed Easting: {easting_out}, Northing: {northing_out}, Bearing: {rad_to_deg(bearing)} degrees")
                                    new_rotation = bearing  # Set rotation to the alignment's bearing (in radians)
                                except Exception as e:
                                    output.append(f"Structure {struct_id}, Error using PointLocation : {str(e)}")
                                    new_rotation = 0.0  # Fallback to 0° if projection fails
                                    output.append(f"Structure {struct_id}, Failed to project onto alignment, setting rotation to 0°")
                            else:
                                new_rotation = 0.0  # Fallback to 0° if no alignment
                                output.append(f"Structure {struct_id}, No valid alignment found, setting rotation to 0°")

                        # Apply rotation if calculated
                        if new_rotation is not None:
                            current_rotation = structure.Rotation
                            output.append(f"Structure {struct_id}, Current Rotation (degrees): {rad_to_deg(current_rotation)}")
                            # Normalize the new rotation for logging
                            normalized_new_rotation_deg = normalize_angle(rad_to_deg(new_rotation))
                            output.append(f"Structure {struct_id}, Description: {desc}, Setting Rotation (degrees): {normalized_new_rotation_deg}")

                            try:
                                structure.UpgradeOpen()
                                output.append(f"Applying rotation to Structure {struct_id}, WriteEnabled: {structure.IsWriteEnabled}")
                                structure.Rotation = new_rotation  # Explicitly set rotation
                                updated_rotation = structure.Rotation
                                output.append(f"Structure {struct_id}, Updated Rotation (degrees) after set: {rad_to_deg(updated_rotation)}")

                                # Attempt to force handle-grip update via PartData or BlockReference
                                if hasattr(structure, 'PartData'):
                                    part_data = structure.PartData
                                    output.append(f"Structure {struct_id}, PartData available: {part_data is not None}")
                                if hasattr(structure, 'BlockId') and not structure.BlockId.IsNull:
                                    block_ref = t.GetObject(structure.BlockId, OpenMode.ForWrite)
                                    output.append(f"Structure {struct_id}, BlockReference found, IsWriteEnabled: {block_ref.IsWriteEnabled}")
                                    if block_ref and isinstance(block_ref, BlockReference):
                                        output.append(f"Structure {struct_id} rotating BlockReference.Rotation to {rad_to_deg(new_rotation)}")
                                        block_ref.Rotation = new_rotation
                                        updated_block_rotation = block_ref.Rotation
                                        output.append(f"Structure {struct_id}, BlockReference Updated Rotation (degrees): {rad_to_deg(updated_block_rotation)}")
                                else:
                                    output.append(f"Structure {struct_id} has no valid BlockId, using Structure.Rotation only")

                            except Exception as upgrade_e:
                                output.append(f"Failed to upgrade or modify Structure {struct_id}: {str(upgrade_e)}")
                                raise

                    output.append(f"Finished processing network {network_id}")
                    active_doc.Editor.Regen()
                    active_doc.Editor.UpdateScreen()
                    time.sleep(1)  # Give Civil 3D time to update between networks

                except Exception as e:
                    output.append(f"Error for network {network_id}: {str(e)}")
                    raise

            output.append("Committing transaction for all networks")
            t.Commit()
            active_doc.Editor.Regen()
            active_doc.Editor.UpdateScreen()
            time.sleep(1)  # Final update

    finally:
        if doc_lock is not None:
            doc_lock.Dispose()
            output.append("Document lock disposed")

    active_doc.Database.CloseInput(True)
    active_doc.Editor.Regen()
    active_doc.Editor.UpdateScreen()
    time.sleep(1)  # Additional delay for display update

except Exception as e:
    output.append(f"Script failed: {str(e)}")

OUT = output

I’ve finnaly found why I wasn’t accessing properly the .PointLocation() method …

It returns a list in tuple :

point_location_outputs = alignment.PointLocation(station, offset, tolerance, easting_out, northing_out, bearing_out)

The list looks like this :

point_location_outputs[0] = null
point_location_outputs[1] = easting
point_location_outputs[2] = northing
point_location_outputs[3] = bearing

3 Likes