Create rebars for manhole

Hi All,

I created a script which generate rebars for a manhole as shown in the image below

I am currently seeking a more efficient logic and approach to reduce the length of my code. I attempted to iterate over the four walls using a main "for loop " and create curve loops (curveloop1, curveloop2, H_Loop) as described in my code, but it has proven challenging. Therefore, I request your assistance in achieving this.

Here my code:

Manhole_rebars
import math
import clr
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import *
from System.Collections.Generic import IList, List, Dictionary
from System import Array


uidoc = __revit__.ActiveUIDocument

results = clr.Reference[IntersectionResultArray]()
doc = uidoc.Document
units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()

# select all walls within the project
walls = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
width = walls[0].Width

# select all floors within the project
floors = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Floors).WhereElementIsNotElementType().ToElements()

# get floor width for the first floor
floor_width = floors[0].get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()

# select all rebars types within the project
rebar_Types = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar).WhereElementIsElementType().ToElements()

# get the rebar type "HA12 (Fe400)" and it's diameter
rebar_Type = [r for r in rebar_Types if Element.Name.GetValue(r) == "HA12 (Fe400)"][0]
rebar_diameter = rebar_Type.BarModelDiameter

# covers parameters    
c1 = width/2 + UnitUtils.ConvertToInternalUnits(0.05, units)
c2 = (floor_width - UnitUtils.ConvertToInternalUnits(0.05, units))
d2 = c2 + rebar_diameter
c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
# spacing parameter
space = UnitUtils.ConvertToInternalUnits(0.15, units)

# create a curveloop from the first wall with the shape "|__|"

wall1 = walls[0]
bbx1 = wall1.get_BoundingBox(None)
h1 = bbx1.Max.Z - bbx1.Min.Z
temp_line1 = wall1.Location.Curve
param1 = rebar_diameter/temp_line1.Length
param2 = 1 - param1
lineB = Line.CreateBound(temp_line1.Evaluate(param1, True), temp_line1.Evaluate(param2, True))
paramA = c1/temp_line1.Length
paramB = 1 - paramA
# create path for repartition of the second CurveLoop "curveloop2"
path1 = Line.CreateBound(temp_line1.Evaluate(paramA, True), temp_line1.Evaluate(paramB, True))
vectB = XYZ(0,0, 1)
lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-c2)))
vectZ = XYZ(0,0, h1 + c2)
lineA = Line.CreateBound(lineB.GetEndPoint(0).Add(vectZ), lineB.GetEndPoint(0))
lineC = Line.CreateBound(lineB.GetEndPoint(1), lineB.GetEndPoint(1).Add(vectZ))
# curveloop1 obtained
curveloop1 = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
# Translate the curveloop1 to the start position before doing a multiple offset
plane1 = curveloop1.GetPlane()
trans1 = Transform.CreateTranslation(plane1.Normal.Negate().Multiply(c1))
curveloop1.Transform(trans1)

# create a curveloop from the second wall with the shape "|__|"

wall2 = walls[1]
bbx2 = wall2.get_BoundingBox(None)
h2 = bbx1.Max.Z - bbx1.Min.Z
temp_line2 = wall2.Location.Curve
param3 = rebar_diameter/temp_line2.Length
param4 = 1 - param1
lineE = Line.CreateBound(temp_line2.Evaluate(param3, True), temp_line2.Evaluate(param4, True))
paramC = c1/temp_line2.Length
paramD = 1 - paramC
path2 = Line.CreateBound(temp_line2.Evaluate(paramC, True), temp_line2.Evaluate(paramD, True))
vectE = XYZ(0,0, 1)
lineE = lineE.CreateTransformed(Transform.CreateTranslation(vectE.Multiply(-d2)))
vectZ = XYZ(0,0, h2 + c2)
lineD = Line.CreateBound(lineE.GetEndPoint(0).Add(vectZ), lineE.GetEndPoint(0))
lineF = Line.CreateBound(lineE.GetEndPoint(1), lineE.GetEndPoint(1).Add(vectZ))
# curveloop1 obtained
curveloop2 = CurveLoop.Create(List[Curve]([lineD, lineE, lineF]))
# Translate the curveloop2 to the start position before doing a multiple offset
plane2 = curveloop2.GetPlane()
trans2 = Transform.CreateTranslation(plane2.Normal.Negate().Multiply(c1))
curveloop2.Transform(trans2)
#                                                 __
#Create an horizontal CurveLoop as a rectangle "|__|"
line1 = temp_line1
end_pt1 = line1.GetEndPoint(1)
L = line1.Length
norm1 = line1.Direction.Normalize()
vect1 = (XYZ(end_pt1.X, end_pt1.Y, end_pt1.Z + L)).Subtract(end_pt1)
line2 = Line.CreateBound(end_pt1, end_pt1.Add((vect1).CrossProduct(norm1)))
end_pt2 = line2.GetEndPoint(1)
norm2 = line2.Direction.Normalize()
vect2 = (XYZ(end_pt2.X, end_pt2.Y, end_pt2.Z + L)).Subtract(end_pt2)
line3 = Line.CreateBound(end_pt2, end_pt2.Add((vect2).CrossProduct(norm2)))
end_pt3 = line3.GetEndPoint(1)
norm3 = line3.Direction.Normalize()
vect3 = (XYZ(end_pt3.X, end_pt3.Y, end_pt3.Z + L)).Subtract(end_pt3)
line4 = Line.CreateBound(end_pt3, end_pt3.Add((vect3).CrossProduct(norm3)))
# Horizontal curveloop "H_Loop" obtained
H_Loop = CurveLoop.Create(List[Curve]([line1, line2, line3, line4]))
# Translate the curveloop2 to the start position before doing a multiple offset
H_plane = H_Loop.GetPlane()
H_trans = Transform.CreateTranslation(H_plane.Normal.Multiply(c3))
H_Loop.Transform(H_trans)
# create path for repartition of the horizontal CurveLoop
temp_pt = temp_line1.GetEndPoint(0)
path3 = Line.CreateBound(XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + c3), XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + h1 - c3))

# define a function to create a multiple translation of a curveloop
def curveloop_Transform(curveloop, path, space):
    n = path.Length // space
    loop = [curveloop]
    if curveloop.GetPlane().Normal.IsAlmostEqualTo(XYZ.BasisZ):
        offset = loop[0].GetPlane().Normal.Multiply(space)
        xform = Transform.CreateTranslation(offset)
    else:
        offset = loop[0].GetPlane().Normal.Negate().Multiply(space)
        xform = Transform.CreateTranslation(offset)
    count = 0
    while count < n:
        loop.append(loop[-1].CreateViaTransform(loop[-1], xform))
        count += 1
    return(loop)
# Multiple translation of the the curveloop1    
loop1 = curveloop_Transform(curveloop1, path2, space)
# Multiple translation of the the curveloop2
loop2 = curveloop_Transform(curveloop2, path1, space)
# Multiple translation of the the curveloop3
loop3 = curveloop_Transform(H_Loop, path3, space)

# create Array Lists from Lists Curveloop
H_arrayIlist = List[CurveLoop]()
V_arrayIlist1 = List[CurveLoop]()
V_arrayIlist2 = List[CurveLoop]()

for i in loop3:
    H_arrayIlist.Add(CurveLoop.Create([j for j in i]))

for i in loop1:
   V_arrayIlist1.Add(CurveLoop.Create([j for j in i]))

for i in loop2:
   V_arrayIlist2.Add(CurveLoop.Create([j for j in i]))
    
# create rebars
with Transaction(doc, 'create rebars') as t :
    t.Start()
    validationResult = clr.Reference[RebarFreeFormValidationResult]()
    rebar1 = Rebar.CreateFreeForm(doc, rebar_Type, floors[0], H_arrayIlist, validationResult)
    rebar2 = Rebar.CreateFreeForm(doc, rebar_Type, floors[0], V_arrayIlist1, validationResult)
    rebar3 = Rebar.CreateFreeForm(doc, rebar_Type, floors[0], V_arrayIlist2, validationResult)
    t.Commit()

Thanks.

Hi @REDO_79, sorry if no one helped you yet


Im not a rebar expert, and I can be totally wrong, but isn’t this something you can do with a rebar set?

If this is not the case, here are some tips:

The first two curve loops are pretty similar, so you can create a function and call it 2 times with different parameters.
One thing: youre using the first bounding box even for the height of second wall, is this wanted or a typo? In the firt case, assuming the heights are the same, you can save some computing time by removing the calculation of bbx2 and pass the height to the function instead

The creation of lines 2 to 4 of the horizontal curve loop is repeated code, so you can extract a function that creates the line with the previous one as an input argument and call it 3 times.

Instead of a while loop, you can use for count in range(n).

The “array list” (what does it mean? They are .Net lists, not arrays) creation section could be reduced to this function:

def build_loops(curveloop, path, space)
    loops = curveloop_transform(curveloop, path, space)
    return List[CurveLoop]([CurveLoop.Create(loop) for loop in loops])
1 Like

@ErikFrits
Sorry, @sanzoghenzo , for this delayed feedback. I was traveling and didn’t have the opportunity to use my PC or access the PyRevit forum

In fact, before you replied, I was working on completing my tank reinforcement tools by implementing a user interface using WPF. I made sure to incorporate your remarks to optimize my script, and now I would appreciate your comments and suggestions to further improve the final script attached below

xaml _Tank_rebar
<?xml version="1.0" encoding="utf-8"?>
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	Title="Window1"
	Height="350"
	Width="330">
	<Grid>
		<Label
			VerticalAlignment="Top"
			HorizontalAlignment="Left"
			Grid.Row="0"
			Grid.Column="0"
			Margin="25,25,0,0"
			Height="30"
			Width="135"
			Content="Enrobage Mur : e1 (cm)" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Width="100"
			x:Name="e1_value"
			Margin="0,25,25,0"
			Height="30" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Margin="25,75,0,0"
			Height="30"
			Content="Enrobage Radier : e2 (cm)"
			Width="145" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Width="100"
			x:Name="e2_value"
			Margin="0,75,25,0"
			Height="30" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Margin="25,177,0,0"
			Height="30"
			Content="DiamĂštre de barre d (mm)"
			Width="150" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Width="125"
			Height="29"
			Margin="25,125,0,0"
			Content="Espacement : s (cm)" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Height="30"
			Margin="0,125,25,0"
			Width="100"
			x:Name="spacing_value" />
		<ComboBox
			x:Name="select_rebar"
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Margin="0,177,25,0"
			Width="100"
			Height="30" />
		<Button
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Width="90"
			Height="25"
			Margin="25,0,0,20"
			Content="OK"
			x:Name="OK_button"
			Click="OK_Clicked" />
		<Button
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Bottom"
			Height="25"
			Width="90"
			Margin="0,0,25,20"
			Content="Cancel"
			x:Name="Cancel_button"
			Click="Cancel_Clicked" />		
	</Grid>
