Efficiently Find Door Open Direction – 即時得到門開方向

When using Revit, have you ever ask yourself: How could I know the opening direction of a door? This question emerged while I was working on a project in its execution phase, and it was a huge task to deliver a door list with the full information about all doors – including door opening directions.

While taking on this issue, I have come to three relevant questions: How could one know efficiently, that a door family instance has been flipped by using the flipping handle in the family instance and/or mirrored by using the mirror function? Is there any difference between using flipping handle and the mirror without copy function? How does the mirror function affect the family which has no flipping handles? These questions are probably especially interesting for those who have happened to be the former users of AutoCAD’s blocks, someone like me.

After a few simple tests, many try-and-errors, and some “looking-into”, I might can answer these questions. Let’s begin with questions number two and three, and the answer is that by using mirroring without copy one creates actually a new family instance with new element id whose flipping parameters document in which direction this element is flipped, no matter if this family has flipping handle or not. As to the question number one, it might help at the answer with a code example:

type FindDoorOpenDirection() as this =
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let dir = Left
      let selected =
        uidoc.Selection.GetElementIds() |> Seq.map uidoc.Document.GetElement
        |> Seq.filter(
          fun s ->
            match s with
            | :? FamilyInstance -> true
            | _ -> false
        ) 
        |> Seq.filter(
          fun s ->
            match s.Category.Id.IntegerValue with
            | x when x = int BuiltInCategory.OST_Doors -> true
            | _ -> false
        ) |> List.ofSeq
      match selected with
      | [] -> msg <- "Select door(s)"; Result.Cancelled
      | _ ->
        let t = new Transaction(uidoc.Document, string this)
        t.Start() |> ignore
        selected |> Seq.cast<FamilyInstance>
        |> Seq.map(
          fun dr ->
            let opendir = 
              match dr.HandFlipped, dr.FacingFlipped with
              | true, true | false, false -> dir
              | _, _ -> dir.ops
            let par = dr.get_Parameter(BuiltInParameter.DOOR_NUMBER)
            par.Set(par.AsString() + " | " + string opendir)
        ) |> List.ofSeq |> ignore
        t.Commit() |> ignore
        Result.Succeeded

In this example, we see the usual selection filtration (line ), for the type match, each of the selected elements is compared with its type against FamilyInstance, since the door elements are created family elements. However, this includes also the other building elements, such as windows. For specifying the door element filtration, we have to filter the selected with its category against the door category – to be exact, it’s the IntegerValue of the category id that we are comparing here.

Before we determine in which direction a door family instance opens, we have to define, of course, the default situation of the certain family type – since it’s the default situation of the door type to be newly set in the model scene – here we say that the default door family type has been drawn as a left side opened door. To know how the door instance has been mirrored or flipped from its default to its now status, all we need to retrieve are just two of its parameters – HandFlipped and FacingFlipped. (line ) By comparing them with boolean operants, if both of them are true or false at the same time, the door has the same status as the default, and vice versa.

Before I introduce the following handy type, let’s finish the main code at writing the retrieved door open status into its door number parameter additionally. Here it is set that every time when we re-run this code, the the information of door opening direction will be appended to the existing text information in the door number parameter. Surely this is something to be defined according to the project requirement.

type OpenDirection =
  | Left
  | Right
  member x.ops = 
    match x with
    | Left -> Right
    | Right -> Left

This one handy “discriminated union” type exists for helping me to treat each status of door opening direction as a union case. When we create a value of this type, it can either be “Left” or “Right (these are just tags or case identifier with no sub-type.) It is implemented in the line of our main code and assigned to the value “dir“, which is set as default to the case of Left. “dir” has a function member “ops“, which, when called, will toggle the status of “dir” to the opposite status. (line ) It is used in the line of the main code, while we check the combination of the two kinds of flipping status of a door instance, as explained above.

Efficiently View Room Reference – 有效率地快速顯現與隱藏房間的參考

Are you tired of repeatedly taping the tab key for selecting hidden room references? And you are doing so because of the applied view template, which you don’t want to switch to none and back, because it will take too long and you’ve got a deadline to make? Then all you’ll need is a efficient way to just quickly switch the visibility of the room reference on and off.

