Wall.Create with Profile (IList<Curve>) fails only on Level 1, but works perfectly on Level 2+ (Revit 2023)

Hi everyone,

I am developing a custom stair generator script using pyRevit in Revit 2023. The script generates stairs using a combination of Floors (for treads/slopes) and Walls (for risers and side profiles).

I’ve encountered a very strange issue: The side profile wall creation works perfectly when executed on Level 2 or higher, but it completely fails (or rolls back) when executed on Level 1 (or the lowest level of the project).

Here is the specific function I use to create the side profile wall:

# -*- coding: utf-8 -*-
import clr
import math
import os
import json
import codecs
import tempfile

# UI 라이브러리 참조
clr.AddReference('System.Windows.Forms')

from Autodesk.Revit.DB import *
from Autodesk.Revit.UI.Selection import ObjectSnapTypes
from pyrevit import revit, DB, script, forms
from System.Collections.Generic import List

doc = revit.doc
uidoc = revit.uidoc

# =======================================================
# 0. 설정 저장/로드
# =======================================================
CONFIG_FILE = os.path.join(tempfile.gettempdir(), "pyrevit_stair_config.json")

def load_config():
    default_config = {
        "height": "3000",
        "count": "15",
        "width": "1200",
        "depth": "280",
        "thick": "30",
        "offset": "0",
        "underside_drop": "180",
        "run_offset_base": "50",
        "finish_type": u"면손보기"   # 추가
    }

    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, "r") as f:
                saved_config = json.load(f)
                default_config.update(saved_config)
        except:
            pass
    return default_config

def save_config(data):
    try:
        with open(CONFIG_FILE, "w") as f:
            json.dump(data, f)
    except:
        pass


# =======================================================
# 1. UI 설정 (XAML)
# =======================================================
xaml_string = u"""
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="계단 생성기 (PyRevit)" Height="470" Width="380" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
    <StackPanel Margin="20">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="180"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="30"/>
                <RowDefinition Height="45"/>
            </Grid.RowDefinitions>

            <Label Grid.Row="0" Content="1. 총 높이 (mm):"/>
            <TextBox Name="tb_height" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center"/>

            <Label Grid.Row="1" Content="2. 계단 단 수 (개):"/>
            <TextBox Name="tb_count" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center"/>

            <Label Grid.Row="2" Content="3. 계단 폭 (mm):"/>
            <TextBox Name="tb_width" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center"/>

            <Label Grid.Row="3" Content="4. 디딤판 깊이 (mm):"/>
            <TextBox Name="tb_depth" Grid.Row="3" Grid.Column="1" VerticalAlignment="Center"/>

            <Label Grid.Row="4" Content="5. 몰탈 두께 (T):"/>
            <TextBox Name="tb_thick" Grid.Row="4" Grid.Column="1" VerticalAlignment="Center"/>

            <Label Grid.Row="5" Content="6. 시작 높이 (mm):"/>
            <TextBox Name="tb_offset" Grid.Row="5" Grid.Column="1" VerticalAlignment="Center"
                     ToolTip="참(Landing) 높이만큼 띄워서 시작할 때 입력"/>

            <Label Grid.Row="6" Content="7. 계단참 슬래브 두께 (mm):"/>
            <TextBox Name="tb_underside_drop" Grid.Row="6" Grid.Column="1" VerticalAlignment="Center"
                     ToolTip="계단 하부선보다 아래로 내릴 값 / 기본 180"/>

            <Label Grid.Row="7" Content="8. 진행방향 오프셋 (mm):"/>
            <TextBox Name="tb_run_offset_base" Grid.Row="7" Grid.Column="1" VerticalAlignment="Center"
                     ToolTip="최종 Run Offset = 입력값 + 몰탈두께 / 기본 50"/>

            <Label Grid.Row="8" Content="9. 하부 마감 타입:"/>
            <ComboBox Name="cb_finish_type" Grid.Row="8" Grid.Column="1" VerticalAlignment="Center">
                <ComboBoxItem Content="면손보기"/>
                <ComboBoxItem Content="수성페인트"/>
            </ComboBox>

            <Button Name="btn_ok" Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2"
                    Content="위치 지정 및 생성" Margin="0,12,0,0"/>
        </Grid>
    </StackPanel>
</Window>
"""

xaml_path = os.path.join(tempfile.gettempdir(), "pyrevit_stair_ui.xaml")
with codecs.open(xaml_path, "w", "utf-8") as f:
    f.write(xaml_string)