</Window>
Tank rebar script
# -*- coding: UTF-8 -*-
from pyrevit import forms
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
from System import Array
from System.Collections.Generic import List, IList, Dictionary
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
results = clr.Reference[IntersectionResultArray]()

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

# select all walls within the project
walls = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
width = walls[0].Width

# select all floors within the project
floors = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Floors).WhereElementIsNotElementType().ToElements()

# get floor width for the first floor
floor_width = floors[0].get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()

# select all rebars types within the project
rebar_Types = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Rebar).WhereElementIsElementType().ToElements()

script_path = os.path.dirname(__file__)

class manhole_rebar(Window):
    def __init__(self):
        xaml_path = os.path.join(script_path, 'ferraillage.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.rebar_diameters = [10, 12, 14, 16, 20, 25, 32, 40]
        self.select_rebar.ItemsSource = self.rebar_diameters
        self.data = {}
        self.ShowDialog()

    def diameter(self):
        if self.select_rebar.SelectedItem:
            return self.select_rebar.SelectedItem

    def collect_input_data(self):
        """Collect and convert input data from the UI"""
        try:
            # Attempt to convert e1 and e2 inputs to internal units
            e1 = UnitUtils.ConvertToInternalUnits(float(self.e1_value.Text), UnitTypeId.Centimeters)
            print(e1)
            e2 = UnitUtils.ConvertToInternalUnits(float(self.e2_value.Text), UnitTypeId.Centimeters)
            s = UnitUtils.ConvertToInternalUnits(float(self.spacing_value.Text), UnitTypeId.Centimeters)
            D = self.diameter()
            self.data = {"E1": e1, "E2": e2, "S": s, "d": D}

            if self.data["d"] is None:
                forms.alert("Please select a rebar diameter: d")
                return None
            return self.data

        except ValueError:
            # Identify which key has missing or invalid input and alert the user
            if not self.e1_value.Text:
                forms.alert("Please enter a valid value for : e1")
            elif not self.e2_value.Text:
                forms.alert("Please enter a valid value for : e2")
            elif not self.spacing_value.Text:
                forms.alert("Please enter a valid value for : s")
            else:
                forms.alert("Invalid input detected. Please ensure all inputs are numeric where required.")

            return None

    def rebar_Type(self):
        rebar_Type = [r for r in rebar_Types if Element.Name.GetValue(r) == "HA{} (Fe400)".format(self.data["d"])][0]
        return rebar_Type
    # cover parameters
    def covers(self):
        c1 = width/ 2 + self.data["E1"]
        c2 = floor_width - self.data["E2"]
        d2 = c2 - UnitUtils.ConvertToInternalUnits(self.data["d"], UnitTypeId.Millimeters)
        c3 = UnitUtils.ConvertToInternalUnits(0.05, units)

        return c1, c2, d2, c3
    # curveloop function to create the vertical rebar shape "|_| "
    def V_curveloop_create(self, wall, cover):
        c1, c2, d2, c3 = cover
        bbx = wall.get_BoundingBox(None)
        h = bbx.Max.Z - bbx.Min.Z
        temp_line = wall.Location.Curve
        p1 = -(d2-c2) / temp_line.Length
        p2 = 1 - p1
        pA = c1 / temp_line.Length
        pB = 1 - pA
        lineB = Line.CreateBound(temp_line.Evaluate(p1, True), temp_line.Evaluate(p2, True))
        vectB = XYZ(0, 0, 1)
        if wall == walls[0]:
            lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-c2)))
        else:
            lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-d2)))
        vectZ = XYZ(0, 0, h + c2)
        lineA = Line.CreateBound(lineB.GetEndPoint(0).Add(vectZ), lineB.GetEndPoint(0))
        lineC = Line.CreateBound(lineB.GetEndPoint(1), lineB.GetEndPoint(1).Add(vectZ))
        # curveloop1 obtained
        curveloop = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
        # Translate the curveloop1 to the start position before doing a multiple offset
        plane = curveloop.GetPlane()
        trans = Transform.CreateTranslation(plane.Normal.Negate().Multiply(c1))
        curveloop.Transform(trans)
        return curveloop

    # curveloop function to create the horizontal rebar shape as a rectangle
    def H_curveloop_create(self, walls, cover):
        c1, c2, d2, c3 = cover
        points = []
        L1 = walls[0].Location.Curve.Length
        L2 = walls[1].Location.Curve.Length
        pt1 = walls[0].Location.Curve.GetEndPoint(0)
        points.append(pt1)
        pt2 = walls[0].Location.Curve.GetEndPoint(1)
        points.append(pt2)
        vect = (XYZ(pt2.X, pt2.Y, pt2.Z + L2)).Subtract(pt1)
        cross_direction = vect.CrossProduct(walls[0].Location.Curve.Direction).Normalize()
        pt3 = pt2 + cross_direction.Multiply(L2)
        points.append(pt3)
        pt4 = pt3 + walls[0].Location.Curve.Direction.Negate().Multiply(L1)
        points.append(pt4)
        lines = [Line.CreateBound(points[i], points[i + 1]) for i in range(len(points)-1)]
        lines.append(Line.CreateBound(pt4, pt1))
        # Horizontal curveloop "H_Loop" obtained
        H_Loop = CurveLoop.Create(lines)
        # Translate the curveloop2 to the start position before doing a multiple offset
        H_plane = H_Loop.GetPlane()
        H_trans = Transform.CreateTranslation(H_plane.Normal.Multiply(c3))
        H_Loop.Transform(H_trans)
        return H_Loop

    # define path for vertical rebar distribution
    def V_path(self, wall, cover):
        c1, c2, d2, c3 = cover
        if wall == walls[0]:
            temp_line = walls[1].Location.Curve
            pA = c1 / temp_line.Length
            pB = 1 - pA
            path = Line.CreateBound(temp_line.Evaluate(pA, True), temp_line.Evaluate(pB, True))
        else:
            temp_line = walls[0].Location.Curve
            pA = c1 / temp_line.Length
            pB = 1 - pA
            path = Line.CreateBound(temp_line.Evaluate(pA, True), temp_line.Evaluate(pB, True))
        return path

    # define path for horizontal rebar distribution
    def H_path(self, wall, cover):
        c1, c2, d2, c3 = cover
        bbx = wall.get_BoundingBox(None)
        h = bbx.Max.Z - bbx.Min.Z
        temp_pt = wall.Location.Curve.GetEndPoint(0)
        path = Line.CreateBound(XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + c3), XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + h - c3))
        return path

    # define a function to create a multiple translation of a curveloop
    def curveloop_Transform(self, curveloop, path):
        n = path.Length // self.data["S"]
        loop = []
        if curveloop.GetPlane().Normal.IsAlmostEqualTo(XYZ.BasisZ):
            offset = curveloop.GetPlane().Normal.Multiply(self.data["S"])
        else:
            offset = curveloop.GetPlane().Normal.Negate().Multiply(self.data["S"])
        xform = Transform.CreateTranslation(offset)
        loop.append(curveloop)
        count = 0
        while count < n:
            transformed_curves = [c.CreateTransformed(xform) for c in loop[-1]]
            transformed_loop = CurveLoop.Create(transformed_curves)
            loop.append(transformed_loop)
            count += 1
        return List[CurveLoop](loop)

    def OK_Clicked(self, sender, e):
        data = self.collect_input_data()
        type = self.rebar_Type()
        cover = self.covers()
        if not data:
            return
        self.Manhole_rebar(type, cover)
        self.Close()
    # Main function to create rebars
    def Manhole_rebar(self, type, cover):
        c1, c2, d2, c3 = cover
        # create the vertical curveloop "curveloop1"
        curveloop1 = self.V_curveloop_create(walls[0], cover)
        # Path distribution for "curveloop1"
        path1 = self.V_path(walls[1], cover)
        # create the vertical curveloop "curveloop2"
        curveloop2 = self.V_curveloop_create(walls[1], cover)
        # Path distribution for "curveloop2"
        path2 = self.V_path(walls[0], cover)
        # create the horizontal curveloop "curveloop3"
        curveloop3 = self.H_curveloop_create(walls, cover)
        # Path distribution for "curveloop3"
        path3 = self.H_path(walls[0], cover)
        # Multiple translation of the the curveloop1
        loop1 = self.curveloop_Transform(curveloop1, path2)
        # Multiple translation of the the curveloop2
        loop2 = self.curveloop_Transform(curveloop2, path1)
        # Multiple translation of the the curveloop3
        loop3 = self.curveloop_Transform(curveloop3, path3)

        # create rebars
        with Transaction(doc, 'create rebars') as t:
            t.Start()
            validationResult = clr.Reference[RebarFreeFormValidationResult]()
            rebar1 = Rebar.CreateFreeForm(doc, type, floors[0], loop1, validationResult)
            rebar2 = Rebar.CreateFreeForm(doc, type, floors[0], loop2, validationResult)
            rebar3 = Rebar.CreateFreeForm(doc, type, floors[0], loop3, validationResult)
            t.Commit()

    def Cancel_Clicked(self, sender, e):
        self.Close()
        
r1 = manhole_rebar()

If you want to test the rebar tool, you must first create the tank using the tool below. (Note: make sure to select the structure template for the rebar tool to work correctly

xaml_Tank_creation
<?xml version="1.0" encoding="utf-8"?>
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	Title="Window1"
	Height="350"
	Width="330">
	<Grid>
		<Label
			Content="Largeur A (m)"
			Height="25"
			Width="90"
			VerticalAlignment="Top"
			HorizontalAlignment="Left"
			Grid.Row="0"
			Grid.Column="0"
			Margin="25,25,0,0" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Width="100"
			Height="25"
			x:Name="A_value"
			Margin="0,25,25,0" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Content="Largeur B (m)"
			Width="90"
			Height="25"
			Margin="25,75,0,0" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			Height="25"
			Width="100"
			x:Name="B_value"
			Margin="0,75,25,0" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Top"
			Width="85"
			Height="25"
			Content="Hauteur H (m)"
			Margin="25,125,0,0" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Top"
			x:Name="H_value"
			Width="100"
			Height="25"
			Margin="0,125,25,0" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Height="25"
			Content="epaisseur Mur (m)"
			Width="105"
			Margin="25,0,0,120" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Bottom"
			x:Name="ep_value"
			Height="25"
			Width="100"
			Margin="0,0,25,120" />
		<Label
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Height="25"
			Content="Contrainte de béton (MPA)"
			Width="155"
			Margin="25,0,0,75" />
		<TextBox
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			Height="25"
			Width="100"
			VerticalAlignment="Bottom"
			Margin="0,217,25,75"
			x:Name="contrainte_value" />
		<Button
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Left"
			VerticalAlignment="Bottom"
			Width="90"
			Height="25"
			Margin="25,0,0,20"
			Content="OK"
			x:Name="OK_button"
			Click="ok_button" />
		<Button
			Grid.Column="0"
			Grid.Row="0"
			HorizontalAlignment="Right"
			VerticalAlignment="Bottom"
			Height="25"
			Width="90"
			Margin="0,0,25,20"
			Content="Cancel"
			x:Name="Cancel_button"
			Click="cancel_button" />
	</Grid>
</Window>
Tank_rscript
# -*- coding: UTF-8 -*-
from pyrevit import forms
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument
# get the path for the script
PATH_SCRIPT = os.path.dirname(__file__)

#get the default wall type within the document
wall_Type_Id = doc.GetDefaultElementTypeId(ElementTypeGroup.WallType)
def_wall_Type = doc.GetElement(wall_Type_Id)

#get the default floor type within the document
floor_Type_Id = doc.GetDefaultElementTypeId(ElementTypeGroup.FloorType)
def_floor_Type = doc.GetElement(floor_Type_Id)

#collecting levels
levels = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Levels).WhereElementIsNotElementType().ToElements()
#collecting materials
materials = FilteredElementCollector(doc).WherePasses(ElementCategoryFilter(BuiltInCategory.OST_Materials))