你厭倦了反覆地按同一 tab 鍵只為了選取房間量體的參考十字嗎?而你這麼做只是為了不想把已經設定好的視圖樣版在視圖控制取不取消之間轉換,而浪費寶貴的趕圖時間?若是,你需要的只是一個快速的房間參考顯示轉換機制。

The following code gives you an overview to see how to manipulate the active view by switch the category’s visibility. Surely, it depends on how the status of your currently view setup is. In this short code, we have two scenarios, with or without view template, as examples.

下面的編碼將給你一個概念,我們可以如何利用品類的顯示與否來控制當下視圖。當然,這是視你當下視圖的預設設定而定。在此簡短的編碼中,我們可先設定兩個預定情況,已設或未設視圖樣版,當作示範。


type ViewRoomReference() as this = 
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument        
      let catRoom = 
        uidoc.Document.Settings.Categories
        |> Seq.cast<Category>
        |> Seq.filter(fun cat -> cat.Name = "Rooms")
        |> Seq.head
      let subcats =
        catRoom.SubCategories |> Seq.cast<Category>
        |> Seq.filter(
          fun subcat ->
            subcat.Name = "Reference" ||
            subcat.Name = "Interior Fill"
        )
      match uidoc.ActiveView with
      | :? View3D ->
        msg <- "3D View"
        Result.Cancelled
      | _ ->
        let catAll = [catRoom] @ (subcats |> List.ofSeq)
        let isStat = catAll |> List.map(fun cat -> uidoc.ActiveView.GetCategoryHidden(cat.Id))
        let toggle() =
          match isStat with
          | [false; false; false] ->
            catAll 
            |> List.iter(fun cat -> uidoc.ActiveView.SetCategoryHidden(cat.Id, true))
          | _ ->
            catAll 
            |> List.iter(fun cat -> uidoc.ActiveView.SetCategoryHidden(cat.Id, false))
        let t = new Transaction(uidoc.Document, string this)
        t.Start() |> ignore
        match uidoc.ActiveView.ViewTemplateId with
        | x when x = ElementId.InvalidElementId ->
          toggle()
        | _ ->
          match uidoc.ActiveView.IsTemporaryViewPropertiesModeEnabled() with
          | true ->
            uidoc.ActiveView.DisableTemporaryViewMode(
              TemporaryViewMode.TemporaryViewProperties
              )
          | false ->
            uidoc.ActiveView.EnableTemporaryViewPropertiesMode(
              uidoc.ActiveView.ViewTemplateId
              ) |> ignore
            toggle()                       
        t.Commit() |> ignore
        Result.Succeeded

Our goal is to turn on the visibility of the room category in active view, and its sub-categories “interior fill” and “reference” as well, so that we can easily recognize where the room reference points are and select the room body which we want. If the active view is not under the control of a view template, it’s simple to turn on and off the categories related to rooms. However, even if it is, we can still set the view under the “temporary view properties” mode, and toggle the visibility of the room categories.

我們的目標,是在當下的視圖中,快速地顯示或隱藏房間品類與其下《内部填滿》和《參考》的兩個次品類;而藉此,我們可容易地辨識房間的參考點在何處,進而選取我們要的房間量體,以達有效率地進行編輯。如果現下的視圖並未被試圖樣板控制,開啟或關閉與房間相關的品類是相當簡單的。若被控制,我們還是可將視圖設於暫時視圖性質模式之下,再進行房間品類的顯示編輯。

Of course, according to different workflows or office standards, the process can be customized according to the needs. The most important is, take your time to discover your own efficient way in Revit modeling.

當然,這是視各個不同的工作方式與公司案件標準而定。這個進程是可視不同需要,而量身定作的。重點是,我們應當利用一點時間,你可以發掘屬於自己最有效率的建模方式。

Summed Length of Linear Element

線型元件的長度總和

還記得我們上一回的文章中,我們探討了如何取得線段長總和,還有以相似的方法取得空間的面積總和?我們也在上述的第二篇中,加入了自動依照案件的長度單位,換算成符合的面積單位。

Remember in the last post, where we discussed about how we retrieve the summed length of curve elements? After that post, we had a similar one, where the summed value is the retrieved area of room instead of length? And we also had a improved function, with which the report of value is determined automatically according to the current project unit. 