class StairWindow(forms.WPFWindow):
    def __init__(self):
        forms.WPFWindow.__init__(self, xaml_path)

        config = load_config()
        self.tb_height.Text = config["height"]
        self.tb_count.Text = config["count"]
        self.tb_width.Text = config["width"]
        self.tb_depth.Text = config["depth"]
        self.tb_thick.Text = config["thick"]
        self.tb_offset.Text = config["offset"]
        self.tb_underside_drop.Text = config["underside_drop"]
        self.tb_run_offset_base.Text = config["run_offset_base"]

        saved_finish = config.get("finish_type", u"면손보기")
        for item in self.cb_finish_type.Items:
            try:
                if unicode(item.Content) == saved_finish:
                    self.cb_finish_type.SelectedItem = item
                    break
            except:
                pass

        if self.cb_finish_type.SelectedIndex < 0:
            self.cb_finish_type.SelectedIndex = 0

        self.btn_ok.Click += self.btn_ok_Click

    def btn_ok_Click(self, sender, e):
        selected_finish = u"면손보기"
        if self.cb_finish_type.SelectedItem:
            try:
                selected_finish = unicode(self.cb_finish_type.SelectedItem.Content)
            except:
                pass

        current_data = {
            "height": self.tb_height.Text,
            "count": self.tb_count.Text,
            "width": self.tb_width.Text,
            "depth": self.tb_depth.Text,
            "thick": self.tb_thick.Text,
            "offset": self.tb_offset.Text,
            "underside_drop": self.tb_underside_drop.Text,
            "run_offset_base": self.tb_run_offset_base.Text,
            "finish_type": selected_finish
        }
        save_config(current_data)
        self.Close()


# =======================================================
# 2. 실행 및 데이터 수집
# =======================================================
try:
    window = StairWindow()
    window.ShowDialog()
except Exception as e:
    forms.alert(u"UI 로드 중 에러 발생:\n" + str(e))
    script.exit()

try:
    if not window.tb_height.Text:
        script.exit()

    def mm_to_feet(mm):
        return mm * 0.00328084

    total_height_mm = float(window.tb_height.Text)
    step_count = int(window.tb_count.Text)
    stair_width_mm = float(window.tb_width.Text)
    tread_depth_mm = float(window.tb_depth.Text)
    mortar_thick_mm = float(window.tb_thick.Text)
    start_offset_mm = float(window.tb_offset.Text) if window.tb_offset.Text else 0.0

    # 사용자 입력값
    underside_drop_mm = float(window.tb_underside_drop.Text) if window.tb_underside_drop.Text else 180.0
    run_offset_base_mm = float(window.tb_run_offset_base.Text) if window.tb_run_offset_base.Text else 50.0

    finish_type = u"면손보기"
    if window.cb_finish_type.SelectedItem:
        try:
            finish_type = unicode(window.cb_finish_type.SelectedItem.Content)
        except:
            pass

    # 최종 진행방향 오프셋 = 기본 오프셋 + 몰탈 두께
    underside_run_offset_mm = run_offset_base_mm + mortar_thick_mm

    mortar_t = mm_to_feet(mortar_thick_mm)
    half_mortar = mortar_t * 0.5
    start_offset_feet = mm_to_feet(start_offset_mm)
    underside_drop = mm_to_feet(underside_drop_mm)
    underside_run_offset = mm_to_feet(underside_run_offset_mm)

except ValueError:
    forms.alert(u"숫자 형식이 잘못되었습니다.")
    script.exit()


# =======================================================
# 3. 타입 찾기
# =======================================================
target_type_name = u"시멘트몰탈_{}T#계단실".format(window.tb_thick.Text)

def get_type_by_name(cls, name):
    collector = FilteredElementCollector(doc).OfClass(cls).WhereElementIsElementType()
    for elem in collector:
        try:
            p = elem.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM)
            elem_name = p.AsString() if p else None
            if not elem_name:
                elem_name = getattr(elem, "Name", None)
            if elem_name == name:
                return elem
        except:
            continue
    return None

target_wall_type = get_type_by_name(WallType, target_type_name)
target_floor_type = get_type_by_name(FloorType, target_type_name)

if not target_wall_type or not target_floor_type:
    forms.alert(u"타입을 찾을 수 없습니다: " + target_type_name)
    script.exit()

tile_floor_type = get_type_by_name(FloorType, u"자기질타일_300X300_계단실#10T_디딤판")
tile_wall_type  = get_type_by_name(WallType,  u"자기질타일_300X300_계단실#10T")
if finish_type == u"수성페인트":
    underside_floor_type_name = u"수성페인트#1T"
    underside_wall_type_name  = u"수성페인트_골조#1T"