# get the base level 0.00
level = [l for l in levels if
         l.LookupParameter('Elévation').AsValueString() == '0.00' or l.LookupParameter('Elévation').AsDouble() == 0]

if len(level) > 0:
    lvl = level[0]
    lvl_Id = lvl.Id

# the form class
class ModalForm(Window):
    def __init__(self):
        path_xaml_file = os.path.join(PATH_SCRIPT, 'cofrage_modifié.xaml')
        wpf.LoadComponent(self, path_xaml_file)
        self.data = {}
        self.ShowDialog()

    def collect_input_data(self):
        """Collect and convert input data from the UI"""
        try:
            A = UnitUtils.ConvertToInternalUnits(float(self.A_value.Text), UnitTypeId.Meters)
            B = UnitUtils.ConvertToInternalUnits(float(self.B_value.Text), UnitTypeId.Meters)
            H = UnitUtils.ConvertToInternalUnits(float(self.H_value.Text), UnitTypeId.Meters)
            ep = UnitUtils.ConvertToInternalUnits(float(self.ep_value.Text), UnitTypeId.Meters)
            contrainte = float(self.contrainte_value.Text)
            self.data = {"a": A, "b": B, "h": H, "t": ep, "c": contrainte}
            return self.data
        except ValueError:
            forms.alert("Invalid input. Please enter numeric values.")
            return None

    def get_or_create_concrete_material(self):
        """Retrieve or create the required concrete material"""
        material = [i for i in materials if i.Name == "Béton - Coulé sur place - Béton{}".format(self.data["c"])]
        if len(material) > 0:
            return material[0].Id
        else:
            with Transaction(doc, 'Get/create concrete material') as t:
                t.Start()
                m_Id = Material.Create(doc, "Béton - Coulé sur place - Béton{}".format(self.data["c"]))
                mat = doc.GetElement(m_Id)
                mat.MaterialClass = "BĂ©ton"
                mat.MaterialCategory = "BĂ©ton"
                mat.Color = Color(192, 192, 192)
                mat.Shininess = 128
                mat.Smoothness = 50
                asset = StructuralAsset("ConcreteStructuralAsset", StructuralAssetClass.Concrete)
                asset.ConcreteCompression = UnitUtils.ConvertToInternalUnits(float(self.data["c"]), UnitTypeId.Megapascals)
                asset.Density = UnitUtils.ConvertToInternalUnits(2500, UnitTypeId.KilogramsPerCubicMeter)
                asset.SetPoissonRatio(0.2)
                asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(self.data["c"]) ** (1 / 3), UnitTypeId.Megapascals))
                struc = PropertySetElement.Create(doc, asset)
                mat.SetMaterialAspectByPropertySet(MaterialAspect.Structural, struc.Id)
                appar_Id = ElementId(177984)
                mat.AppearanceAssetId = appar_Id
                t.Commit()
            return m_Id

    def ok_button(self, sender, e):
        """OK button handler"""
        if self.collect_input_data():
            m_Id = self.get_or_create_concrete_material()
            self.create_tank_geometry(m_Id)

        self.Close()

    def create_tank_geometry(self, m_Id):
        """Creates the tank geometry inside a transaction"""
        Pt0 = XYZ(-(self.data["a"] + self.data["t"]) / 2, -(self.data["b"] + self.data["t"]) / 2, 0)
        Pt1 = XYZ((self.data["a"] + self.data["t"]) / 2, -(self.data["b"] + self.data["t"]) / 2, 0)
        Pt2 = XYZ((self.data["a"] + self.data["t"]) / 2, (self.data["b"] + self.data["t"]) / 2, 0)
        Pt3 = XYZ(-(self.data["a"] + self.data["t"]) / 2, (self.data["b"] + self.data["t"]) / 2, 0)

        Lines = [Line.CreateBound(Pt0, Pt1), Line.CreateBound(Pt1, Pt2), Line.CreateBound(Pt2, Pt3), Line.CreateBound(Pt3, Pt0)]
        loop = CurveLoop.Create(Lines)
        offset = self.data["t"]/2
        loop = CurveLoop.CreateViaOffset(loop, offset, loop.GetPlane().Normal)

        sloop = Line.CreateBound(XYZ(0, 0, 0), XYZ(1 / 0.3048, 0, 0))

        with Transaction(doc, 'Create Tank Geometry') as t:
            t.Start()
            # new wall type
            voile = def_wall_Type.Duplicate('voile-BA_20')
            # new floor type
            Dalle = def_floor_Type.Duplicate('Dalle_BA_20')
            cs1 = voile.GetCompoundStructure()
            cs2 = Dalle.GetCompoundStructure()

            for structLayer1, structLayer2 in zip(cs1.GetLayers(), cs2.GetLayers()):
                cs1.SetLayerFunction(structLayer1.LayerId, MaterialFunctionAssignment.Structure)
                cs1.SetMaterialId(structLayer1.LayerId, m_Id)
                cs1.SetLayerWidth(0, self.data["t"])
                cs2.SetLayerFunction(structLayer2.LayerId, MaterialFunctionAssignment.Structure)
                cs2.SetMaterialId(structLayer2.LayerId, m_Id)
                cs2.SetLayerWidth(0, self.data["t"])

            voile.SetCompoundStructure(cs1)
            Dalle.SetCompoundStructure(cs2)
            for l in Lines:
                # create walls
                Wall.Create(doc, l, voile.Id, lvl_Id, self.data["h"], 0.00, False, True)
                # create floor
            Floor.Create(doc, [loop], Dalle.Id, lvl_Id, True, sloop, 0)
            t.Commit()

    def cancel_button(self, sender, e):
        self.Close()

form = ModalForm()

Thanks.

Hi @REDO_79, nice work!

I already gave you some advice here

I see that you went through the dictionary route, but didn’t catch the rest of the advice, so here we are


By putting the logic inside the Window subclasses, you have coupled the GUI with the logic, and while this is fine for a simple, single use case, it doesn’t allow you to reuse the code in another fashion, for example via the pyrevit CLI or via a route endpoint.
Also, in the unlikely event that WPF is no longer supported by new versions of Revit, you would have to rewrite everything instead of only the graphic part.

Some tips to avoid this coupling:

  • the methods that don’t access self inside them can be quickly moved out of the class an made into functions;
  • after that, some of the remaining methods only access self.data, and they are called after collect_input_data, so just pass data as argument, and they also become simple functions to move out of the class!

You now should have the classes that only load the XML, initialize the data, handle the clicks and reads the input data.
And since the OK_Clicked method will mostly call external function, you might just turn into a method that collects the input data and closes the window if there is no error, then move the rest of the logic to a main function.