在本篇中,我們更利用從 F# 中相當方便的功能,將上述的程式作改變與加強。我們將取得 “線型元件”的總長度,例如在此範例中,曲線元件、牆面與樑架構元件的長度總和。

Now, in this post, we are also going to do some improvement, by using some of the convenient F# functions. We are going to retrieve the summed length of linear elements, such as, in this example, we combined the length of curve elements, wall and structural framing elements. 

type FindSumLengthOfLinearElement() as this = 
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let selected =
        uidoc.Selection.GetElementIds() 
        |> Seq.map uidoc.Document.GetElement
        |> Seq.map(
          fun e ->
            match e with
            | :? CurveElement as ce -> Some ce.GeometryCurve
            | :? Wall as w -> Some (w.Location:?>LocationCurve).Curve
            | :? FamilyInstance as fi ->
              match fi.Category.Id.IntegerValue with
              | x when x = int BuiltInCategory.OST_StructuralFraming -> 
                Some (fi.Location:?>LocationCurve).Curve
              | _ -> None
            | _ -> None
        )
        |> Seq.filter(
          fun opt -> opt.IsSome
        ) |> List.ofSeq
      match selected with
      | [] -> 
        msg <- "Select line(s) / Wall(s)."
        Result.Cancelled
      | _ ->
        let unitlen =
          selected
          |> List.map(fun opt -> opt.Value.Length)
          |> List.sum
          |> fsMath.toCurrentUnits uidoc.Document 1.0
        let txt =
          let uSys, sum = unitlen
          match uSys with
          | fsMath.Metric ->
            sum |> fsMath.toRoundUp 4.0 |> string |> fun x -> x + " m"
          | fsMath.Imperial ->
            sum |> fsMath.toRoundUp 4.0 |> string |> fun x -> x + " ft"
        TaskDialog.Show(string this, string txt) |> ignore
        Result.Succeeded

如同上面的編碼所展現的,這其實是與取得曲線線長總和的程式非常相似。但不同之處在於,如何確認何種物件應當被選取(14行)。在此步驟,不同於之前在直接求線長程式中的過濾 (filter)功能,我們使用序列(Seq)的映射(map)功能。此功能將輸入的物件映射於它們的代表曲線作為輸出,而這取決於我們要選出的類型:第一與第二兩類,也就是曲線元件與牆體(16、17 行)。這一功能將映射出一包含其幾何曲線的 Some 物件 –  Some 屬於在 F# 語言中的選擇權 Option 性質物件。而其所包含的曲線,即為從曲線物件中取得的幾何曲線(GeometryCurve)或從牆體所得的地點曲線(LocationCurve)。或是第三類,結構框架(StructuralFraming)。這一類將經由下列方式被決定:第一,物件是否屬於家族物件(FamilyInstance) ,然後再由其品類決定它是否是結構框架。最後,在此映射的程式中,因為 F# 是一 “type safe“的程式語言,就算輸入的物件不符合上述的任一類別,我們還是必須要與以 Some 物件同類的輸出。而此時,這程式將以另一同 Option 的物件回應:None。在接下來的選取過程中,我們將可在選項物件上用 “Seq.filter” 直接過濾出那些含有 ”IsSome“ 即是我們要的:曲線,在此一曲線中,將含有線型物件長度的屬性。

As here demonstrated, it is quite similar to the code for the summed length of curve elements. However, it is different at the part, determining which kind of elements are supposed to be selected (line 14). In this step, we use, instead of the “filter“, we use the “map” function of Seq. This function maps the elements to their representative curves according to our chosen types: If the element is, of the first and second types, i.e. of Curve Element or Wall types (line 16, 17), the function returns a “Some“, an F# Option instance, with a value of Curve, which is retrieved from the element’s GeometryCurve or LocationCurve properties, respectively. Or it is of the third type, StructuralFraming, which is being determined by, firstly, if the element is a FamilyInstance, then, secondly,  if its category is StructuralFraming. Finally in this mapping function, since F# is a “type safe” language, if none of these three types is matched, we still have to have the same type as Some returned, the function will return another Option instance: None. In the following selection process, we can then filter out what we want – the curves, which have the length of the linear elements, by using “Seq.filter” on the option objects, if its “IsSome” property is true.

