Issue with Line Creation Between Revit API and DesignScript

Hi All, @Mike.Buttery

Can anyone explain why I am getting a difference in the count of lines created using the Revit API compared to DesignScript (which, in this case, is correct), even though I used the same logic and inputs?

As an example I used the following inputs:

Lx = 20 m, Ly = 25 m, dx = 4 m, dy = 5 m, e = 2 m

Lines by DesignScript
import sys
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

Lx = 20
Ly = 25
dx = 4
dy = 5
e = 2

def create_Lines_along_X(lx, ly, d_x, e0):
    Start_pts_X = [Point.ByCoordinates(x, (-ly/2)-e0 , 0) for x in [i * dx - Lx / 2 for i in range(int(lx / d_x) + 1)]]
    End_pts_X = [Point.ByCoordinates(x, ly/2 + e0 , 0) for x in [i * dx - Lx / 2 for i in range(int(lx / d_x) + 1)]]
    if lx % dx != 0:
        Start_pts_X.append(Point.ByCoordinates(lx/2, (-ly/2)-e0 , 0))
        End_pts_X.append(Point.ByCoordinates(lx/2, ly/2 + e0 , 0))
    return [Line.ByStartPointEndPoint(start, end) for start, end in zip(Start_pts_X, End_pts_X)]
    
def create_Lines_along_Y(lx, ly, d_y, e0):
    Start_pts_Y = [Point.ByCoordinates((-lx/2)-e0, y, 0) for y in [i * dy - Ly / 2 for i in range(int(ly / d_y) + 1)]]
    End_pts_Y = [Point.ByCoordinates(lx/2 + e0, y, 0) for y in [i * dy - Ly / 2 for i in range(int(ly / d_y) + 1)]]
    if ly % dy != 0:
        Start_pts_Y.append(Point.ByCoordinates((-lx/2)-e0, ly/2 , 0))
        End_pts_Y.append(Point.ByCoordinates(lx/2 + e0, ly/2 , 0))
    
    return [Line.ByStartPointEndPoint(start, end) for start, end in zip(Start_pts_Y, End_pts_Y)]
    
Lines_X = create_Lines_along_X(Lx, Ly, dx, e)

Lines_Y = create_Lines_along_Y(Lx, Ly, dy, e)

OUT = Lines_X, Lines_Y
Lnes by Revit API
import clr
import sys

# Import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument

# Retrieve units
units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()


Lx = UnitUtils.ConvertToInternalUnits(20, units)  # Total length along X
Ly = UnitUtils.ConvertToInternalUnits(25, units)  # Total length along Y

# Define spaces along each axis
dx = UnitUtils.ConvertToInternalUnits(4, units)  # Space along X
dy = UnitUtils.ConvertToInternalUnits(5, units)  # Space along Y

# Define extension distance 
e = UnitUtils.ConvertToInternalUnits(2, units)

def create_Lines_along_X(lx, ly, d_x, e0):
    Start_pts_X = [XYZ(x, (-ly/2)-e0 , 0) for x in [i * dx - Lx / 2 for i in range(int(lx / d_x) + 1)]]
    End_pts_X = [XYZ(x, ly/2 + e0 , 0) for x in [i * dx - Lx / 2 for i in range(int(lx / d_x) + 1)]]
    if lx % dx != 0:
        Start_pts_X.append(XYZ(lx/2, (-ly/2)-e0 , 0))
        End_pts_X.append(XYZ(lx/2, ly/2 + e0 , 0))
    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_X, End_pts_X)]
    
def create_Lines_along_Y(lx, ly, d_y, e0):
    Start_pts_Y = [XYZ((-lx/2)-e0, y, 0) for y in [i * dy - Ly / 2 for i in range(int(ly / d_y) + 1)]]
    End_pts_Y = [XYZ(lx/2 + e0, y, 0) for y in [i * dy - Ly / 2 for i in range(int(ly / d_y) + 1)]]
    if ly % dy != 0:
        Start_pts_Y.append(XYZ((-lx/2)-e0, ly/2 , 0))
        End_pts_Y.append(XYZ(lx/2 + e0, ly/2 , 0))
    
    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_Y, End_pts_Y)]
    