Refactored window class
class ManholeRebar(Window):
    def __init__(self):
        xaml_path = os.path.join(os.path.dirname(__file__), 'ferraillage.xaml')
        wpf.LoadComponent(self, xaml_path)
        # no need to save the list in a separate attribute that it not used anywhere...
        self.select_rebar.ItemsSource = [10, 12, 14, 16, 20, 25, 32, 40]
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # for this particular case, its seems clearer to me to check things beforehand
        # rather than wrapping everything in a try/except
        if not self.e1_value.Text:
            forms.alert("Please enter a valid value for : e1")
        elif not self.e2_value.Text:
            forms.alert("Please enter a valid value for : e2")
        elif not self.spacing_value.Text:
            forms.alert("Please enter a valid value for : s")
        elif self.select_rebar.SelectedItem is None:
            forms.alert("Please select a rebar diameter: d")
        else:
            try:
                self.data = {
                    "E1": _input_to_centimeters(self.e1_value.Text),
                    "E2": _input_to_centimeters(self.e2_value.Text),
                    "S": _input_to_centimeters(self.spacing_value.Text),
                    "d": self.select_rebar.SelectedItem
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()

# extracted out the repeating code from the collect input data method
def _input_to_centimeters(value):
    """Convert a textual value to centimeters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Centimeters)

Next, you load all sort of things before loading the input form, but if the user clicks cancel you have wasted time and memory for nothing. Collect the walls, floors and rebar types only when needed.
Also, using globally scoped variables (the one you declared before the form class) inside functions is not a great idea if you want to make them more “atomic”, that is, to be able to run them from other scripts. Let’s try to fix that.

Since you use only the first floor element that you find, you can retrieve it directly with FirstOrDefault() (returns None if no floors are found) First(), (raises an error if no floors are found) or Single() (raises an error if no or more than one floors are found).

def get_first_floor(doc):
    """Return the first floor found in the document."""
    return (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Floors)
        .WhereElementIsNotElementType()
        .ToElements()
        .FirstOrDefault()
    )

NOTE: As you can see, I use the doc argument to refer to the document, to be able to use the function on other documents and not only the active one; we’ll see how it can be done later.
I’ll do this to all the functions that needs access to the document.

The rebar types list is only used in the rebar_Type function, so you can embed it there.
Also, to find a single element in a sequence, use next to speedup the process, since it will stop at the first matching item instead of looping through all the list to just pick one item.
I also believe Element.Name.GetValue(r) could be simplified to just r.Name
 but I have not tested it.

def get_rebar_type(diameter, doc):
    """Return the rebar type associated to the given diameter."""
    # we only need the rebar types collection here, so I moved it from the global scope
    rebar_name = "HA{} (Fe400)".format(diameter)
    rebar_types = (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
        .WhereElementIsElementType()
        .ToElements()
    )
    return next(r for r in rebar_types if r.Name == rebar_name)

NOTE: I’ve renamed this and the other functions to be more explanatory, see the naming style comments below

You can also use the LinQ .Where method to do it all at once

def get_rebar_type(diameter, doc):
    """Return the rebar type associated to the given diameter."""
    # we only need the rebar types collection here, so I moved it from the global scope
    rebar_name = "HA{} (Fe400)".format(diameter)
    return (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
        .WhereElementIsElementType()
        .Where(lambda r: r.Name == rebar_name)
        .FirstOrDefault()
    )

The units variable is used only in the covers function, so it can be moved inside there.

def get_covers_parameters(data, floor_width, doc):
    """Cover parameters."""
    units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()
    c1 = width/ 2 + data["E1"]
    c2 = floor_width - data["E2"]
    d2 = c2 - UnitUtils.ConvertToInternalUnits(data["d"], UnitTypeId.Millimeters)
    c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
    return c1, c2, d2, c3

Note on the cover variable: you pass it as is to the various functions, but then use only a subset of the items; I suggest to be more explicit and specify the exact parameters that a function needs and only pass those parameters.

Here’s what your core function could look like:

def create_manhole_rebars(type, cover, floor, step, doc):
    """Create the rebars for a manhole."""
    # functions are now self explanatory, no need for comments at each line
    c1, c2, d2, c3 = cover
    curveloop1 = create_vertical_curveloop(walls[0], c1, c2, d2)
    path1 = create_vertical_rebar_path_distribution(walls[0], c1)
    curveloop2 = create_vertical_curveloop(walls[1], c1, c2, d2)
    path2 = create_vertical_rebar_path_distribution(walls[1], c1)
    curveloop3 = create_horizontal_curveloop(walls, c3)
    path3 = create_horizontal_rebar_path_distribution(walls[0], c3)
    loop1 = translate_curveloop_along_path(curveloop1, path2, step)
    loop2 = translate_curveloop_along_path(curveloop2, path1, step)
    loop3 = translate_curveloop_along_path(curveloop3, path3, step)

    # create rebars
    with Transaction(doc, 'create rebars') as t:
        t.Start()
        validationResult = clr.Reference[RebarFreeFormValidationResult]()
        Rebar.CreateFreeForm(doc, type, floor, loop1, validationResult)
        Rebar.CreateFreeForm(doc, type, floor, loop2, validationResult)
        Rebar.CreateFreeForm(doc, type, floor, loop3, validationResult)
        t.Commit()

The hardest thing for me to undestand and refactor is the walls variable, but I’ve invested too much time already on this review, so I’ll leave it to you.
The only thing I will point out is that the V_path function is nonsense to me: you pass a wall as argument to then check against the first item in the walls list to then use the other wall
 just pass the other wall and it’s done! (I already changed the arguments in the core function above)

def create_vertical_rebar_path_distribution(other_wall, c1):
    """Define path for vertical rebar distribution."""
    temp_line = other_wall.Location.Curve
    p_a = c1 / temp_line.Length
    return Line.CreateBound(
        temp_line.Evaluate(p_a, True),
        temp_line.Evaluate(1 - p_a, True)
    )

Putting it all together, the main function goes like this

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc  # revit can be imported from pyrevit 
    form = ManholeRebar()
    if not form.data:
        return
    data = form.data
    # see the advices below to understand these changes
    type = get_rebar_type(data["d"], doc)
    floor = get_first_floor(doc)
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    cover = get_covers_parameters(data, floor_width, doc)
    create_manhole_rebars(type, cover, floor, data["S"], doc)

if __name__ == "__main__":
    main()

The last two lines allows you to run the main function on the active document if the script is run via the button click, but you can also import the main function in another script and call it by passing whatever document you want with main(my_document).

Other tips:

  • type is a reserved word, don’t use it as variable name
  • In c# the naming standard is PascalCase for classes and methods and camelCase for variables and method arguments; Python uses PascalCases for classes and snake_case (lowercase) for almost everything else, you’re mixing the two styles; not an error, but it hurts my eyes :rofl: a general advice is to stick with one of the two styles through all your code, your future self will thank you. Obviously this is for the classes/methods/variables you create/define.
  • method names should be of the form verb_noun, like get_covers_parameters, and in general the naming should be self explanatory instead of having to explain it with a comment
  • instead of using comments above the function definition, use docstrings inside them
2 Likes

@sanzoghenzo
First of all, I want to thank you for taking the time to deconstruct my code and help me refactor it to make it more efficient and easier to understand.
I have noted your remarks to avoid coupling the logic of the code with the GUI as much as possible, so I can reuse my code later and prevent unnecessary memory consumption.

Regarding my mistakes in naming functions and making them self-explanatory, they stem from beginner’s errors compared to you :roll_eyes: and from not yet having developed the habit of using more functions in my code. That said, I will take your advice into account for the future.

I hope the following image helps explain the V_path function. (In the case of a rectangular tank, we have two walls of different lengths, resulting in two distinct Curveloops, each requiring a different path for rebar distribution).

I tried to assemble the code according to your changes, but as you can see below, I encountered an error with the _input_to_centimeters(value) function. Additionally, I don’t understand why you prefixed it with a single underscore?

IronPython Traceback:
Traceback (most recent call last):
File “D:\Examples_pyRevit\Tank\Tank.extension\Tank.tab\Tank rebars.panel\Rebars.pushbutton\Tank_rebars_script.py”, line 252, in
File “D:\Examples_pyRevit\Tank\Tank.extension\Tank.tab\Tank rebars.panel\Rebars.pushbutton\Tank_rebars_script.py”, line 240, in main
File “D:\Examples_pyRevit\Tank\Tank.extension\Tank.tab\Tank rebars.panel\Rebars.pushbutton\Tank_rebars_script.py”, line 43, in init
File “D:\Examples_pyRevit\Tank\Tank.extension\Tank.tab\Tank rebars.panel\Rebars.pushbutton\Tank_rebars_script.py”, line 71, in OK_Clicked
File “D:\Examples_pyRevit\Tank\Tank.extension\Tank.tab\Tank rebars.panel\Rebars.pushbutton\Tank_rebars_script.py”, line 58, in _collect_input_data
NameError: global name ‘_input_to_centimeters’ is not defined

Here the new code:

New code
# -*- coding: UTF-8 -*-
from pyrevit import forms
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
from System import Array
from System.Collections.Generic import List, IList, Dictionary
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
results = clr.Reference[IntersectionResultArray]()

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

# select all walls within the project
walls = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
width = walls[0].Width

script_path = os.path.dirname(__file__)

class manhole_rebar(Window):
    def __init__(self):
        xaml_path = os.path.join(script_path, 'Tank_rebars.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.select_rebar.ItemsSource = [10, 12, 14, 16, 20, 25, 32, 40]
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.e1_value.Text:
            forms.alert("Please enter a valid value for : e1")
        elif not self.e2_value.Text:
            forms.alert("Please enter a valid value for : e2")
        elif not self.spacing_value.Text:
            forms.alert("Please enter a valid value for : s")
        elif self.select_rebar.SelectedItem is None:
            forms.alert("Please select a rebar diameter : d")
        else:
            try:
                self.data = {
                    "E1": _input_to_centimeters(self.e1_value.Text),
                    "E2": _input_to_centimeters(self.e2_value.Text),
                    "S": _input_to_centimeters(self.spacing_value.Text),
                    "d": self.select_rebar.SelectedItem
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()

    def _input_to_centimeters(value):
        """Convert a textual value to centimeters."""
        return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Centimeters)

    def get_first_floor(doc):
        """Return the first floor found in the document."""
        return (
            FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Floors)
            .WhereElementIsNotElementType()
            .ToElements()
            .FirstOrDefault()
        )

    def get_rebar_type(diameter, doc):
        """Return the rebar type associated to the given diameter."""
        rebar_name = "HA{} (Fe400)".format(diameter)
        rebar_types = (
            FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
            .WhereElementIsElementType()
            .ToElements()
        )
        return next(r for r in rebar_types if r.Name == rebar_name)

    def get_covers_parameters(data, floor_width, doc):
        """Cover parameters."""
        units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()
        c1 = width / 2 + data["E1"]
        c2 = floor_width - data["E2"]
        d2 = c2 - UnitUtils.ConvertToInternalUnits(data["d"], UnitTypeId.Millimeters)
        c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
        return c1, c2, d2, c3

    # curveloop function to create the vertical rebar shape "|_| "
    def V_curveloop_create(wall, cover):
        c1, c2, d2, c3 = cover
        bbx = wall.get_BoundingBox(None)
        h = bbx.Max.Z - bbx.Min.Z
        temp_line = wall.Location.Curve
        p1 = -(d2-c2) / temp_line.Length
        p2 = 1 - p1
        pA = c1 / temp_line.Length
        pB = 1 - pA
        lineB = Line.CreateBound(temp_line.Evaluate(p1, True), temp_line.Evaluate(p2, True))
        vectB = XYZ(0, 0, 1)
        if wall == walls[0]:
            lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-c2)))
        else:
            lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-d2)))
        vectZ = XYZ(0, 0, h + c2)
        lineA = Line.CreateBound(lineB.GetEndPoint(0).Add(vectZ), lineB.GetEndPoint(0))
        lineC = Line.CreateBound(lineB.GetEndPoint(1), lineB.GetEndPoint(1).Add(vectZ))
        # curveloop1 obtained
        curveloop = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
        # Translate the curveloop1 to the start position before doing a multiple offset
        plane = curveloop.GetPlane()
        trans = Transform.CreateTranslation(plane.Normal.Negate().Multiply(c1))
        curveloop.Transform(trans)
        return curveloop

    # curveloop function to create the horizontal rebar shape as a rectangle
    def H_curveloop_create(walls, cover):
        c1, c2, d2, c3 = cover
        points = []
        L1 = walls[0].Location.Curve.Length
        L2 = walls[1].Location.Curve.Length
        pt1 = walls[0].Location.Curve.GetEndPoint(0)
        points.append(pt1)
        pt2 = walls[0].Location.Curve.GetEndPoint(1)
        points.append(pt2)
        vect = (XYZ(pt2.X, pt2.Y, pt2.Z + L2)).Subtract(pt1)
        cross_direction = vect.CrossProduct(walls[0].Location.Curve.Direction).Normalize()
        pt3 = pt2 + cross_direction.Multiply(L2)
        points.append(pt3)
        pt4 = pt3 + walls[0].Location.Curve.Direction.Negate().Multiply(L1)
        points.append(pt4)
        lines = [Line.CreateBound(points[i], points[i + 1]) for i in range(len(points)-1)]
        lines.append(Line.CreateBound(pt4, pt1))
        # Horizontal curveloop "H_Loop" obtained
        H_Loop = CurveLoop.Create(lines)
        # Translate the curveloop2 to the start position before doing a multiple offset
        H_plane = H_Loop.GetPlane()
        H_trans = Transform.CreateTranslation(H_plane.Normal.Multiply(c3))
        H_Loop.Transform(H_trans)
        return H_Loop

    # define path for vertical rebar distribution
    def V_path(wall, cover):
        c1, c2, d2, c3 = cover
        if wall == walls[0]:
            temp_line = walls[1].Location.Curve
            pA = c1 / temp_line.Length
            pB = 1 - pA
            path = Line.CreateBound(temp_line.Evaluate(pA, True), temp_line.Evaluate(pB, True))
        else:
            temp_line = walls[0].Location.Curve
            pA = c1 / temp_line.Length
            pB = 1 - pA
            path = Line.CreateBound(temp_line.Evaluate(pA, True), temp_line.Evaluate(pB, True))
        return path

    # define path for horizontal rebar distribution
    def H_path(wall, cover):
        c1, c2, d2, c3 = cover
        bbx = wall.get_BoundingBox(None)
        h = bbx.Max.Z - bbx.Min.Z
        temp_pt = wall.Location.Curve.GetEndPoint(0)
        path = Line.CreateBound(XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + c3), XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + h - c3))
        return path

    # define a function to create a multiple translation of a curveloop
    def curveloop_Transform(curveloop, path):
        n = path.Length // data["S"]
        loop = []
        if curveloop.GetPlane().Normal.IsAlmostEqualTo(XYZ.BasisZ):
            offset = curveloop.GetPlane().Normal.Multiply(data["S"])
        else:
            offset = curveloop.GetPlane().Normal.Negate().Multiply(data["S"])
        xform = Transform.CreateTranslation(offset)
        loop.append(curveloop)
        count = 0
        while count < n:
            transformed_curves = [c.CreateTransformed(xform) for c in loop[-1]]
            transformed_loop = CurveLoop.Create(transformed_curves)
            loop.append(transformed_loop)
            count += 1
        return List[CurveLoop](loop)

    # Main function to create rebars
    def Manhole_rebar(type, cover, floor, doc):
        c1, c2, d2, c3 = cover
        curveloop1 = V_curveloop_create(walls[0], cover)
        path1 = V_path(walls[1], cover)
        curveloop2 = V_curveloop_create(walls[1], cover)
        path2 = V_path(walls[0], cover)
        curveloop3 = H_curveloop_create(walls, cover)
        path3 = H_path(walls[0], cover)
        loop1 = curveloop_Transform(curveloop1, path2)
        loop2 = curveloop_Transform(curveloop2, path1)
        loop3 = curveloop_Transform(curveloop3, path3)

        # create rebars
        with Transaction(doc, 'create rebars') as t:
            t.Start()
            validationResult = clr.Reference[RebarFreeFormValidationResult]()
            rebar1 = Rebar.CreateFreeForm(doc, type, floors[0], loop1, validationResult)
            rebar2 = Rebar.CreateFreeForm(doc, type, floors[0], loop2, validationResult)
            rebar3 = Rebar.CreateFreeForm(doc, type, floors[0], loop3, validationResult)
            t.Commit()

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc   # HOST_APP can be imported from pyrevit
    form = manhole_rebar()
    if not form.data:
        return
    data = form.data
    # see the advices below to understand these changes
    type = get_rebar_type(data["d"], doc)
    floor = get_first_floor(doc)
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    cover = get_covers_parameters(data, floor_width, doc)
    create_manhole_rebars(type, cover, floor, data["S"], doc)

if __name__ == "__main__":
    main()

Thanks.

Thanks for the visual and sorry for my bad wording. What I was trying to say is that the V_path function was needlessly complicated, and it was also because you mixed function arguments with global variables. I get that you need the opposite wall to create the path for the rebar, and this is why the function is much simpler and easier if you just pass the opposite wall in the first place.

you need to de-indent the functions: everything after the Cancel_Clicked method needs to be outside the class, so the defs needs to not have spaces before.

This is a python style convention to signal that the function is an internal one; in the context of pyRevit it doesn’t matter much, but I use python in my daily work and I tend to be consistent with the style
 Sorry if I’ve confused you.

1 Like

@sanzoghenzo

I de-indented all the blocks after the Cancel_Clicked method, but this time I encountered an error related to the get_rebar_type function you defined. I made some changes to it (the diameter value should be returned from the key data["d"]) and tried your two methods for that, but I still get the same error, which says:

here the modified get_rebar_type function:

def get_rebar_type(data, doc):
    """Return the rebar type associated to the given diameter."""
    rebar_name = "HA{} (Fe400)".format(data["d"])
    rebar_types = (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
        .WhereElementIsElementType()
        .ToElements()
    )
    return next(r for r in rebar_types if r.Name == rebar_name)

Thanks.

I should have stated that my code was not tested, but
 The edit you made didn’t change anything, at the cost of clarity: you pass data to only read the “d” value, but this it not clear from the function signature.

Anyway, from the error, and without seeing the rest of your final code, it seems that you’re still passing the diameter to the function instead of the whole data dictionary.

Forgive me if I sound harsh, but are you looking for solutions yourself? These kind of errors are something that you should be able to solve by yourself with a little effort, google, stack overflow and chat gpt are your friends :wink:

2 Likes

I finally solved my issue and got my tool working perfectly. Thank you, @sanzoghenzo

Here my final script:

Refactored rebar manhool tool
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
from System import Array
from System.Collections.Generic import List, IList, Dictionary
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
results = clr.Reference[IntersectionResultArray]()

walls = FilteredElementCollector(doc).OfCategory(
        BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()

width = walls[0].Width

script_path = os.path.dirname(__file__)

class manhole_rebar(Window):
    def __init__(self):
        xaml_path = os.path.join(script_path, 'Tank_rebars.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.select_rebar.ItemsSource = [10, 12, 14, 16, 20, 25, 32, 40]
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.e1_value.Text:
            forms.alert("Please enter a valid value for : e1")
        elif not self.e2_value.Text:
            forms.alert("Please enter a valid value for : e2")
        elif not self.spacing_value.Text:
            forms.alert("Please enter a valid value for : s")
        elif self.select_rebar.SelectedItem is None:
            forms.alert("Please select a rebar diameter : d")
        else:
            try:
                self.data = {
                    "E1": _input_to_centimeters(self.e1_value.Text),
                    "E2": _input_to_centimeters(self.e2_value.Text),
                    "S": _input_to_centimeters(self.spacing_value.Text),
                    "d": self.select_rebar.SelectedItem
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        #self.data
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()

def _input_to_centimeters(value):
    """Convert a textual value to centimeters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Centimeters)