至此,若不論那被過濾和選取後的物件還仍不是曲線,此程式已接近完成。我們要等待曲線還仍被包裹在屬於 “Option” 類的 “Some” 的物件中。但擷取它們只需要呼喚出在 ”Some“ 中的 ”Value“ 屬性,而長度正是此 ”Value“(也就是 Curve) 的眾多屬性之一(35行)。

Until this point, this code is almost completed, except that the filtered and selected elements are not curves yet. They are still “packed” in the Some instance of the Option type. But to get them is just simply by calling the Value property of Some, and the Length is directly an property of this Value, i.e. the Curve (line 35). 

勿忘參考 / References:
Start up an F# solution for Revit plug-in / 在 Visual Studio 中用 F# 編程 Revit 外部指令
Run external command in Revit / 加入 Revit 外部指令
Start coding F# library for Revit / 開始編程 Revit 的 F# 指令集

Rhino Plug-in with F#

A few years back, when I was writing my thesis, titled “Permeation”, about philosophy, architectural geometry and programming, RhinoCommon, Grasshopper and Python were the great helpers to bring my ideas into the light. Since then, they are essential parts for my work along with Revit. When F# came to my life, it brought my understanding of programming further into a broader view and another level.

F# is a mature, open source, cross-platform, functional-first programming language. It empowers users and organizations to tackle complex computing problems with simple, maintainable and robust code.

F# Software Foundation

Parallel to introductions about writing external commands for Revit with Revit API in F# or Python, I’d like to share writing commands for Rhino with RhinoCommon in both languages as well.

Let’s start with adapting a Python code example from the Rhino Developer Docs samples: Custom Getpoint. We will build a dynamic-link library (.dll) as plug-in for the newest version Rhino 6, with its RhinoCommon.dll and Rhino.UI.dll as References. For building an dll file, we can use Visual Studio Community as coding environment, and you can find a similar post about setting it up. However, the difference is just instead of references from Revit, we’ll have to use the ones from Rhino 6. 

namespace TYB.RHN.DOES
type PlugIn() =
  class
    inherit Rhino.PlugIns.PlugIn()
  end

Just like coding with Revit API – as mentioned in this previous post, we have to set the TransactionAttribute and using IExternalCommand interface for each external command – and here we’ll have to write, for RhinoCommon, at least, two types with similar setups – first, a type inheriting PlugIn as base class (code above) and, second, a type inheriting Command as base class (code below). We can have many of the types that inherit Command class, but we must have one type which inherits the PlugIn class within one project, i. e.  within one dynamic-link library (.dll) file.

namespace TYB.RHN.Does
open Rhino
open Rhino.Geometry
open Rhino.Input
type DrawDynamicLines() as this =
  inherit Rhino.Commands.Command()
  override x.EnglishName = string this
  override x.RunCommand(doc, mode) =
    RhinoApp.WriteLine(string this)
    let gp0 = RhinoGet.GetPoint("Pick a point", false)
    let gp1 = RhinoGet.GetPoint("Pick 2nd point", true)
    let points =
      match gp0, gp1 with
      | (_, pt0), (_, pt1) ->
        [pt0; pt1]
    let gp2 = new Rhino.Input.Custom.GetPoint()
    gp2.DynamicDraw.Add(
      fun arg ->
        arg.Display.DrawLine(points.[0], arg.CurrentPoint, System.Drawing.Color.AliceBlue)
        arg.Display.DrawLine(points.[1], arg.CurrentPoint, System.Drawing.Color.HotPink)
    )
    gp2.Get()
    doc.Views.Redraw()
    Commands.Result.Success 

Just as I felt, as I slowly learnt how to write in F# with the understanding of Python, translating from Python code to F# is easy. The most different part might be to declare a type, to inherit a class and override the name of this command and the RunCommand() method. Since I didn’t have to do those. If you’re using Python editor or writing Python script in Grasshopper, these processes are not necessary, since these are already taken care of.

The process of this code is clear and similar to the one in Python. By overriding the EnglishName, we can set the name of this command to be called in Rhino command line and under RunCommand we implement what this code is actually doing. By using RhinoGet, the user enter the two points on screen and the third one we set for the CurrentPoint, where your mouse cursor is located, from the event argument of DynamicDraw event.