Lines_X = create_Lines_along_X(Lx, Ly, dx, e)

Lines_Y = create_Lines_along_Y(Lx, Ly, dy, e)

OUT = Lines_X, Lines_Y

How to fix issue in Revit API?

Thanks.

Odd. I ran it in R2024. Got the same 12 in each,.

1 Like

@aaron_rumple

I’m using Revit 2023. What could be the issue in this case?

Thanks

You are using globals dx and dy in your functions instead of d_x and d_y
I would not use modulo with floating points (ie after unit conversion) as you can get results like this
100 % 100.0001 = 100.1
Use not math.isclose(lx, (lx // d_x) * d_x) // is floor division and returns an integer or whole number float
You can make the code a lot clearer and keep it to one function
Here is some python giving a different method of generating lines (unfortunately I’m off work so no Revit for me)
I’m also passing the line function into lines so you can test different results (although you will also have to pass a point function)

from math import isclose


class XYZ:
    def __init__(self, x=0, y=0, z=0):
        self.x = x
        self.y = y
        self.z = z

    def __repr__(self):
        return f"XYZ{self.x, self.y, self.z}"


class Line:
    def __init__(self, start=XYZ(), end=XYZ(1, 1, 1)):
        self.start = start
        self.end = end

    def __repr__(self):
        return f"Line(start={self.start}, end={self.end}"


def lines(h, w, d, e, y_axis=False, units=None, func=Line):
    def convert_unit(i, unit):
        return i * unit

    if units:
        h, w, d, e = map(lambda i: convert_unit(i, units), (h, w, d, e))

    if y_axis:
        h, w = w, h

    n = h // d  # // floor division
    ph = [d * i - h / 2 for i in range(int(n) + 1)]

    # check
    # safer than modulo which can return very close to d (not zero) with floats
    if not isclose(h, n * d):
        ph.append(h / 2)

    pw = [-w / 2 - e, w / 2 + e]

    if y_axis:
        return [func(*(XYZ(x, y, 0) for x in pw)) for y in ph]

    return [func(*(XYZ(x, y, 0) for y in pw)) for x in ph]


lx = 20
ly = 26
dx = 4
dy = 5
e = 2

print("X Lines m", *lines(lx, ly, dx, e), sep="\n")
print("\nY Lines m", *lines(lx, ly, dy, e, True), sep="\n")

print("\nX Lines ft", *lines(lx, ly, dx, e, units=3.28084), sep="\n")
print("\nY Lines ft", *lines(lx, ly, dy, e, True, units=3.28084), sep="\n")

# X Lines m
# Line(start=XYZ(-10.0, -15.0, 0), end=XYZ(-10.0, 15.0, 0)
# Line(start=XYZ(-6.0, -15.0, 0), end=XYZ(-6.0, 15.0, 0)
# Line(start=XYZ(-2.0, -15.0, 0), end=XYZ(-2.0, 15.0, 0)
# Line(start=XYZ(2.0, -15.0, 0), end=XYZ(2.0, 15.0, 0)
# Line(start=XYZ(6.0, -15.0, 0), end=XYZ(6.0, 15.0, 0)
# Line(start=XYZ(10.0, -15.0, 0), end=XYZ(10.0, 15.0, 0)

# Y Lines m
# Line(start=XYZ(-12.0, -13.0, 0), end=XYZ(12.0, -13.0, 0)
# Line(start=XYZ(-12.0, -8.0, 0), end=XYZ(12.0, -8.0, 0)
# Line(start=XYZ(-12.0, -3.0, 0), end=XYZ(12.0, -3.0, 0)
# Line(start=XYZ(-12.0, 2.0, 0), end=XYZ(12.0, 2.0, 0)
# Line(start=XYZ(-12.0, 7.0, 0), end=XYZ(12.0, 7.0, 0)
# Line(start=XYZ(-12.0, 12.0, 0), end=XYZ(12.0, 12.0, 0)
# Line(start=XYZ(-12.0, 13.0, 0), end=XYZ(12.0, 13.0, 0)

# X Lines ft
# Line(start=XYZ(-32.8084, -49.2126, 0), end=XYZ(-32.8084, 49.2126, 0)
# Line(start=XYZ(-19.68504, -49.2126, 0), end=XYZ(-19.68504, 49.2126, 0)
# Line(start=XYZ(-6.561679999999999, -49.2126, 0), end=XYZ(-6.561679999999999, 49.2126, 0)
# Line(start=XYZ(6.561680000000003, -49.2126, 0), end=XYZ(6.561680000000003, 49.2126, 0)
# Line(start=XYZ(19.68504, -49.2126, 0), end=XYZ(19.68504, 49.2126, 0)
# Line(start=XYZ(32.8084, -49.2126, 0), end=XYZ(32.8084, 49.2126, 0)

# Y Lines ft
# Line(start=XYZ(-39.37008, -42.65092, 0), end=XYZ(39.37008, -42.65092, 0)
# Line(start=XYZ(-39.37008, -26.24672, 0), end=XYZ(39.37008, -26.24672, 0)
# Line(start=XYZ(-39.37008, -9.84252, 0), end=XYZ(39.37008, -9.84252, 0)
# Line(start=XYZ(-39.37008, 6.5616799999999955, 0), end=XYZ(39.37008, 6.5616799999999955, 0)
# Line(start=XYZ(-39.37008, 22.96588, 0), end=XYZ(39.37008, 22.96588, 0)
# Line(start=XYZ(-39.37008, 39.37008, 0), end=XYZ(39.37008, 39.37008, 0)
# Line(start=XYZ(-39.37008, 42.65092, 0), end=XYZ(39.37008, 42.65092, 0)
2 Likes

First of all, I don’t understand why @aaron_rumple tested my code with the same inputs and got the correct line count, whereas I ended up with extra lines!?

Secondly, I’m completely lost and can’t figure out how to solve this issue, despite trying multiple options.

As you suggested, I used math.isclose instead of modulus in both the create_Lines_along_X and create_Lines_along_Y functions and replaced the for loop with a while loop as shown below, but my problem remains unresolved.

def create_Lines_along_X(lx, ly, d_x, e0):
    x = -lx / 2
    Start_pts_X, End_pts_X = [], []
    while x <= lx / 2:
        Start_pts_X.append(XYZ(x, (-ly / 2) - e0, 0))
        End_pts_X.append(XYZ(x, (ly / 2) + e0, 0))
        x += d_x
    if iscolose(x - d_x, lx / 2):
        Start_pts_X.append(XYZ(lx / 2, (-ly / 2) - e0, 0))
        End_pts_X.append(XYZ(lx / 2, (ly / 2) + e0, 0))
    
    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_X, End_pts_X)]