def get_first_floor(doc):
    """Return the first floor found in the document."""
    return FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Floors).WhereElementIsNotElementType().FirstElement()

def get_rebar_type(diameter, doc):
    """Return the rebar type associated to the given diameter."""
    rebar_name = "HA{} (Fe400)".format(diameter)
    rebar_types = (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
        .WhereElementIsElementType()
        .ToElements()
    )
    return next(r for r in rebar_types if Element.Name.GetValue(r) == rebar_name)

def get_covers_parameters(data, floor_width, doc):
    """Cover parameters."""
    units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()
    c1 = width / 2 + data["E1"]
    c2 = floor_width - data["E2"]
    d2 = c2 - UnitUtils.ConvertToInternalUnits(data["d"], UnitTypeId.Millimeters)
    c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
    return c1, c2, d2, c3

# curveloop function to create the vertical rebar shape "|_| "
def create_vertical_curveloop(wall, c1, c2, d2):
    """create vertical curvloop needed for vertical rebar shape"""
    bbx = wall.get_BoundingBox(None)
    h = bbx.Max.Z - bbx.Min.Z
    temp_line = wall.Location.Curve
    p1 = -(d2-c2) / temp_line.Length
    p2 = 1 - p1
    pA = c1 / temp_line.Length
    pB = 1 - pA
    lineB = Line.CreateBound(temp_line.Evaluate(p1, True), temp_line.Evaluate(p2, True))
    vectB = XYZ(0, 0, 1)
    if wall == walls[0]:
        lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-c2)))
    else:
        lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(-d2)))
    vectZ = XYZ(0, 0, h + c2)
    lineA = Line.CreateBound(lineB.GetEndPoint(0).Add(vectZ), lineB.GetEndPoint(0))
    lineC = Line.CreateBound(lineB.GetEndPoint(1), lineB.GetEndPoint(1).Add(vectZ))
    # curveloop1 obtained
    curveloop = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
    # Translate the curveloop1 to the start position before doing a multiple offset
    plane = curveloop.GetPlane()
    trans = Transform.CreateTranslation(plane.Normal.Negate().Multiply(c1))
    curveloop.Transform(trans)
    return curveloop

# curveloop function to create the horizontal rebar shape as a rectangle
def create_horizontal_curveloop(walls, c3):
    """create horizontal curvloop needed for horizontal rebar shape"""
    points = []
    L1 = walls[0].Location.Curve.Length
    L2 = walls[1].Location.Curve.Length
    pt1 = walls[0].Location.Curve.GetEndPoint(0)
    points.append(pt1)
    pt2 = walls[0].Location.Curve.GetEndPoint(1)
    points.append(pt2)
    vect = (XYZ(pt2.X, pt2.Y, pt2.Z + L2)).Subtract(pt1)
    cross_direction = vect.CrossProduct(walls[0].Location.Curve.Direction).Normalize()
    pt3 = pt2 + cross_direction.Multiply(L2)
    points.append(pt3)
    pt4 = pt3 + walls[0].Location.Curve.Direction.Negate().Multiply(L1)
    points.append(pt4)
    lines = [Line.CreateBound(points[i], points[i + 1]) for i in range(len(points)-1)]
    lines.append(Line.CreateBound(pt4, pt1))
    # Horizontal curveloop "H_Loop" obtained
    H_Loop = CurveLoop.Create(lines)
    # Translate the curveloop2 to the start position before doing a multiple offset
    H_plane = H_Loop.GetPlane()
    H_trans = Transform.CreateTranslation(H_plane.Normal.Multiply(c3))
    H_Loop.Transform(H_trans)
    return H_Loop

def create_vertical_rebar_path_distribution(wall, c1):
    """Define path for vertical rebar distribution."""
    if wall == walls[0]:
        temp_line = walls[1].Location.Curve
    else:
        temp_line = walls[0].Location.Curve
    pA = c1 / temp_line.Length
    pB = 1 - pA
    path = Line.CreateBound(temp_line.Evaluate(pA, True), temp_line.Evaluate(pB, True))
    return path