Finally, build this solution in Visual Studio, you’ll have a dynamic-link library file (.dll). In Rhino 6 under Option >> Plug-in, you can import this command and call it from the command line.

Efficiently At: Compiling Python Code as External Command / 裝載 Python 編碼成 Revit 外部程式

幾年前,當我剛開始使用 Revit,用我當時寫論文時,從使用 Python 與 RhinoCommon 的經驗來使用 Revit API 時,我接觸到兩個非常有用的外部程式:pyRevit 和 RevitPythonShell。感謝此兩程式的作者:Ehsan Iran-Nejad 和 Daren Thomas;也感謝那些給予深刻經驗的許多部落格作者(其中,Jeremy Tammik 的 The Building Coder),還有許許多多在 Stack Overflow 的問題解答,而也解答了我在當時有的相關疑惑。從那時起,我學習了許多,也在閒暇之餘寫了很多之後在工作上很有效率的工具。此外,也因為這些經驗,我才能用 F# 寫出許多外部指令與程式。

A couple years back, when I started to use Revit and Revit API with my limited Python knowledge from using RhinoCommon for my Master thesis, I’ve got to contact with two very useful external applications – pyRevit and RevitPythonShell. Many thanks to their creators – Ehsan Iran-Nejad and Daren Thomas, respectively, and also thanks to those bloggers (among them – The Building Coder from Jeremy Tammik) who give numerous insights, and uncountable replies on Stack Overflow to those questions of problems which I happened also to encounter, I have since learnt and written many Python codes in my free time and use them daily at work. Furthermore, because of these experiences I can now write the stand-alone external commands and applications for Revit with F#.

最近,作了一些搜尋,參考此二程式,也用 F# 寫出了一個簡單的 Python 編碼裝載器。此一,裝載了 Python 的編碼,結合 IronPython 以一外部指令進入 Revit 的軟件環境中。

Recently, I have done a little research,  took these two programs from above as references and wrote a simple Python code loader in F#, which loads a Python script into Revit as an external command, combining with IronPython.

open IronPython.Compiler
module loadPythonScript =
  let loader(cdata:ExternalCommandData)(pth:string) =
    let lst = [("Frames", true:>obj); ("FullFrames", true:>obj); ("LightweightScoped", true:>obj)]
    let dic = Seq.ofList lst |> dict

    // IronPython Engine
    let engine = IronPython.Hosting.Python.CreateEngine(options = dic)
    engine.Runtime.LoadAssembly(typeof<Autodesk.Revit.DB.Document>.Assembly)
    engine.Runtime.LoadAssembly(typeof<Autodesk.Revit.UI.Result>.Assembly)

    // Builtin Module
    let mdlBuiltin = IronPython.Hosting.Python.GetBuiltinModule(engine)
    mdlBuiltin.SetVariable("uiapp", cdata.Application)
    mdlBuiltin.SetVariable("__window__", 0)

    // Scope
    let scope = IronPython.Hosting.Python.CreateModule(engine, "__main__")
    scope.SetVariable("d", d)
    //scope.SetVariable("msg", msg)
    scope.SetVariable("res", Result.Succeeded)
    scope.SetVariable("__file__", 0)

    // Script
    let script = engine.CreateScriptSourceFromFile(path = pth, encoding = System.Text.Encoding.UTF8, kind = Microsoft.Scripting.SourceCodeKind.Statements)

    // Compile
    let optCompiler = engine.GetCompilerOptions(scope):?>PythonCompilerOptions
    optCompiler.ModuleName <- "__main__"
    optCompiler.Module <- IronPython.Runtime.ModuleOptions.Initialize

    // Command
    let command = script.Compile(optCompiler)

    scope, script, command

[<TransactionAttribute(TransactionMode.Manual)>]
type LoadPythonScript() =
  interface IExternalCommand with 
    member x.Execute(cdata, msg, elset) =
      let pth = @"C:\pythonRunByFSharp.py"

      let scope, script, command = loadPythonScript.loader cdata pth
      match command with
      | null -> Result.Cancelled
      | _ ->
        try
          script.Execute(scope)
          Result.Succeeded
        with
        | :? System.Exception as exn ->
          Result.Cancelled