Lastly, I thought about using a remainder distance to simplify things. I tested the code with the inputs in the image below and, again, ended up with an extra line for lines // X direction

latest code tested
import clr
import sys

# Import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from math import isclose
doc = DocumentManager.Instance.CurrentDBDocument

# Retrieve units
units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()


Lx = UnitUtils.ConvertToInternalUnits(20, units)  # Total length along X
Ly = UnitUtils.ConvertToInternalUnits(25, units)  # Total length along Y

# Define spaces along each axis
dx = UnitUtils.ConvertToInternalUnits(4, units)  # Space along X
dy = UnitUtils.ConvertToInternalUnits(5, units)  # Space along Y

# Define extension distance 
e = UnitUtils.ConvertToInternalUnits(2, units)

def create_Lines_along_X(lx, ly, d_x, e0):
    x = -lx / 2
    Start_pts_X, End_pts_X = [], []
    
    while x < lx / 2:
        Start_pts_X.append(XYZ(x, (-ly / 2) - e0, 0))
        End_pts_X.append(XYZ(x, (ly / 2) + e0, 0))
        x += d_x
    
    # Calculate the remainder and handle the last line
    remainder = lx - (x - d_x)
    if remainder > 0:
        Start_pts_X.append(XYZ(lx / 2, (-ly / 2) - e0, 0))
        End_pts_X.append(XYZ(lx / 2, (ly / 2) + e0, 0))
    
    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_X, End_pts_X)]
    