def create_horizontal_rebar_path_distribution(wall, c3):
    """Define path for horizontal rebar distribution."""
    bbx = wall.get_BoundingBox(None)
    h = bbx.Max.Z - bbx.Min.Z
    temp_pt = wall.Location.Curve.GetEndPoint(0)
    path = Line.CreateBound(XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + c3), XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + h - c3))
    return path

def translate_curveloop_along_path(curveloop, path, step):
    """Multiple translation of curveloop with given space and creating arraay list from curvloop """
    n = path.Length // step
    loop = [curveloop]
    if curveloop.GetPlane().Normal.IsAlmostEqualTo(XYZ.BasisZ):
        offset = curveloop.GetPlane().Normal.Multiply(step)
    else:
        offset = curveloop.GetPlane().Normal.Negate().Multiply(step)
    xform = Transform.CreateTranslation(offset)
    count = 0
    while count < n:
        transformed_curves = [c.CreateTransformed(xform) for c in loop[-1]]
        transformed_loop = CurveLoop.Create(transformed_curves)
        loop.append(transformed_loop)
        count += 1
    return List[CurveLoop](loop)

def create_manhole_rebars(data, r_type, cover, floor, doc):
    """Create the rebars for a manhole."""
    c1, c2, d2, c3 = cover
    curveloop1 = create_vertical_curveloop(walls[0], c1, c2, d2)
    path1 = create_vertical_rebar_path_distribution(walls[1], c1)
    curveloop2 = create_vertical_curveloop(walls[1], c1, c2, d2)
    path2 = create_vertical_rebar_path_distribution(walls[0], c1)
    curveloop3 = create_horizontal_curveloop(walls, c3)
    path3 = create_horizontal_rebar_path_distribution(walls[0], c3)
    loop1 = translate_curveloop_along_path(curveloop1, path2, data["S"])
    loop2 = translate_curveloop_along_path(curveloop2, path1, data["S"])
    loop3 = translate_curveloop_along_path(curveloop3, path3, data["S"])

    # create rebars
    with Transaction(doc, 'create rebars') as t:
        t.Start()
        validationResult = clr.Reference[RebarFreeFormValidationResult]()
        rebar1 = Rebar.CreateFreeForm(doc, r_type, floor, loop1, validationResult)
        rebar2 = Rebar.CreateFreeForm(doc, r_type, floor, loop2, validationResult)
        rebar3 = Rebar.CreateFreeForm(doc, r_type, floor, loop3, validationResult)
        t.Commit()

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    form = manhole_rebar()
    if not form.data:
        return
    data = form.data
    r_type = get_rebar_type(data["d"], doc)
    floor = get_first_floor(doc)
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    cover = get_covers_parameters(data, floor_width, doc)
    create_manhole_rebars(data, r_type, cover, floor, doc)

if __name__ == "__main__":
    main()


Thanks.

2 Likes

That’s good to hear @REDO_79 !

I’m still worried about the walls variable, and in general by the way you collect the elements: what if you have other walls and floors inside your model?

To make it future proof you could ask the user to select the wall1, wall2 and floor, so that you exactly have the items you need.

You can use the pyrevit.revit.selection.pick_element_by_category for that.

2 Likes

@sanzoghenzo

To answer your question, I need to specify the following points:

This tool is used exclusively within the Tank tools and should be used only after the tank itself has been created using the other tool called ‘Dimensions.’ This ensures that the tank will have four walls and a floor. Additionally, since I’m not trying to reinforce each wall independently, the rebar shape I’m aiming to create is not dependent on a single wall.
image

I’m also not satisfied with how I use the variables wall1 and wall2 inside the functions create_vertical_curveloop and create_vertical_rebar_path_distribution. My intuition tells me there’s a better way to define the logic for these functions, but I’m struggling to figure out how to do it!?
The reason I excluded the selection option for the user is to prevent them from selecting the wrong walls, which could result in an incorrect CurveLoop and an improper path for the rebar distribution. For more details and to better understand the logic I use in my code, please refer to the image below and suggest any changes to improve the logic.

Thanks.

for the vertical rebar path distribution, I already wrote what you can do to simplify it, but let’s analyze it one step at time:

    if wall == walls[0]:
        temp_line = walls[1].Location.Curve
    else:
        temp_line = walls[0].Location.Curve

since .Location.Curve is common to both if branches, we can just use the if to select the wall:

if wall == walls[0]:
    opposite_wall = walls[1]
else:
    opposite_wall = walls[0]
temp_line = opposite_wall.Location.Curve

But wall and walls are only used for this, to get the opposite wall, so you might just remove all this logic an directly pass the opposite wall when you call the function

def create_vertical_rebar_path_distribution(opposite_wall, c1):
    """Define path for vertical rebar distribution."""
    temp_line = opposite_wall.Location.Curve
    p_a = c1 / temp_line.Length
    # here I removed pB since it is only used once, i find it more readable like this
    return Line.CreateBound(
        temp_line.Evaluate(p_a, True),
        temp_line.Evaluate(1 - p_a, True)
    )

You can simplify the create_vertical_curveloop check, since there’s also only one thing that changes: the argument to the Multiply method:

    multiplier = -c2 if wall == walls[0] else -d2
    lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB.Multiply(multiplier)))

This doesn’t remove the need for the walls variable, but it is more readable to me.
Moving on with the cleanup:

def create_vertical_curveloop(wall, c1, c2, d2):
    """Create vertical curvloop needed for vertical rebar shape."""
    # bbx and h are use more down the line, moving them
    temp_line = wall.Location.Curve
    p1 = -(d2-c2) / temp_line.Length
    # p2 only used once, same as above
    # pA and pB are not used at all, removed
    lineB = Line.CreateBound(
        temp_line.Evaluate(p1, True),
        temp_line.Evaluate(1 - p1, True)
    )
    # you multiply the vector 0,0,1 by the parameter,
    # so you just can create the vector with the parameter in Z
    vectB = XYZ(0, 0, -c2 if wall == walls[0] else -d2)
    lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB))

    bbx = wall.get_BoundingBox(None)
    # h used only once with c2, this is my personal preference for better clarity
    delta_z = bbx.Max.Z - bbx.Min.Z + c2
    vectZ = XYZ(0, 0, delta_z)
    # the repeated GetEndpoint can be extracted (it will save you a little of CPU time)
    start_point = lineB.GetEndPoint(0)
    lineA = Line.CreateBound(start_point.Add(vectZ), start_point)
    end_point = lineB.GetEndPoint(1)
    lineC = Line.CreateBound(end_point, end_point.Add(vectZ))
    # curveloop1 obtained
    curveloop = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
    # Translate the curveloop1 to the start position before doing a multiple offset
    plane = curveloop.GetPlane()
    trans = Transform.CreateTranslation(plane.Normal.Negate().Multiply(c1))
    curveloop.Transform(trans)
    return curveloop

There are other places in which you can use variables to simplify things, for instance:

  • walls[n].Location.Curve, and now that I see it, the create_horizontal_curveloop could just accept the 2 wall curves as arguments
  • curveloop.GetPlane().Normal.

I also notice that sometimes you use point.Add(XYZ(0,0,something)), ant other times you go with XYZ(point.X, point.Y, point.Z+something)
 it’s fine, but I would stick to a single way to do a thing, again to improve readability (and maybe find repetitive patterns to extract out as reusable functions
)

1 Like

@sanzoghenzo

I understood the logic you used to simplify the function create_vertical_curveloop. I applied your changes, and it works well. However, for the function create_vertical_rebar_path_distribution, I understood the principle of using opposite_wall to define the appropriate path, but I can’t figure out why the check for this condition is outside this function (could it be an error?). Additionally, when I tried to use the function as you defined it, I got an error:

NameError: name ‘wall’ is not defined.

By including opposite_wall inside the function, the error was resolved, and it works as expected.

here my defintion for create_vertical_rebar_path_distribution function

def create_vertical_rebar_path_distribution(wall, c1):
    """Define path for vertical rebar distribution."""
    if wall == wall_1:
        opposite_wall = wall_2
    else:
        opposite_wall = wall_1
    temp_line = opposite_wall.Location.Curve
    p_a = c1 / temp_line.Length
    return Line.CreateBound(temp_line.Evaluate(p_a, True), temp_line.Evaluate(1-p_a, True))

I had the same thought as you before you posted your answer. For that reason, I initially considered returning the variables wall_1 and wall_2 and wall_width directly from the collected walls and adding the get_walls function to the main. Then, I planned to call the variables wall_1 and wall_2 as instances in their respective functions. However, I couldn’t figure out how to use their definitions correctly, either inside the main function or within their respective functions, because I encountered this error:

NameError: global name ‘wall_width’ is not defined

I tried to remove wall_width from the returned values of the get_walls function and define it later in the get_covers_parameters function, but I still encountered the same error. (Sorry if this is a silly question, but I suspect the error is related to the definition of a global or local variable inside a function. However, I still can’t figure out how to resolve it or whether it’s also related to the main function definition.)

Please check my remodeled script:

remodeled script
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
from System import Array
from System.Collections.Generic import List, IList, Dictionary
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
results = clr.Reference[IntersectionResultArray]()

script_path = os.path.dirname(__file__)