else:
    underside_floor_type_name = u"면손보기(노출면)#1T"
    underside_wall_type_name  = u"면손보기(노출면)#1T"

underside_floor_type = get_type_by_name(FloorType, underside_floor_type_name)
underside_wall_type  = get_type_by_name(WallType,  underside_wall_type_name)

if not tile_floor_type or not tile_wall_type:
    forms.alert(u"타일 타입을 찾을 수 없습니다.")
    script.exit()

if not underside_floor_type:
    forms.alert(u'밑면 경사판용 FLOOR 타입을 찾을 수 없습니다: "{}"'.format(underside_floor_type_name))
    script.exit()

if not underside_wall_type:
    forms.alert(u'밑면 측면 마감용 WALL 타입을 찾을 수 없습니다: "{}"'.format(underside_wall_type_name))
    script.exit()

tile_t = mm_to_feet(10.0)
half_tile = tile_t * 0.5


# =======================================================
# 4. 위치 지정
# =======================================================
try:
    snap_opts = ObjectSnapTypes.Endpoints | ObjectSnapTypes.Intersections | ObjectSnapTypes.Nearest
    start_pt = uidoc.Selection.PickPoint(snap_opts, u"계단의 [시작점]을 클릭하세요.")
    end_pt = uidoc.Selection.PickPoint(snap_opts, u"계단이 진행될 [방향]을 클릭하세요.")
except Exception:
    script.exit()


# =======================================================
# 5. Transform 계산
# =======================================================
def get_transform(start_p, end_p):
    vec = end_p - start_p
    vec_flat = XYZ(vec.X, vec.Y, 0)

    if vec_flat.IsZeroLength():
        return Transform.Identity

    y_axis = XYZ.BasisY
    angle = y_axis.AngleTo(vec_flat)

    if y_axis.CrossProduct(vec_flat).Z < 0:
        angle = -angle

    return Transform.CreateTranslation(start_p).Multiply(
        Transform.CreateRotationAtPoint(XYZ.BasisZ, angle, XYZ.Zero)
    )

final_transform = get_transform(start_pt, end_pt)


# =======================================================
# 5-1. 밑면 경사판(Floor 1개) 생성
# =======================================================
def create_underside_slope_floor(doc, level, level_id, floor_type, final_transform,
                                 width, total_run, start_z, total_h, run_offset):
    """
    밑면 경사판을 Floor 1개로 생성
    - 타입: 면손보기(노출면)#1T
    - 경사 방향: 계단 진행방향(Y)
    - 시작 높이: start_z
    - 끝 높이: start_z + total_h
    - 진행방향 오프셋: run_offset
    """

    if total_run <= 0:
        raise Exception("total_run 값이 0 이하입니다.")

    slope = float(total_h) / float(total_run)

    # 계단 진행방향(Y) 오프셋 적용
    y0 = run_offset
    y1 = total_run + run_offset

    pts = [
        final_transform.OfPoint(XYZ(0,     y0, 0)),
        final_transform.OfPoint(XYZ(width, y0, 0)),
        final_transform.OfPoint(XYZ(width, y1, 0)),
        final_transform.OfPoint(XYZ(0,     y1, 0))
    ]

    arrow_start = final_transform.OfPoint(XYZ(width * 0.5, y0, 0))
    arrow_end   = final_transform.OfPoint(XYZ(width * 0.5, y1, 0))
    slope_arrow = Line.CreateBound(arrow_start, arrow_end)

    new_floor = None

    # 방법 1: Floor.Create 오버로드 시도
    try:
        profile = CurveLoop()
        for k in range(4):
            profile.Append(Line.CreateBound(pts[k], pts[(k + 1) % 4]))

        new_floor = Floor.Create(
            doc,
            [profile],
            floor_type.Id,
            level_id,
            False,
            slope_arrow,
            slope
        )

    # 방법 2: 구버전 fallback
    except Exception:
        try:
            ca = CurveArray()
            for k in range(4):
                ca.Append(Line.CreateBound(pts[k], pts[(k + 1) % 4]))

            new_floor = doc.Create.NewSlab(ca, level, slope_arrow, slope, False)

            if new_floor.GetTypeId() != floor_type.Id:
                new_floor.ChangeTypeId(floor_type.Id)

        except Exception as e:
            raise Exception(u"밑면 경사판(Floor) 생성 실패: {}".format(e))

    p_offset = new_floor.get_Parameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM)
    if p_offset and (not p_offset.IsReadOnly):
        p_offset.Set(start_z)

    return new_floor