def create_Lines_along_Y(lx, ly, d_y, e0):
    y = -ly / 2
    Start_pts_Y, End_pts_Y = [], []
    while y < ly / 2:
        Start_pts_Y.append(XYZ((-lx/2)-e0, y, 0))
        End_pts_Y.append(XYZ(lx/2 + e0, y, 0))
        y += d_y
    remainder = ly - (y - d_y)
    if remainder > 0:  # Correct the last point
        Start_pts_Y.append(XYZ((-lx/2)-e0, ly/2 , 0))
        End_pts_Y.append(XYZ(lx/2 + e0, ly/2 , 0))
    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_Y, End_pts_Y)]
    
Lines_X = create_Lines_along_X(Lx, Ly, dx, e)

Lines_Y = create_Lines_along_Y(Lx, Ly, dy, e)

OUT = Lines_X, Lines_Y, [l.ToProtoType() for l in Lines_X], [l.ToProtoType() for l in Lines_Y]

@c.poupin Have you an idea how to solve this issue?

Thanks.

Hey, why don’t you use Mr. Mike’s time
to answer your question

import sys
import clr
import math
from math import isclose
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import XYZ,Line
clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)


def lines(h, w, d, e, y_axis=False, units=None):
    def convert_unit(i, unit):
        return i * unit

    if units:
        h, w, d, e = map(lambda i: convert_unit(i, units), (h, w, d, e))

    if y_axis:
        h, w = w, h

    n = h // d  # // floor division
    ph = [d * i - h / 2 for i in range(int(n) + 1)]

    # check
    # safer than modulo which can return very close to d (not zero) with floats
    if not isclose(h, n * d):
        ph.append(h / 2)

    pw = [-w / 2 - e, w / 2 + e]

    if y_axis:
        return [Line.CreateBound(*(XYZ(x, y, 0) for x in pw)) for y in ph]

    return [Line.CreateBound(*(XYZ(x, y, 0) for y in pw)) for x in ph]


lx = 20
ly = 25
dx = 4
dy = 5
e = 2

a=lines(lx,ly,dx,e,units=1/0.3048)
b=lines(lx,ly,dy,e,units=1/0.3048)

OUT = [a,b,[a.ToProtoType() for a in a],[b.ToProtoType() for b in b]]


edit:
oups b=lines(lx,ly,dy,e,True,units=1/0.3048)
Sincerely
christian.stan

3 Likes

@christian.stan

Did you test my initial code? Did it work for you?

Although Mike’s solution seems perfect with an error tolerance of 0, I still can’t understand why @aaron_rumple tested my initial code and got the expected result, while it doesn’t work for me!

Anyway, as you can see in the code below, I made some changes to @Mike.Buttery’s solution by replacing the for loop with a while loop for points range calculations, while keeping the main logic.

import clr
import sys

# Import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from math import isclose
doc = DocumentManager.Instance.CurrentDBDocument

# Retrieve units
units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()


Lx = UnitUtils.ConvertToInternalUnits(57, units)  # Total length along X
Ly = UnitUtils.ConvertToInternalUnits(63, units)  # Total length along Y

# Define spaces along each axis
dx = UnitUtils.ConvertToInternalUnits(4.75, units)  # Space along X
dy = UnitUtils.ConvertToInternalUnits(4.35, units)  # Space along Y

# Define extension distance 
e = UnitUtils.ConvertToInternalUnits(2, units)


def create_lines(lx, ly, d, e, y_axis=False):
    """Create lines along X, Y axis """
    if y_axis:
        lx, ly = ly, lx
    n = lx // d
    points_X = []
    i = 0
    while i <= int(n):
        points_X.append(d * i - lx / 2)
        i += 1
    if not isclose(lx, n * d):
        points_X.append(lx / 2)
    points_Y = [-ly / 2 - e, ly / 2 + e]

    if y_axis:
        return [Line.CreateBound(*(XYZ(x, y, 0) for x in points_Y)) for y in points_X]

    return [Line.CreateBound(*(XYZ(x, y, 0) for y in points_Y)) for x in points_X]
    
lines_x = create_lines(Lx, Ly, dx, e)
lines_y = create_lines(Lx, Ly, dy, e, y_axis=True)

OUT = [l.ToProtoType() for l in lines_x], [l.ToProtoType() for l in lines_y]

Thanks.

1 Like