class manhole_rebar(Window):
    def __init__(self):
        xaml_path = os.path.join(script_path, 'Tank_rebars.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.select_rebar.ItemsSource = [10, 12, 14, 16, 20, 25, 32, 40]
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.e1_value.Text:
            forms.alert("Please enter a valid value for : e1")
        elif not self.e2_value.Text:
            forms.alert("Please enter a valid value for : e2")
        elif not self.H_spacing_value.Text:
            forms.alert("Please enter a valid value for : s1")
        elif not self.V_spacing_value.Text:
            forms.alert("Please enter a valid value for : s2")
            
        elif self.select_rebar.SelectedItem is None:
            forms.alert("Please select a rebar diameter : d")
        else:
            try:
                self.data = {
                    "E1": _input_to_centimeters(self.e1_value.Text),
                    "E2": _input_to_centimeters(self.e2_value.Text),
                    "S1": _input_to_centimeters(self.H_spacing_value.Text),
                    "S2": _input_to_centimeters(self.V_spacing_value.Text),
                    "d": self.select_rebar.SelectedItem
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        #self.data
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()

def _input_to_centimeters(value):
    """Convert a textual value to centimeters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Centimeters)

def get_walls(doc):
    """Return the first and second wall with their width found in the document."""
    walls = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
    wall_1 = walls[0]
    wall_2 = walls[1]
    wall_width = wall_1.Width
    return wall_1, wall_2 , wall_width

def get_first_floor(doc):
    """Return the first floor found in the document."""
    return FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Floors).WhereElementIsNotElementType().FirstElement()

def get_rebar_type(diameter, doc):
    """Return the rebar type associated to the given diameter."""
    rebar_name = "HA{} (Fe400)".format(diameter)
    rebar_types = (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
        .WhereElementIsElementType()
        .ToElements()
    )
    return next(r for r in rebar_types if Element.Name.GetValue(r) == rebar_name)

def get_covers_parameters(data, wall_width, floor_width, doc):
    """Cover parameters."""
    units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()
    c1 = wall_width / 2 + data["E1"]
    c2 = floor_width - data["E2"]
    d2 = c2 - UnitUtils.ConvertToInternalUnits(data["d"], UnitTypeId.Millimeters)
    c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
    return c1, c2, d2, c3

# curveloop function to create the vertical rebar shape "|_| "
def create_vertical_curveloop(wall, c1, c2, d2):
    """create vertical curvloop needed for vertical rebar shape"""
    temp_line = wall.Location.Curve
    p1 = -(d2-c2) / temp_line.Length
    lineB = Line.CreateBound(temp_line.Evaluate(p1, True), temp_line.Evaluate(1-p1, True))
    vectB = XYZ(0, 0, -c2 if wall == wall_1 else -d2)
    lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB))
    bbx = wall.get_BoundingBox(None)
    delta_z = bbx.Max.Z - bbx.Min.Z + c2
    vectZ = XYZ(0, 0, delta_z )
    lineA = Line.CreateBound(lineB.GetEndPoint(0).Add(vectZ), lineB.GetEndPoint(0))
    lineC = Line.CreateBound(lineB.GetEndPoint(1), lineB.GetEndPoint(1).Add(vectZ))
    curveloop = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
    # Translate the curveloop1 to the start position before doing a multiple offset
    plane = curveloop.GetPlane()
    trans = Transform.CreateTranslation(plane.Normal.Negate().Multiply(c1))
    curveloop.Transform(trans)
    return curveloop

# curveloop function to create the horizontal rebar shape as a rectangle
def create_horizontal_curveloop(wall_1, wall_2, c3):
    """create horizontal curvloop needed for horizontal rebar shape"""
    points = []
    L1 = wall_1.Location.Curve.Length
    L2 = wall_2.Location.Curve.Length
    pt1 = wall_1.Location.Curve.GetEndPoint(0)
    points.append(pt1)
    pt2 = wall_1.Location.Curve.GetEndPoint(1)
    points.append(pt2)
    vect = (XYZ(pt2.X, pt2.Y, pt2.Z + L2)).Subtract(pt1)
    cross_direction = vect.CrossProduct(wall_1.Location.Curve.Direction).Normalize()
    pt3 = pt2 + cross_direction.Multiply(L2)
    points.append(pt3)
    pt4 = pt3 + wall_1.Location.Curve.Direction.Negate().Multiply(L1)
    points.append(pt4)
    lines = [Line.CreateBound(points[i], points[i + 1]) for i in range(len(points)-1)]
    lines.append(Line.CreateBound(pt4, pt1))
    # Horizontal curveloop "H_Loop" obtained
    H_Loop = CurveLoop.Create(lines)
    # Translate the curveloop2 to the start position before doing a multiple offset
    H_plane = H_Loop.GetPlane()
    H_trans = Transform.CreateTranslation(H_plane.Normal.Multiply(c3))
    H_Loop.Transform(H_trans)
    return H_Loop

def create_vertical_rebar_path_distribution(wall, c1):
    """Define path for vertical rebar distribution."""
    if wall == wall_1:
        opposite_wall = wall_2
    else:
        opposite_wall = wall_1
    temp_line = opposite_wall.Location.Curve
    p_a = c1 / temp_line.Length
    return Line.CreateBound(temp_line.Evaluate(p_a, True), temp_line.Evaluate(1-p_a, True))

def create_horizontal_rebar_path_distribution(wall, c3):
    """Define path for horizontal rebar distribution."""
    bbx = wall.get_BoundingBox(None)
    h = bbx.Max.Z - bbx.Min.Z
    temp_pt = wall.Location.Curve.GetEndPoint(0)
    path = Line.CreateBound(XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + c3), XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + h - c3))
    return path

def translate_curveloop_along_path(curveloop, path, step):
    """Multiple translation of curveloop with given space and creating arraay list from curvloop """
    n = path.Length // step
    loop = [curveloop]
    if curveloop.GetPlane().Normal.IsAlmostEqualTo(XYZ.BasisZ):
        offset = curveloop.GetPlane().Normal.Multiply(step)
    else:
        offset = curveloop.GetPlane().Normal.Negate().Multiply(step)
    xform = Transform.CreateTranslation(offset)
    count = 0
    while count < n:
        transformed_curves = [c.CreateTransformed(xform) for c in loop[-1]]
        transformed_loop = CurveLoop.Create(transformed_curves)
        loop.append(transformed_loop)
        count += 1
    return List[CurveLoop](loop)

def create_manhole_rebars(data, r_type, cover, walls, floor, doc):
    """Create the rebars for a manhole."""
    c1, c2, d2, c3 = cover
    wall_1, wall_2, wall_width = walls
    curveloop1 = create_vertical_curveloop(wall_1, c1, c2, d2)
    path1 = create_vertical_rebar_path_distribution(wall_2, c1)
    curveloop2 = create_vertical_curveloop(wall_2, c1, c2, d2)
    path2 = create_vertical_rebar_path_distribution(wall_1, c1)
    curveloop3 = create_horizontal_curveloop(wall_1, wall_2, c3)
    path3 = create_horizontal_rebar_path_distribution(wall_1, c3)
    loop1 = translate_curveloop_along_path(curveloop1, path2, data["S2"])
    loop2 = translate_curveloop_along_path(curveloop2, path1, data["S2"])
    loop3 = translate_curveloop_along_path(curveloop3, path3, data["S1"])

    # create rebars
    with Transaction(doc, 'create rebars') as t:
        t.Start()
        validationResult = clr.Reference[RebarFreeFormValidationResult]()
        rebar1 = Rebar.CreateFreeForm(doc, r_type, floor, loop1, validationResult)
        rebar2 = Rebar.CreateFreeForm(doc, r_type, floor, loop2, validationResult)
        rebar3 = Rebar.CreateFreeForm(doc, r_type, floor, loop3, validationResult)
        t.Commit()

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    form = manhole_rebar()
    if not form.data:
        return
    data = form.data
    r_type = get_rebar_type(data["d"], doc)
    walls = get_walls(doc)
    floor = get_first_floor(doc)
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    cover = get_covers_parameters(data, wall_width, floor_width, doc)
    create_manhole_rebars(data, r_type, cover, walls, floor, doc)

if __name__ == "__main__":
    main()


because you don’t need it! this is what I’m trying to explain from the beginning

Let’s recap:

  • in create_manhole_rebars you called the function with either walls[0] or walls[1] (using the previous notation here, but it works also with wall_1 and wall_2)
  • when you pass walls[0], the temp_line is built with walls[1]
  • when you pass walls[1], the temp_line is built with walls[0]

so this check is absolutely useless if you call the function directly with the other wall! no need for an if :wink:

And this is the same with the create_vertical_curveloop, you can just add another argument, let’s call it z_offset, and use that directly in the vectB

def create_vertical_curveloop(wall, c1, c2, d2, z_offset):
    ...
    vectB = XYZ(0, 0, z_offset)
    ...

def create_manhole_rebars(....):
    ...
    curveloop1 = create_vertical_curveloop(wall_1, c1, c2, d2, -c2)
    ...
    curveloop2 = create_vertical_curveloop(wall_2, c1, c2, d2, -d2)
    ...

No more need for walls (or wall_1 and wall_2) inside those functions, only on the main one.

Are you sure you copied the entire function? which line throws this exception, is it a line inside that function or elsewhere?

This is because of you’re still passing wall_width to get_covers_parameters in the main function, but you didn’t extract the width ftom the walls there.

Anyway, and sorry if I’m being pedantic (someday your future self will thank us both for that :wink:), but since the function is called get walls and not get_walls_and_the_width_of_the_first_wall :rofl: , it is less confusing to just return the walls and retrieve the width in the main function.

def get_walls(doc):
    """Return the first and second wall found in the document."""
    walls = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
    return walls[:2]  # this will return the first 2 items of the list

def main(doc=None):
    ...
    walls = get_walls(doc)
    wall_width = walls[0].Width
    ...

Obviously the create_manhole_rebars will have to be changed accordingly to accept the width as another parameter instead of extracting it from walls.

2 Likes

@sanzoghenzo

Oh, my bad! :crazy_face: It wasn’t any easier than that
 I was so focused on figuring out how to correlate the wall_1 and wall_2 definitions between the main function and their related functions that I completely overlooked how simple it was to pass the opposite_wall as an argument to get the appropriate path for the coresponding wall.

I added the z_offset in create_vertical_curveloop, as you suggested, and I think it enhances the function’s utility. However, instead of returning walls[:2] from the get_walls function, I opted to directly output wall_1 and wall_2 from the same function. I then use them as arguments across all functions in my code to improve their readability.

def get_walls(doc):
    """Return the first and second wall with their width found in the document."""
    walls = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
    wall_1 = walls[0]
    wall_2 = walls[1]
    #return walls[:2]
    return wall_1, wall_2


It took me some time to understand how to define things in the main function and use them in their respective functions. let’s me recap what I’ve understood, and correct me if I’m wrong.

if I want to retreive a variable from the doc and use it as an argument in a function, I have two options:
1- define the variable in the main function and use it directly as an argument in the coresponding function (as you did for floor_width used in get_covers_parameters function )

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    ....
    floor = get_first_floor(doc)
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    cover = get_covers_parameters(data, wall_width, floor_width, doc)
    ....
    

2- Define the variable (in this case, floor) as global in both main and the corresponding function, then use the derived parameter (in this case, floor_width) as local within the function, so we can wrote:

def get_covers_parameters(data, wall_width, floor, doc):
    """Cover parameters."""
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()
    c1 = wall_width / 2 + data["E1"]
    c2 = floor_width - data["E2"]
    d2 = c2 - UnitUtils.ConvertToInternalUnits(data["d"], UnitTypeId.Millimeters)
    c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
    return c1, c2, d2, c3

and

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    ....
    floor = get_first_floor(doc)
    cover = get_covers_parameters(data, wall_width, floor, doc)
    ....
    

so far I think my issue is solved but I still have some questions to deepen my knowledge:

1- Can you explain why you included doc = None in the main function?

2- how can I import directely the variables walls, wall_1, wall_2, wall_width, floor, floor_width from the script Tank_rscript and define them in the main function?

here my final script:

final script
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, os, clr
clr.AddReference("System")
from System.Windows import Window
from System import Array
from System.Collections.Generic import List, IList, Dictionary
import Autodesk
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import *
from Autodesk.Revit.UI import *
from pyrevit.forms import WPFWindow

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
results = clr.Reference[IntersectionResultArray]()

script_path = os.path.dirname(__file__)

class manhole_rebar(Window):
    def __init__(self):
        xaml_path = os.path.join(script_path, 'Tank_rebars.xaml')
        wpf.LoadComponent(self, xaml_path)
        self.select_rebar.ItemsSource = [10, 12, 14, 16, 20, 25, 32, 40]
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.e1_value.Text:
            forms.alert("Please enter a valid value for : e1")
        elif not self.e2_value.Text:
            forms.alert("Please enter a valid value for : e2")
        elif not self.H_spacing_value.Text:
            forms.alert("Please enter a valid value for : s1")
        elif not self.V_spacing_value.Text:
            forms.alert("Please enter a valid value for : s2")
            
        elif self.select_rebar.SelectedItem is None:
            forms.alert("Please select a rebar diameter : d")
        else:
            try:
                self.data = {
                    "E1": _input_to_centimeters(self.e1_value.Text),
                    "E2": _input_to_centimeters(self.e2_value.Text),
                    "S1": _input_to_centimeters(self.H_spacing_value.Text),
                    "S2": _input_to_centimeters(self.V_spacing_value.Text),
                    "d": self.select_rebar.SelectedItem
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        #self.data
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()

def _input_to_centimeters(value):
    """Convert a textual value to centimeters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Centimeters)

def get_walls(doc):
    """Return the first and second wall with their width found in the document."""
    walls = FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Walls).WhereElementIsNotElementType().ToElements()
    wall_1 = walls[0]
    wall_2 = walls[1]
    #return walls[:2]
    return wall_1, wall_2