這個編程分於兩部分:第一部分是用 IronPython 裝載 Python 編碼(行5),而第二是主要的執行外部指令編程。分兩部分的原因,是讓這第一部分的模距,可在其他的外部指令中,只需再鍵入不同的 Python 編碼檔案路徑,被重複利用。在第一部分中,先建立 IronPython 引擎(行15),預設模組(行20)和領域(行25),然後,從此部分輸出的,是在第二部分會使用到的:建立的領域(scope)、IronPython 引擎從 Python 編碼讀取成的 C# 底稿(script)和編譯的 C# 指令(command)。再接下來的步驟中,先確定在此三物件中,編譯的指令不是空集合(行55),在藉由 F# 的 “try… with…” (行56)來確定,倘若在執行底稿時,有錯誤發生時,主要外部指令應該如何反應(行59)。

寫到這裡,必須要說,這個程式碼還是相當粗糙。使用起來,還是沒有像 pyRevit 或 RevitPythonShell 成熟。這個編碼只是讓我們可簡單地,在 Revit 中,使用 Python 編碼。我們若有時間,在許多方面還是必須要加強(譬如說,引入其它的 Python 工具,或自動完成文字輸入(auto-complete)),使它更加完備。

It has two parts. First one is loading Python code with IronPython (line 5). The other one is our main external command. The reason to split it into two parts is it allows us to re-use the first part in other F# external command just by giving different path towards other Python code files. In the first part, we set up a IronPython engine (line 15), built-in module (line 20) and scope (line 25). Then, output from the first part would be the variables used in the next part – the built scope and C# script, read by IronPython engine, and the command, compiled by the engine. In the next step, be sure that within these three variables, the command is not “null” (line 55), and then use “try… with…” (line 56) from F# to instruct the program how it should response, when any error or exception once happened (line 59).

Writing til this point, must say, this code is actually simple, and thus quite rough. Using it doesn’t feel as smoothly as the other mature programs like pyRevit or RevitPythonShell. This program just simply loads the Python code through IronPython into Revit. Once we have time, there are many points to be improved, such as importing other Python tools or auto-completion… etc.

下面,是一 Python 編碼範例。其中值得注意的是,“uiapp” 是一預設程式變數。此一變數是經由主要執行外部指令編程的第21行,經由 IronPython 預設模組與 Python 編碼連結的。只需將此範例存為一 pythonRunByFSharp.py 於 C 硬碟底下(當然,命名與檔案位置可自由決定,但必須與符合第50行。)在 Revit 中,即可藉由執行外部指令,執行 Python 編碼。

Below, it is a Python code example. Note, “uiapp” is a pre-set variable, which is connected between the external command through the built-in module in line 21, with the python code. Just save this code under C drive, named as pythonRunByFSharp.py – of course, name and location are freely to decide, however, it must correspond to the setting in line 50.

import Autodesk.Revit.DB as db
import Autodesk.Revit.UI as ui
uidoc = uiapp.ActiveUIDocument

idsel = uidoc.Selection.GetElementIds()
sel = [uidoc.Document.GetElement(id) for id in idsel]
txt = "\n".join([s.Name for s in sel])
ui.TaskDialog("FSharp runs Python code", txt)

勿忘參考 / References:
Start up an F# solution for Revit plug-in / 在 Visual Studio 中用 F# 編程 Revit 外部指令
Run external command in Revit / 加入 Revit 外部指令
Start coding F# library for Revit / 開始編程 Revit 的 F# 指令集

View Control for Design Option Elements / 圖形取代設計選項中的物件顏色

        接下來,來討論該操控物件(Element)在視圖中如何顯示的問題。譬如,給予與其他類似物件不同的顏色。這裏,我以一個例子來做說明:依據不同的設計選項群組,在同一視圖中,賦予不同的顏色。
        時常,我們需要利用設計選項(Design Option)使設計能被完善的考慮,而當我們設計了一段時間之後,在同一個模型檔案中,可能有好幾個不同的設計選項。在Revit的預設中,在同一個視圖裡,可以呈現選擇在選項群組的某一設計選項。或選擇自動,也就是所呈現的會是依據當下編輯的選項內容。但這有一盲點,我們無法在同一視圖中,區分介於不同設計選項群組中的物件,如果它們在於其他個方面都類似,譬如,都屬同種類(Category)且同類型(Type)。
        我們可簡單地寫一個指令來解決這個問題。

        In this blog entry, let’s talk about how to handle the display of elements in views for example, giving different color to elements from the other similar elements. Here, I’ll demonstrate with an example: assigning distinctive colors to elements according to their different design options. 
        Sometimes, we have to work with DesignOptions for considering different design possibilities to improve our planing, and after a while, in the same model file, we might have many design options. In Revit’s default setting, in one single View, we can only show one of the Design Options in their group. Or we can select “automatic”, which shows the content of the Design Option which is currently being edited. It has a blind spot. We can distinguish the elements between different Design Option Groups, if they are similar in every other field, such as of the same category or of the same type.
        We can however simply write a command to solve this problem.