from System.Collections.Generic import List

def create_left_side_profile_wall(doc, level_id, wall_type, final_transform,
                                  run_offset, step_count, tread, riser_h,
                                  stair_start_z, underside_start_z,
                                  total_h, mortar_t, tile_t, underside_drop):
    
    wall_t = wall_type.Width
    total_run = step_count * tread
    gap_mm = 0.5
    gap_feet = gap_mm *0.00328084
    
    # 진행방향(+Y) 기준 왼쪽
    x_loc = -wall_t * 0.5 - gap_feet
    
    y0 = run_offset
    y1 = run_offset + total_run
    
    # 수정 1: y00 계산
    y00 = y0 - run_offset + mortar_t
    
    final_base_z = underside_start_z
    
    pts_local = []
    
    # 하부 시작/끝 (밑면 경사선)
    pts_local.append(XYZ(x_loc, y0, 0.0))
    pts_local.append(XYZ(x_loc, y1, total_h))
    
    # 수정 2: top_drop 대신 - tile_t 적용
    # 상부 끝점
    top_last_abs_z = stair_start_z + total_h + mortar_t - mortar_t + tile_t - tile_t
    top_last_rel_z = top_last_abs_z - final_base_z
    pts_local.append(XYZ(x_loc, y1, top_last_rel_z))
    
    # 계단 단차 형상
    for i in reversed(range(step_count)):
        # 수정 3: step_y0 계산 수정
        step_y0 = y00 + i * tread

        step_top_abs_z = stair_start_z + (i + 1) * riser_h + mortar_t - mortar_t + tile_t - tile_t
        step_top_rel_z = step_top_abs_z - final_base_z
        pts_local.append(XYZ(x_loc, step_y0, step_top_rel_z))
        
        if i > 0:
            prev_step_abs_z = stair_start_z + i * riser_h + mortar_t - mortar_t + tile_t - tile_t
            prev_step_rel_z = prev_step_abs_z - final_base_z
            pts_local.append(XYZ(x_loc, step_y0, prev_step_rel_z))
            
    # 수정 1: 꼭지점 하나를 추가하여 닫힌 프로파일(Closed Loop) 완성
    pts_local.append(XYZ(x_loc, y00, 0.0))
    
    # world 좌표 변환
    pts_world = [final_transform.OfPoint(p) for p in pts_local]
    
    # 닫힌 프로파일 작성
    profile = List[Curve]()
    for i in range(len(pts_world)):
        p1 = pts_world[i]
        p2 = pts_world[(i + 1) % len(pts_world)]
        if not p1.IsAlmostEqualTo(p2):
            profile.Add(Line.CreateBound(p1, p2))
            
    bx = final_transform.BasisX
    wall_normal = XYZ(-bx.X, -bx.Y, -bx.Z)
    
    new_wall = Wall.Create(
        doc,
        profile,
        wall_type.Id,
        level_id,
        False,
        wall_normal
    )
    
    doc.Regenerate()
    
    # 벽 전체 Z 위치 지정
    p_base = new_wall.get_Parameter(BuiltInParameter.WALL_BASE_OFFSET)
    if p_base and (not p_base.IsReadOnly):
        p_base.Set(final_base_z)
        
    doc.Regenerate()
    
    return new_wall

# =======================================================
# 6. 생성
# =======================================================
total_h = mm_to_feet(total_height_mm)
width = mm_to_feet(stair_width_mm)
tread = mm_to_feet(tread_depth_mm)
riser_h = total_h / step_count

active_view = doc.ActiveView
level = active_view.GenLevel

if not level:
    p = active_view.get_Parameter(BuiltInParameter.PLAN_VIEW_LEVEL)
    if p and p.AsElementId() != ElementId.InvalidElementId:
        level = doc.GetElement(p.AsElementId())

if not level:
    level = forms.SelectFromList.show(
        sorted(FilteredElementCollector(doc).OfClass(Level), key=lambda x: x.Elevation),
        name_attr="Name",
        title=u"기준 레벨 선택"
    )

if not level:
    script.exit()

level_id = level.Id

t = Transaction(doc, "Create User Defined Stairs")
t.Start()