def get_first_floor(doc):
    """Return the first floor found in the document."""
    return FilteredElementCollector(doc).OfCategory(
    BuiltInCategory.OST_Floors).WhereElementIsNotElementType().FirstElement()

def get_rebar_type(diameter, doc):
    """Return the rebar type associated to the given diameter."""
    rebar_name = "HA{} (Fe400)".format(diameter)
    rebar_types = (
        FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Rebar)
        .WhereElementIsElementType()
        .ToElements()
    )
    return next(r for r in rebar_types if Element.Name.GetValue(r) == rebar_name)

def get_covers_parameters(data, wall_width, floor_width, doc):
    """Cover parameters."""
    units = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId()
    c1 = wall_width / 2 + data["E1"]
    c2 = floor_width - data["E2"]
    d2 = c2 - UnitUtils.ConvertToInternalUnits(data["d"], UnitTypeId.Millimeters)
    c3 = UnitUtils.ConvertToInternalUnits(0.05, units)
    return c1, c2, d2, c3

# curveloop function to create the vertical rebar shape "|_| "
def create_vertical_curveloop(wall, c1, c2, d2, z_offset):
    """create vertical curvloop needed for vertical rebar shape"""
    temp_line = wall.Location.Curve
    p1 = -(d2-c2) / temp_line.Length
    lineB = Line.CreateBound(temp_line.Evaluate(p1, True), temp_line.Evaluate(1-p1, True))
    vectB = XYZ(0, 0, -z_offset)
    lineB = lineB.CreateTransformed(Transform.CreateTranslation(vectB))
    bbx = wall.get_BoundingBox(None)
    delta_z = bbx.Max.Z - bbx.Min.Z + z_offset
    vectZ = XYZ(0, 0, delta_z )
    lineA = Line.CreateBound(lineB.GetEndPoint(0).Add(vectZ), lineB.GetEndPoint(0))
    lineC = Line.CreateBound(lineB.GetEndPoint(1), lineB.GetEndPoint(1).Add(vectZ))
    curveloop = CurveLoop.Create(List[Curve]([lineA, lineB, lineC]))
    # Translate the curveloop1 to the start position before doing a multiple offset
    plane = curveloop.GetPlane()
    trans = Transform.CreateTranslation(plane.Normal.Negate().Multiply(c1))
    curveloop.Transform(trans)
    return curveloop

# curveloop function to create the horizontal rebar shape as a rectangle
def create_horizontal_curveloop(wall_1, wall_2, c3):
    """create horizontal curvloop needed for horizontal rebar shape"""
    points = []
    L1 = wall_1.Location.Curve.Length
    L2 = wall_2.Location.Curve.Length
    pt1 = wall_1.Location.Curve.GetEndPoint(0)
    points.append(pt1)
    pt2 = wall_1.Location.Curve.GetEndPoint(1)
    points.append(pt2)
    vect = (XYZ(pt2.X, pt2.Y, pt2.Z + L2)).Subtract(pt1)
    cross_direction = vect.CrossProduct(wall_1.Location.Curve.Direction).Normalize()
    pt3 = pt2 + cross_direction.Multiply(L2)
    points.append(pt3)
    pt4 = pt3 + wall_1.Location.Curve.Direction.Negate().Multiply(L1)
    points.append(pt4)
    lines = [Line.CreateBound(points[i], points[i + 1]) for i in range(len(points)-1)]
    lines.append(Line.CreateBound(pt4, pt1))
    # Horizontal curveloop "H_Loop" obtained
    H_Loop = CurveLoop.Create(lines)
    # Translate the curveloop2 to the start position before doing a multiple offset
    H_plane = H_Loop.GetPlane()
    H_trans = Transform.CreateTranslation(H_plane.Normal.Multiply(c3))
    H_Loop.Transform(H_trans)
    return H_Loop

def create_vertical_rebar_path_distribution(opposite_wall, c1):
    """Define path for vertical rebar distribution."""
    temp_line = opposite_wall.Location.Curve
    p_a = c1 / temp_line.Length
    return Line.CreateBound(temp_line.Evaluate(p_a, True), temp_line.Evaluate(1-p_a, True))

def create_horizontal_rebar_path_distribution(wall, c3):
    """Define path for horizontal rebar distribution."""
    bbx = wall.get_BoundingBox(None)
    h = bbx.Max.Z - bbx.Min.Z
    temp_pt = wall.Location.Curve.GetEndPoint(0)
    path = Line.CreateBound(XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + c3), XYZ(temp_pt.X, temp_pt.Y, temp_pt.Z + h - c3))
    return path

def translate_curveloop_along_path(curveloop, path, step):
    """Multiple translation of curveloop with given space and creating arraay list from curvloop """
    n = path.Length // step
    loop = [curveloop]
    if curveloop.GetPlane().Normal.IsAlmostEqualTo(XYZ.BasisZ):
        offset = curveloop.GetPlane().Normal.Multiply(step)
    else:
        offset = curveloop.GetPlane().Normal.Negate().Multiply(step)
    xform = Transform.CreateTranslation(offset)
    count = 0
    while count < n:
        transformed_curves = [c.CreateTransformed(xform) for c in loop[-1]]
        transformed_loop = CurveLoop.Create(transformed_curves)
        loop.append(transformed_loop)
        count += 1
    return List[CurveLoop](loop)

def create_manhole_rebars(data, r_type, cover, wall_1, wall_2, floor, doc):
    """Create the rebars for a manhole."""
    c1, c2, d2, c3 = cover
    curveloop1 = create_vertical_curveloop(wall_1, c1, c2, d2, c2)
    path1 = create_vertical_rebar_path_distribution(wall_2, c1)
    curveloop2 = create_vertical_curveloop(wall_2, c1, c2, d2, d2)
    path2 = create_vertical_rebar_path_distribution(wall_1, c1)
    curveloop3 = create_horizontal_curveloop(wall_1, wall_2, c3)
    path3 = create_horizontal_rebar_path_distribution(wall_1, c3)
    loop1 = translate_curveloop_along_path(curveloop1, path1, data["S2"])
    loop2 = translate_curveloop_along_path(curveloop2, path2, data["S2"])
    loop3 = translate_curveloop_along_path(curveloop3, path3, data["S1"])

    # create rebars
    with Transaction(doc, 'create rebars') as t:
        t.Start()
        validationResult = clr.Reference[RebarFreeFormValidationResult]()
        rebar1 = Rebar.CreateFreeForm(doc, r_type, floor, loop1, validationResult)
        rebar2 = Rebar.CreateFreeForm(doc, r_type, floor, loop2, validationResult)
        rebar3 = Rebar.CreateFreeForm(doc, r_type, floor, loop3, validationResult)
        t.Commit()

def main(doc=None):
    """Main entrypoint for the script."""
    doc = doc or revit.doc
    form = manhole_rebar()
    if not form.data:
        return
    data = form.data
    r_type = get_rebar_type(data["d"], doc)
    wall_1, wall_2 = get_walls(doc)
    wall_width = wall_1.Width
    floor = get_first_floor(doc)
    floor_width = floor.get_Parameter(BuiltInParameter.FLOOR_ATTR_THICKNESS_PARAM).AsDouble()
    cover = get_covers_parameters(data, wall_width, floor_width, doc)
    create_manhole_rebars(data, r_type, cover, wall_1, wall_2, floor, doc)

if __name__ == "__main__":
    main()


Thanks.

This is exactly the same thing; it doesn’t matter how you call the variables you return from a function, the names are not used outside it; only the values are passed.
Here you’re using the same names in the main function, but you don’t need to do that, it is not mandatory

In fact, a function could return values directly instead of named function, but you can then assign the values to separate values in the calling scope:

def something():
    return 1, 5

foo, bar = something()

this is known as “unpacking” in python (and other languages).

“global” is not the right word here, see variable scoping to get the terminology right
 floor is still a local variable of the main function, and, as I wrote earlier, the variable in the main function and the argument in the called function don’t need to have the same name, it is just for clarity (or not, since you seem confused by having the same name in different places :sweat_smile: ).
But the bottom line is more or less what you’re saying, you could move things around based on the needs of the functions

these are called keyword arguments; if you don’t pass anything to the function when you called it, then the doc argument starts with the None value. then the first line of the function makes sure that the doc is either a the value passed by the caller, if any, or else it uses the current open document. As I explained before, it is not needed, but can become handy if you then want to use this main function with many models at once without the need to make them the active ones.
you should remove the

uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document

at the beginning of the script because it isn’t used anymore (the revit.doc inside the main function does the same thing).

I’m not sure what are you asking
 do you want to chain the two commands together?

@sanzoghenzo
sorry for this late feeback

Let me explain:
In the Tank_rscript, I used a function called get_or_create_concrete_material to create a material. Can I isolate this function in a separate script and then call it in my main script, Tank_rscript, to make it shorter?
The same applies to other functions, such as get_walls and get_first_floor, which are derived from Tank_rscript and also used in the Tank Rebar Script. If this is possible, how can I do it?

Thanks.

Pliase, please, please, have a look a some python tutorials!!!

You can’t pretend that some random guy on the internet becomes you personal python instructor.

What you’re asking is one of the basic of python programming, and the fact that you didn’t find the answer yourself makes me think that you’re not even trying.

1 Like