type FitColorOfOptionElement() as this =
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let selected =
        let col = new FilteredElementCollector(uidoc.Document, uidoc.ActiveView.Id)
        col.WhereElementIsNotElementType()
        |> Seq.filter(
          fun e -> 
            try
              match e.DesignOption.Id.IntegerValue with
              | x when x = ElementId.InvalidElementId.IntegerValue -> false
              | _ -> true
            with
            | :? NullReferenceException -> false
        )
        |> Seq.map(
          fun e -> e.DesignOption, e
        )
        |> Seq.sortBy(fun(o, _) -> o.Id.IntegerValue)
      match selected |> List.ofSeq with
      | [] ->
        msg <- "No DesignOption in this document"
        Result.Cancelled
      | _ ->
        let options =
          selected |> Seq.map(fun(o, _) -> o) |> Seq.distinctBy(fun o -> o.Id.IntegerValue)
        let colors =
          options 
          |> Seq.indexed
          |> Seq.map(
            fun(i, o) -> 
              let ratio = float i / (float (Seq.length options))
              let color = new Color(byte (int(255.0 * ratio)), byte 128, byte 250)
              let ogs = new OverrideGraphicSettings()
              ogs.SetCutLineColor(color) |> ignore
              ogs.SetCutFillColor(color) |> ignore
              ogs.SetProjectionLineColor(color) |> ignore
              ogs.SetProjectionFillColor(color) |> ignore
              o, ogs
          )
        let t = new Transaction(uidoc.Document, string this)
        t.Start() |> ignore
        selected
        |> Seq.iter(
          fun(o, e) ->
            let _, ogs = 
              colors 
              |> Seq.find(fun(opt, _) -> opt.Id.IntegerValue = o.Id.IntegerValue)
            uidoc.ActiveView.SetElementOverrides(e.Id, ogs)
        )
        t.Commit() |> ignore
        Result.Succeeded

        在此,要注意的是,當我們篩選在設計選項的物件時,如果此物件依照它的屬性不能歸類於任何一設計選項時(例如細部線),我們會碰上 System.NullReferenceExeception 的錯誤。這時,可以用 try… with 來處理(16行)。而如果是可以歸類於設計選項,但並沒有被歸類,則可用判斷它的設計選項是否有無效的識別碼 InvalidElementId 來篩選。接下來的重點是,如何賦予特定的物件,特定的圖形取代;這時,OverrideGraphicSettings 將派上用場(41行)。這與我們在Revit中使用圖形取代時,是一樣的方法。在初使化 OverrideGraphicSetting 後,在其底下有與在Revit中一樣的設定:CutLineColor, CutLinePattern…等等,在此範例,我們要的是不同的顏色,將需要顏色的設定(40行)。最後,將此設定在現下的視圖中賦予各個物件(56行),依據它們不同的設計選項,將呈現不同的 RGB 紅色層級以作示範。

        Note, when we try to filter those elements, belonging to design options, we might confront the System.NullReferenceException error, if the elements cannot belong to any design option – such as detail lines, and we can handle it with “try… with” to avoid program crash (line 16). If the elements can belong to a design option but not, the id of its DesignOption parameter can be compared with InvalidElementId to be sorted. The next point is how to assign an override setting to a certain element; now the OverrideGraphicSettings would be useful (line41). This usage is similar to the way in Revit interface. After initialization of the OverrideGraphicSettings, its methods contain the setting available in Revit as well: CutLineColor, CutLinePattern… etc. In our example, we want to change the color of elements, for which we need to set color (line 40). Finally, assign these overriding settings to each of the design option elements in the current active view (line 56), and according to their distinguish design options, they will appear with different red tones in the RGB value.