try:
    current_loc_y = 0.0
    current_z = start_offset_feet

    for i in range(step_count):
        # A. 챌판 (Wall)
        local_p1 = XYZ(0, current_loc_y + half_mortar, 0)
        local_p2 = XYZ(width, current_loc_y + half_mortar, 0)
        p1 = final_transform.OfPoint(local_p1)
        p2 = final_transform.OfPoint(local_p2)

        new_wall = Wall.Create(
            doc,
            Line.CreateBound(p1, p2),
            target_wall_type.Id,
            level_id,
            riser_h,
            0,
            False,
            False
        )
        new_wall.get_Parameter(BuiltInParameter.WALL_BASE_OFFSET).Set(current_z)

        # B. 디딤판 (Floor)
        floor_z = current_z + riser_h + mortar_t
        y0 = current_loc_y
        y1 = current_loc_y + tread

        pts = [
            XYZ(0, y0, 0),
            XYZ(width, y0, 0),
            XYZ(width, y1, 0),
            XYZ(0, y1, 0)
        ]
        w_pts = [final_transform.OfPoint(p) for p in pts]

        profile = CurveLoop()
        for k in range(4):
            profile.Append(Line.CreateBound(w_pts[k], w_pts[(k + 1) % 4]))

        try:
            new_floor = Floor.Create(doc, [profile], target_floor_type.Id, level_id)
        except AttributeError:
            ca = CurveArray()
            for k in range(4):
                ca.Append(Line.CreateBound(w_pts[k], w_pts[(k + 1) % 4]))
            new_floor = doc.Create.NewFloor(ca, target_floor_type, level, False)

        new_floor.get_Parameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM).Set(floor_z)

        # C. 챌판 타일 (Wall)
        tile_y = current_loc_y - half_tile
        tp1 = final_transform.OfPoint(XYZ(0, tile_y, 0))
        tp2 = final_transform.OfPoint(XYZ(width, tile_y, 0))

        t_wall = Wall.Create(
            doc,
            Line.CreateBound(tp1, tp2),
            tile_wall_type.Id,
            level_id,
            riser_h,
            0,
            False,
            False
        )
        t_wall.get_Parameter(BuiltInParameter.WALL_BASE_OFFSET).Set(current_z + mortar_t)

        # D. 디딤판 타일 (Floor)
        ty0 = current_loc_y - tile_t
        ty1 = current_loc_y + tread - tile_t

        t_pts = [
            XYZ(0, ty0, 0),
            XYZ(width, ty0, 0),
            XYZ(width, ty1, 0),
            XYZ(0, ty1, 0)
        ]
        tw_pts = [final_transform.OfPoint(p) for p in t_pts]

        t_profile = CurveLoop()
        for k in range(4):
            t_profile.Append(Line.CreateBound(tw_pts[k], tw_pts[(k + 1) % 4]))

        try:
            t_floor = Floor.Create(doc, [t_profile], tile_floor_type.Id, level_id)
        except AttributeError:
            ca = CurveArray()
            for k in range(4):
                ca.Append(Line.CreateBound(tw_pts[k], tw_pts[(k + 1) % 4]))
            t_floor = doc.Create.NewFloor(ca, tile_floor_type, level, False)

        t_floor.get_Parameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM).Set(floor_z + tile_t)

        current_loc_y += tread
        current_z += riser_h

    # =======================================================
    # E. 밑면 경사판 (Floor 1개)
    # 타입: 면손보기(노출면)#1T
    # 위치:
    #   - 계단 하부선보다 underside_drop_mm 만큼 아래
    #   - 진행방향으로 (run_offset_base_mm + mortar_thick_mm) 만큼 이동
    # =======================================================
    total_run = step_count * tread

    underside_start_z = start_offset_feet  - underside_drop

    create_underside_slope_floor(
        doc=doc,
        level=level,
        level_id=level_id,
        floor_type=underside_floor_type,
        final_transform=final_transform,
        width=width,
        total_run=total_run,
        start_z=underside_start_z,
        total_h=total_h,
        run_offset=underside_run_offset
    )

    # 최상단 타일 윗면 높이
    left_finish_top_z = start_offset_feet + total_h + mortar_t + tile_t

    create_left_side_profile_wall(
    doc=doc,
    level_id=level_id,
    wall_type=underside_wall_type,
    final_transform=final_transform,
    run_offset=underside_run_offset,
    step_count=step_count,
    tread=tread,
    riser_h=riser_h,
    stair_start_z=start_offset_feet,
    underside_start_z=underside_start_z,
    total_h=total_h,
    mortar_t=mortar_t,
    tile_t=tile_t,
    underside_drop=underside_drop
    )

    t.Commit()



except Exception as e:
    t.RollBack()
    forms.alert(u"에러 발생: " + str(e))

The Question:

Is there a known limitation or specific constraint behavior with Wall.Create using a profile (IList) when created near the project’s base level (Elevation 0) or below it? Why would the exact same geometric logic work flawlessly on Level 2 but fail on Level 1?