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

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("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 PythonScript() =
  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.

Find Sum Of Line Length / 線長總和

In Revit 2017, I haven’t find the way to see directly the sum of the line lengths by selecting line elements within the default Revit interface. However, it is relative easy to write a few lines of code and use the TaskDialog interface in Revit API to return the summed length. And here it goes…

在使用 Revit 2017 中,我至今尚未找到一個方法,當我選取多重線段時,可以直接得到總長度的方法。但事實上,寫幾行 F# 程式經由 Revit API 取得長度,配合 TaskDialog 介面呈現總長數值,來解決這個問題是相當簡單的。以下是一範例:

type FindSumLengthOfLines() as this =
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let units = uidoc.Document.GetUnits()
      let uf = units.GetFormatOptions(UnitType.UT_Length).DisplayUnits
      let factor, tUnits =
        let ary = (string uf).Split '_'
        match Array.contains "METERS" ary with
          | true -> 0.3048, " m"
          | false -> 1.0, " ft"
      let selected =
          uidoc.Selection.GetElementIds()
          |> Seq.map uidoc.Document.GetElement
          |> Seq.filter(
            fun e ->
              match e with
              | :? CurveElement -> true
              | _ -> false
          ) |> Seq.cast |> List.ofSeq
      match selected with
      | [] ->
        msg
        let txt =
          selected
          |> Seq.map(
            fun ce ->
              ce.GeometryCurve.Length * factor
              |> fun x -> x * 10.0**4.0 |> Math.Round
              |> fun x -> x / 10.0**4.0
          ) |> Seq.sum |> string
          TaskDialog.Show("Sum of length:", txt + tUnits) |> ignore
          Result.Succeeded

As usual, we pre-select elements prior to running this command. To note is that we set a filter aiming for the element which are the curve element (line), by comparing the type of each of the pre-selected elements. From the filtered curve elements, we retrieve their length (line). Here comes a fun part. Since we are not sure with which units we should show the length, there is a function at the beginning of the code to determine which units this document has, where we compare if the keyword “METERS” exists in the DisplayUnitType. If so, we have metric system here and multiply the factor as 0.3048 to the summed lengths in order to show them correctly, and if not, it is then imperial units and the factor is 1.0 and we also append a string, either “m” or “ft” to the end of the dimension.

Simple and short, we make our work more efficient and save our valuable time for the other fun part of life…

勿忘參考 / 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# 指令集

Efficiently At: Fitting Active Workset and Element’s Workset

It has been said that using Worksets in Revit is like using layers, analog to AutoCAD or Rhino. Apart from that, working with a work-shared model and manipulating the worksets, either quickly switching / activating between worksets or assigning elements to worksets, can simply improve our working efficiency – at least the additional possibility to change the visibility of each workset in views is already an improvement.

Even this improvement can be further improved. Imagine we’d have to change the active workset often, in order to put the newly created element into correct “layers” – i.e. worksets. Every time you switch the workset, your mouse cursor have to move down the screen and activate the workset you want then you can create elements. This moving up and down frequently actually costs time and your wrist. Why not make your own command to switch and assign between workset more efficiently?

type FitWorkset() 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 |> List.ofSeq
      match uidoc.Document.IsWorkshared with
      | false ->
        msg 
        let namWsTo = "02"
        let col = new FilteredWorksetCollector(uidoc.Document)
        let ws = col.ToWorksets() |> Seq.filter(fun w -> namWsTo = Array.item 0 (w.Name.Split '_')) |> List.ofSeq
        match ws with
        | [] ->
          msg 
          let tblWorkset = uidoc.Document.GetWorksetTable()
          match selected with
          | [] -> 
            tblWorkset.SetActiveWorksetId(ws.Id)
          | _ ->
            let bip = BuiltInParameter.ELEM_PARTITION_PARAM
            let t = new Transaction(uidoc.Document, string this)
            t.Start() |> ignore
            selected |> List.iter(fun e -> (e.get_Parameter(bip)).Set(ws.Id.IntegerValue) |> ignore)
            t.Commit() |> ignore
          Result.Succeeded
        | _ ->
          msg <- "Keyword refer to more than one workset"
          Result.Cancelled

The concept of this code is, firstly, checking if the document is work-shared, if not then the command is cancelled since it has no worksets created by users; and secondly, checking if any elements are pre-selected. After the first checking, we know the file is work-shared and we'll find the workset, which we want to activate or assign to the elements, at first (line 18). If the workset with the keyword is not available and the list will contain nothing, the program returns an error message (line 21) and is cancelled. Otherwise, the program is split further into two cases: at one hand, if there is only one workset left at the end of filtering workset lists according to given keyword (line 23), then this workset will be further operated, otherwise in the rest of the cases, meaning more than one workset returned, the program will be terminated with cancellation and replying with error message (line 36). How the chosen workset is being operated depends on if there is any element selected prior to the starting point of running this program. If so, it is implied that these elements are meant to be assigned with the chosen workset, but if nothing is selected which implies we just want the chosen workset to be active. Assigning the active workset is by retrieving the workset table and using SetActiveWorksetId() method without initialization of a new transaction, which is contrary to assign workset parameter to elements by setting with the IntergerValue of the chosen workset's id.

To be continued…

 

 

 

Synchronize Section Box

When modeling in Revit, we set up a 3D view properly for our reviewing of the collisions in models to be easily recognizable and efficiently modified, and we would like to jump from one issue to the next quickly, we save the settings in the view template for the similar reviewing attentions.

However, the display of the model can easily be saved in template, but the location of the issues can be shown by modifying the section box in the 3d view. by hand. This can be extremely inefficient when dealing with multiple issues with scattered locations. For example, according to my experience, especially when working on the BCFs (Building Collaboration Format). When opening a issue, you will get a default 3d view opened, and the objects that might have collision are shown in the scene. However, the display of this default window might not be the one you want, especially when many overlapping objects are in the scene, you might wish you have your own view open, where you have setup the display.

If you are going to open your own view, every time you open one issue, and then fit the section box in your own view to the one from the BCF, it will be already dark outside your window, since each time the model has been checked, there might be hundreds of new issues coming, and you will be wasting too much time on just adjusting the views.

I don’t know if there is any tool for synchronizing section boxes between more than two views out there or already in the new version of Revit. But with F# and Revit API we can have the opened 3d views synchronized with current active view.

type SyncSectionBox() as this =
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let vAct = uidoc.ActiveView
      let bbxAct = (vAct:?>View3D).GetSectionBox()
      let uiv3ds =
        uidoc.GetOpenUIViews()
        |> Seq.filter(fun uiv -> uiv.ViewId  vAct.Id)
        |> Seq.filter(
          fun uiv ->
          let view = uiv.ViewId |> uidoc.Document.GetElement
          match view with
          | :? View3D -> true
          | _ -> false
        ) |> List.ofSeq
      match uiv3ds with
      | [] ->
        msg
        let t = new Transaction(uidoc.Document, string this)
        t.Start() |> ignore
        uiv3ds
        |> Seq.iter(
          fun uiv ->
            let view = uiv.ViewId |> uidoc.Document.GetElement
            (view:?>View3D).SetSectionBox(bbxAct)
            uiv.ZoomToFit()
        )
        t.Commit() |> ignore
        Result.Succeeded

It is just a short code, but, I think, it can help shortening our time for finding the right view when jumping from spot to spot. The concept is simple: just a manipulation of section boxes in 3d views. First, find the section box of the current 3d view (line 11). Section boxes are in a bounding boxes, which is a rectangular box and has its maximum and minimum points. Second, find the opened UI-views (line 13) which we want to apply this section box into. At this step, we filter out first the active view, since we don’t have to synchronize its section box with itself, and then we filter again and get rid of those views which are not 3d (line 15).

Then it’s the main body of the command. We match the list of opened UI-views, which are filtered. If it is empty, then there is nothing to sync, and we response with message to tell user to open other 3d views. Else, we open a transaction to be ready to apply section box to views (line 27), since we are going to modify the model information. The rest is one iteration of the 3d view list – for each of the view we use the SetSectionBox method to apply the section box to it. However we have to note that the downcast of the view as an element to view is necessary (line 33), just like how we get the section box at first (line 11), otherwise these two methods will not be available. The final step is just for a convenient modeling work-flow, to zoom the UI-view, with the new section box, to fit the screen, or we might see nothing after the view manipulation, since the next spot could be far away.

Stay tuned!

Filter Selected Elements by Category

When a model is at its beginning building phase, the drawn element are just a few, it’s easy to select them by picking in the views. However, with the model getting more complex, it could be quite a lot of elements overlapping each other just at a corner of the building. Now it could be hard to try to just select a group of elements of targeted categories quickly. Certainly, we can select all of them at once by picking them up with a rectangle all at once, then go to the multi-select tab, filter them, un-check those categories which you don’t want, and then hit OK button.

Can it even be faster?

Yes, a little faster we can do is by adding a shortcut for the filter command, and after we select the whole bunch of elements and hit the shortcut keys, we have the selection window popping up in front of us. However, doing this much faster will not be possible since we will have a progress threshold that the machine has to know which category we want to choose from these elements with this pop-up and before we have selected them, it will not know what are those categories to shown in order to know which one to filter. This makes this process with a break and I think, the most of the time we wasted by drawing is at moving the cursor between buttons, check-boxes or windows.

How can we improve this and make it even faster? Think about the process to reach our goal. Select – give category – filter. The only window here we have is the one to provide the category according to which the elements are filtered, and the process has been slowed down at this point. This window is inevitable if we wand to dynamically tell the computer which of the category is what we want, between those of the selected elements. However, if you are doing a routine job, such as selecting those only of the one category – for example, room category – and summing up the area to see if it exceeds the maximum of fire proving sections, then we can do something about it. Otherwise, when the pop-up comes up, we still need time to un-check those categories we don’t need.

type FilterSelectedElementsByCategory() =
  interface IExternalCommand with
    member x.Execute(cdata, msg, elset) =
      let uidoc = cdata.Application.ActiveUIDocument
      let selected = uidoc.Selection.GetElementIds() |> Seq.map uidoc.Document.GetElement |> List.ofSeq
      match selected with
      | [] ->
        msg
        let idcats = selected |> List.map(fun sel -> sel.Category.Id)
        let intIds = idcats |> List.map(fun id -> id.IntegerValue) |> List.distinct
        let idPicked =
          let strCats = selected |> Seq.map string |> Seq.map(fun str -> Array.last (str.Split '.')) |> String.concat ", "
          let promp = sprintf "Pick elements to filter by category: %s." strCats
          uidoc.Selection.PickElementsByRectangle(promp)
          |> Seq.filter(
              fun e -> List.contains e.Category.Id.IntegerValue intIds
          )
          |> Seq.map(fun e -> e.Id)
        uidoc.Selection.SetElementIds(ResizeArray(idPicked))
        Result.Succeeded

When talking about the efficiency of modeling, I found it useful when we more focus on clicking, for example, keys and mouse buttons, and less on moving cursor around. In this selection process example, as said above, a window pops up when it comes to getting user information, according to which categories we want to filter. Using pop-ups and check-boxes after we start the command is not the only way we can tell computer which category it is that we want. How about we give the information before we run the command? We can pre-select arbitrary objects with the exact categories we want, then retrieve this information in code from the Selection of ActiveUIDocument and then run pick object by rectangle.

Usually we would have zoomed to the area when beginning to start this specific selection process, maybe also have one element within the area, whose category is the one we want to filter. We can just select it as an example for running the command. The work-flow to reach our goal would be as simple as just only three steps: pre-selecting elements with one mouse click, hitting the shortcut of the command – depending on the keys you set as shortcut – and finally picking object with a rectangle. The rest will be don by the command for you: getting wanted categories – filtering picked elements by the categories – putting the picked object into the Selection bucket – and show them selected on the screen.

That’s it! As shortly as it can be!