Find Sum Area Of Rooms / 空間面積總合

下一個指令其實相當簡單的。上一次我們說過,該如何從簡單的曲線選取,指令的輸入,快速地取得曲線的長度總和。而我們這一次要做的其實很類似。假設我們想要快速地知道,好幾個空間的面積總和。這個可以用在當我們做建築內空間規劃時,想要快速的知道分區面積總合,是否超過最大的法規規定防火分區面積。在Revit API中,面積之於空間,就好比長度之於曲線,是一個它的相當簡單的預設參數。

The following command is actually quite simple. In the last entry we talked about efficiently pre-selecting curves, then running the external command and retrieve the sum of the line lengths. What we are going to do this time is similar. Say, we want to know instantly the sum of areas from different rooms. This is quite useful, when planning the fire compartments we want to be sure that it does not exceed the limit by the law. In Revit API, an area of a room is in the API’s structure similar to as a length to a curve. It is a pre-set parameter.

type FindSumAreaOfRooms() as this = 
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let selected =
        uidoc.Selection.GetElementIds() 
        |> Seq.map uidoc.Document.GetElement
        |> Seq.filter(
          fun e ->
            match e with
            | :? Room -> true
            | _ -> false
        ) |> Seq.cast<Room> |> List.ofSeq
      match selected with
      | [] -> 
        msg <- "Select Room(s)."
        Result.Cancelled
      | _ ->
        let txt =
          let unitareas =
            selected 
            |> Seq.map(
              fun rm -> 
                fsMath.toCurrentUnits uidoc.Document 2.0 rm.Area
            )
          let uSys, area = 
            unitareas |> Seq.head 
            |> fun(u, _) -> u, unitareas 
            |> Seq.sumBy(fun(_,a) -> a)
          match uSys with
          | fsMath.Metric ->
            area |> fsMath.toRoundUp 4.0 |> string |> fun s -> s + " m²"
          | fsMath.Imperial ->
            area |> fsMath.toRoundUp 4.0 |> string |> fun s -> s + " sq. ft"
        TaskDialog.Show(string this, txt) |> ignore
        Result.Succeeded

我們在這裡可以清楚的看到跟之前的不同處。基本的不同點是:這裏當我們過濾預先選取的物件,比較其是否為空間種類。而當我們要得到面積總和,我們尋找的是空間的面積參數。其餘的過程其實是很相似的。譬如,根據文件的單位在決定是否轉換單位為公尺或保留為英尺(API的預設單位為英制)。但也就是因為相似,我們可以把一樣的過程寫成一模距,然後在其他指令中重複使用。

We can see clearly the difference. The basic ones are: We filter the pre-selected elements by their categories to see if it is room, and when we want to have the sum of areas, we search for the area parameter of rooms. The rest of the process is actually similar. For example, according to the units of the document to decide if we convert the value to meters or keep it in feet (the default units of API is imperial.) The similarity is the reason that we can manage the same process into a module and re-use the module in the other commands.

module fsMath
type Units =
  | Metric
  | Imperial
let toCurrentUnits (doc:Autodesk.Revit.DB.Document)(pow:float)(ft:float) =
  let units = doc.GetUnits()
  let ut = Autodesk.Revit.DB.UnitType.UT_Length
  let fo = units.GetFormatOptions(ut)
  let du = fo.DisplayUnits
  match Array.contains "METERS" ((string du).Split '_') with
  | true -> Metric, ft * 0.3048**pow
  | false -> Imperial, ft
let toRoundUp (dig:float)(n:float) =
  System.Math.Round (n * 10.0 ** dig) / (10.0 ** dig)

程式的詳述還是用原文英文來解釋會比較清楚。
This module has one type and two functions. This type, called discriminated union, has two cases, metric or imperial. In the first function we have three arguments: Revit document, power and dimension in feet, and the function finds whether this document is using metric or imperial system and by given value of the power argument to return one of the union cases with the converted dimension value – depending on the value of power, if the power is 1.0, it’s linear, between meter and foot; if it is 2.0, then it’s of areas, between square meter and square foot, and so on and so forth. The second function rounds a number up, according to the digit value. These three have been used in our main code. You can also try to improve our last code by implementing them in the similar way.