├── README.md ├── chair_numbering__explained.py ├── elementID_conflict_explained.py ├── excel_export_explained.py ├── excel_import_explained.py ├── parking_spaces_explained.py ├── room_report_explained.py ├── unused_items_in_view_map_explained.py ├── zone_numbering_explained.py └── zone_overall_dimensions_explained.py /README.md: -------------------------------------------------------------------------------- 1 | # Archicad Automation Scripts 2 | 3 | This repository contains Python scripts designed to automate various tasks in Archicad using its API. Each script addresses a specific functionality, ranging from managing project elements to generating reports and handling conflicts. 4 | 5 | ## Scripts Overview 6 | 7 | ### 1. **Unused Items in View Map** 8 | - **File:** `unused_items_in_view_map_explained.py` 9 | - **Purpose:** Identifies unused items in the View Map and organizes them into folders. 10 | - **Features:** 11 | - Moves unused navigator items to a designated folder. 12 | - Renames folders from previous runs for better organization. 13 | - Ensures only unused "parent" items are included in the list. 14 | 15 | --- 16 | 17 | ### 2. **Zone Numbering** 18 | - **File:** `zone_numbering_explained.py` 19 | - **Purpose:** Automates numbering of zones based on their position and level. 20 | - **Features:** 21 | - Groups zones by levels and sides of the building. 22 | - Assigns unique numbers to zones using a predefined format. 23 | - Handles tolerance limits for grouping zones. 24 | 25 | --- 26 | 27 | ### 3. **Element ID Conflict Detection** 28 | - **File:** `elementID_conflict_explained.py` 29 | - **Purpose:** Detects conflicts in Element IDs within the project. 30 | - **Features:** 31 | - Identifies duplicate Element IDs across all elements. 32 | - Outputs detailed conflict messages for resolution. 33 | - Confirms when no conflicts are found. 34 | 35 | --- 36 | 37 | ### 4. **Zone Overall Dimensions** 38 | - **File:** `zone_overall_dimensions_explained.py` 39 | - **Purpose:** Calculates and assigns overall dimensions (width x height) for zones. 40 | - **Features:** 41 | - Determines bounding box dimensions for each zone. 42 | - Formats dimensions with the larger value first (office preference). 43 | - Updates zone properties with calculated values. 44 | 45 | --- 46 | 47 | ### 5. **Room Report Generator** 48 | - **File:** `room_report_explained.py` 49 | - **Purpose:** Generates detailed Excel reports for rooms in the project. 50 | - **Features:** 51 | - Extracts room properties like name, number, category, area, volume, etc. 52 | - Includes adjacent zones, equipment details, and openings in the report. 53 | - Uses a predefined template for structured output. 54 | 55 | --- 56 | 57 | ### 6. **Chair Numbering** 58 | - **File:** `chair_numbering__explained.py` 59 | - **Purpose:** Automates numbering of chairs in an auditorium based on layout. 60 | - **Features:** 61 | - Groups chairs by rows and sides (left/right). 62 | - Assigns unique IDs using a row-index format (e.g., `A.1/Right`). 63 | - Handles tolerance limits for grouping chairs. 64 | 65 | --- 66 | 67 | ### 7. **Parking Space Numbering** 68 | - **File:** `parking_spaces_explained.py` 69 | - **Purpose:** Automates numbering of parking spaces based on their layout. 70 | - **Features:** 71 | - Groups parking spaces by levels and rows. 72 | - Assigns unique IDs using a predefined format (e.g., `P112`). 73 | - Handles tolerance limits for grouping spaces. 74 | 75 | --- 76 | 77 | ### 8. **Excel Export Utility** 78 | - **File:** `excel_export_explained.py` 79 | - **Purpose:** Exports element properties to an Excel file for beams and walls. 80 | - **Features:** 81 | - Extracts properties like Element ID, Height, Width, Thickness, etc. 82 | - Creates separate worksheets for beams and walls. 83 | - Auto-adjusts column widths for readability. 84 | 85 | --- 86 | 87 | ### 9. **Excel Import Utility** 88 | - **File:** `excel_import_explained.py` 89 | - **Purpose:** Imports property values from an Excel file into Archicad elements. 90 | - **Features:** 91 | - Reads element IDs and property values from Excel sheets. 92 | - Updates corresponding element properties in Archicad. 93 | - Verifies changes by printing updated values to the console. 94 | 95 | 96 | ## Requirements 97 | 1. Archicad software must be open with an active project file (`.pln`). 98 | 2. Python environment with necessary dependencies installed: 99 | - `archicad` API module 100 | - `openpyxl` (for Excel operations) 101 | 102 | 103 | ## How to Use 104 | 1. Clone this repository to your local machine: 105 | ``` 106 | git clone 107 | ``` 108 | 2. Open your Archicad project file (`.pln`). 109 | 3. Run the desired script using Python: 110 | ``` 111 | python .py 112 | ``` 113 | 4. Follow any prompts or outputs displayed in the console. 114 | 115 | 116 | ## Notes 117 | - Ensure proper configuration of variables within each script before execution (e.g., folder names, output paths). 118 | - Some scripts rely on predefined templates or classification systems; verify their availability before running. 119 | -------------------------------------------------------------------------------- /chair_numbering__explained.py: -------------------------------------------------------------------------------- 1 | # import archicad connection (required) 2 | from archicad import ACConnection 3 | # import typing and string not essential for the code 4 | from typing import List, Tuple, Iterable 5 | # import string to define the row character index in the 'GeneratePropertyValueString' function 6 | import string 7 | 8 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 9 | # 10 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 11 | # as a minimum because all utilities use the connection. 12 | conn = ACConnection.connect() 13 | # assert that the connection is alive 14 | assert conn 15 | 16 | # Create shorts of the commands, types and utilities. 17 | acc = conn.commands 18 | act = conn.types 19 | acu = conn.utilities 20 | 21 | # original comment -> ################################ CONFIGURATION ################################# 22 | # Getting the property Id (guid) for the General_ElementID property in order to use this to identify 23 | # the property exactly when we communicate with the API (this is a unique identifier like our social security number). 24 | propertyId = acu.GetBuiltInPropertyId('General_ElementID') 25 | 26 | # This is a method for collecting all the chairs from the Archicad pln file in a list. 27 | # We need the 'guid' of the classificationItem in order to uniquely identify the classification 28 | # based on we want to collect the elements with the Get elements by classification method. 29 | classificationItem = acu.FindClassificationItemInSystem( 30 | 'ARCHICAD Classification', 'Chair') 31 | # We collect the chairs in the element list using the chair classification 'guid' from above. 32 | # The elements list contains the 'guids' of the chairs. The length of the list is 125 since we have 125 chairs. 33 | elements = acc.GetElementsByClassification( 34 | classificationItem.classificationItemId) 35 | 36 | # This variable is to consider some kind of tolerance in the 'z' coordinate of the chair positions 37 | # when we are sorting them by the level where they are placed. 38 | # For that particular example this wouldn't be necessary since all the chairs are exactly leveled 39 | # and placed exactly on the same levels on the different slabs. 40 | ROW_GROUPING_LIMIT = 0.25 41 | 42 | # With this function we generate a string property value for the General elemnt Id since this is a string type Id. 43 | # Takes as argument the rowindex (A, B, C etc.), index in row (1, 2, 3, etc.) is right (True or False) 44 | # returns a string e.g.: 'A.1/Right' 45 | def GeneratePropertyValueString(rowIndex: int, indexInRow: int, isRight: bool) -> str: 46 | return f'{string.ascii_uppercase[rowIndex]}.{indexInRow}/{"Right" if isRight else "Left"}' 47 | # original comment -> ################################################################################ 48 | 49 | # This function prepares NormalStringPropertyValue type from the string generated by the function 'GeneratePropertyValueString'. 50 | # The function of GeneratePropertyValueString could be combined with this since these two functions are working always together. 51 | def generatePropertyValue(rowIndex: int, indexInRow: int, isRight: bool) -> act.NormalStringPropertyValue: 52 | return act.NormalStringPropertyValue(GeneratePropertyValueString(rowIndex, indexInRow, isRight)) 53 | 54 | # This function is just getting the z Min position of the actual chair element. 55 | # Since it receives a tuple like arguments with two values (first value is the element id, second value is the bounding box) 56 | # we need to get the z Min from the second value. 57 | def getXMin(elemWithBoundingBox: Tuple[act.ElementIdArrayItem, act.BoundingBox3DWrapper]) -> float: 58 | return elemWithBoundingBox[1].boundingBox3D.xMin 59 | 60 | # This function generate the new property values for the chairs. 61 | # Receive the element with bounding box list which contains the element ids followed by their bounding boxes respectively. 62 | # Returns a list with the 'ElementPropertyValue' of every chairs. 63 | # ElementPropertyValue is a class contains the elementId, propertyId and property value. 64 | # The property value in our case a string since we are changing the 'General_ElementID's of the chairs. 65 | # The type of the 'General_ElementID' is string. 66 | def generateNewPropertyValuesForElements(elemsWithBoundingBox: Iterable[Tuple[act.ElementIdArrayItem, act.BoundingBox3DWrapper]], isRight: bool, rowIndex: int): 67 | # Create the empty property value list. 68 | propertyValues = [] 69 | # Start index in row is '1'. 70 | indexInRow = 1 71 | # The loop generates and appends the 'ElementPropertyValue's. 72 | # Using the sorted method on the 'elemsWithBoundingBox', the bases of the sorting is the xMin, 73 | # xMin min is the first (index 1 and from here increasing) if the chair is on the left, 74 | # xMin max is the first (index 1 and from here increasing) if the chair is on the right 75 | for (elem, _) in sorted(elemsWithBoundingBox, key=getXMin, reverse=isRight): 76 | # Append to the 'propertyValues' list the generated property value classes. 77 | # Arguments: element id, property id, calling the 'generatePropertyValue' 78 | # to generate the General element id string. 79 | propertyValues.append(act.ElementPropertyValue( 80 | elem.elementId, propertyId, generatePropertyValue(rowIndex, indexInRow, isRight))) 81 | indexInRow += 1 82 | return propertyValues 83 | 84 | # Create a list of the different slab levels with chairs. 85 | # Arguments: List of elements' zMin values, tolerance limit. 86 | # Returns list of tuples(zMin, zMax) with the different levels. 87 | # zMin and zMax are always equal in our case. 88 | def createClusters(positions: Iterable[float], limit: float) -> List[Tuple[float, float]]: 89 | # When we are getting the chairs to the elements list the order of the chairs is not following the slab levels. 90 | # For that reason 'positions' needs to be sorted to avoid level duplicates in the 'clusters' list. 91 | positions = sorted(positions) 92 | # If there is no elements nothing to sort so return an empty list. 93 | # In our case we always have elemnts so this check is redundant. 94 | if len(positions) == 0: 95 | return [] 96 | 97 | # Create empty list for the levels 98 | clusters = [] 99 | # Make the 'position' list iterabel. The list is iterable so this is not required. 100 | posIter = iter(positions) 101 | # All postions are equel to the first position in the 'positions' list. 102 | # In our case we would only need 1 position i.e. firstPos. 103 | firstPos = lastPos = next(posIter) 104 | 105 | # Loop all the position and append to the 'clusters' list the different slab levels only. 106 | for pos in posIter: 107 | # If the actual position - lastPos which is the start at the beginning <= limit (could be == 0). 108 | if pos - lastPos <= limit: 109 | # Take the actual position as the last one and continue with the next level. 110 | lastPos = pos 111 | # Else (the actual position - last position > limit (could be > 0)). 112 | else: 113 | # Append the first and last position tuple to the clusters list. 114 | # Fisrt position and last position are always equal in our case. 115 | clusters.append((firstPos, lastPos)) 116 | # Update all the positions to be equal to pos and continue with the next level. 117 | firstPos = lastPos = pos 118 | 119 | # Since there is no next level after the last one, 120 | # for the last level the else statement will never happen. 121 | # In order to be this level in the 'clusters' list we need to append this after the loop finishes. 122 | clusters.append((firstPos, lastPos)) 123 | return clusters 124 | 125 | # Getting all 3d bounding boxes of all the chairs. 126 | # The bounding box contains the x, y, z minimum and maximum values of the box 127 | # can be drawn around the element containging the whole element! 128 | # Returns a list. 129 | boundingBoxes = acc.Get3DBoundingBoxes(elements) 130 | 131 | # Here we calculate the avarage x position of the chairs to get the middle point x coordinate 132 | # This will help us define if the chair is Right or Left 133 | averageXPosition = sum([bb.boundingBox3D.xMin for bb in boundingBoxes]) / len(boundingBoxes) 134 | 135 | # We create the list of the slab levels of the auditorium where the chairs are located. 136 | # Arguments: zMin values of the chairs, limit which is the tolerance of the level. 137 | zClusters = createClusters((bb.boundingBox3D.zMin for bb in boundingBoxes), ROW_GROUPING_LIMIT) 138 | 139 | # List of each elements and its bounding box follows. 140 | # Calling zip method on the elements list and the bounding box list. 141 | elementsWithBoundingBoxes = list(zip(elements, boundingBoxes)) 142 | 143 | # rowindex will increase when all chairs in the actual row have their property values generated 144 | # and appended to the elemPropertyValues list. 145 | rowIndex = 0 146 | elemPropertyValues = [] 147 | 148 | # Loop through all the slab levels taking the zMin and zMax. 149 | # These two values are the same practically and in that sense only the zMin positin would be enough. 150 | for (zMin, zMax) in zClusters: 151 | # Using list comprehension we prepare a list with the chairs in the same row (same level) 152 | # including all the chairs on the right and on the left side too. 153 | elemsInRow = [e for e in elementsWithBoundingBoxes if zMin <= e[1].boundingBox3D.zMin <= zMax] 154 | # Create separate lists for the sides. 155 | rightSide, leftSide = [], [] 156 | # Append to the appropriate list the chairs on the left and on the right side. 157 | for elemWithBoundingBox in elemsInRow: 158 | # Calling the getXmin function to define the zMin position of the actual element 159 | # and if it is smaller then the average X psoition the chair is on the left side. 160 | # It is appended to the leftSide list. 161 | if getXMin(elemWithBoundingBox) > averageXPosition: 162 | leftSide.append(elemWithBoundingBox) 163 | # else (it must be greater then the avarage x position) the chair is on the right side. 164 | # It is appended to the rightSide list. 165 | else: 166 | rightSide.append(elemWithBoundingBox) 167 | 168 | # Using the extend method to add both left and right side list to the elem property values list. 169 | # The end of these loops thie list will contain all the new property values of the chairs. 170 | elemPropertyValues.extend(generateNewPropertyValuesForElements(rightSide, True, rowIndex)) 171 | elemPropertyValues.extend(generateNewPropertyValuesForElements(leftSide, False, rowIndex)) 172 | # Increase the rowindex (go to next row) since we finished with the actual row. 173 | rowIndex += 1 174 | 175 | # Set the new property values. 176 | # Argument: elemPropertyValues list. 177 | acc.SetPropertyValuesOfElements(elemPropertyValues) 178 | 179 | # original comment -> # Print the result 180 | 181 | # Check and print the results: 182 | # Using the 'GetPropertyValuesOfElements' command with the 'elements' and propertyId list arguments 183 | # preapre a list with the chair modified properties. 184 | newValues = acc.GetPropertyValuesOfElements(elements, [propertyId]) 185 | # Using list comprehension preapre a list of tuples with elements ids and their property values respectively. 186 | elemAndValuePairs = [(elements[i].elementId.guid, v.propertyValue.value) for i in range(len(newValues)) for v in newValues[i].propertyValues] 187 | # Print the elem 'elemAndValuePairs' list sort by the property values. 188 | # Calling the sorted method on the list using sorting key to be the property value of the tuple. 189 | for elemAndValuePair in sorted(elemAndValuePairs, key=lambda p: p[1]): 190 | print(elemAndValuePair) -------------------------------------------------------------------------------- /elementID_conflict_explained.py: -------------------------------------------------------------------------------- 1 | # Import archicad connection (required). 2 | from archicad import ACConnection 3 | 4 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 5 | # 6 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 7 | # as a minimum because all utilities use the connection. 8 | conn = ACConnection.connect() 9 | assert conn 10 | 11 | # Create shorts of the commands, types and utilities. 12 | acc = conn.commands 13 | act = conn.types 14 | acu = conn.utilities 15 | 16 | # original comment -> ################################ CONFIGURATION ################################# 17 | # Get all elements from the project. 18 | elements = acc.GetAllElements() 19 | # Define messages. 20 | messageWhenNoConflictFound = "There is no elementID conflict." 21 | conflictMessageParts = ["[Conflict]", "elements have", "as element ID:\n"] 22 | 23 | # This function createss the constructed message for the conflicts. 24 | def GetConflictMessage(elementIDPropertyValue, elementIds): 25 | return f"{conflictMessageParts[0]} {len(elementIds)} {conflictMessageParts[1]} '{elementIDPropertyValue}' {conflictMessageParts[2]}{sorted(elementIds, key=lambda id: id.guid)}" 26 | # original comment -> ################################################################################ 27 | 28 | # Get the built in property id of 'General_ElementID' for all the elements. 29 | elementIdPropertyId = acu.GetBuiltInPropertyId('General_ElementID') 30 | # Get the built in property value of 'General_ElementID' for all the elements. 31 | propertyValuesForElements = acc.GetPropertyValuesOfElements(elements, [elementIdPropertyId]) 32 | 33 | # Create a dictionary with the property value : element id, key:value pairs. 34 | propertyValuesToElementIdsDictionary = {} 35 | # Loop through the elements' property values. 36 | for i in range(len(propertyValuesForElements)): 37 | # Take the element id of the actual element. 38 | elementId = elements[i].elementId 39 | # Take the property value of the actual element. 40 | propertyValue = propertyValuesForElements[i].propertyValues[0].propertyValue.value 41 | # If the property value is not in the dictionary create the key of the actual property value 42 | # and its value as an empty set. 43 | if propertyValue not in propertyValuesToElementIdsDictionary: 44 | propertyValuesToElementIdsDictionary[propertyValue] = set() 45 | # Add the actual elmentid to the set. 46 | propertyValuesToElementIdsDictionary[propertyValue].add(elementId) 47 | 48 | # As base condition no conflict. 49 | noConflictFound = True 50 | # Loop through the 'propertyValuesToElementIdsDictionary' items. 51 | for k, v in sorted(propertyValuesToElementIdsDictionary.items()): 52 | # If the actual property value's (key) set has more than one element 53 | # it means the same id is given to those elements which ids in the actual set. 54 | # It is a conflict. 55 | if len(v) > 1: 56 | noConflictFound = False 57 | print(GetConflictMessage(k, v)) 58 | # If there was no conflict (the element id set of the actual value contains only one element). 59 | if noConflictFound: 60 | print(messageWhenNoConflictFound) 61 | -------------------------------------------------------------------------------- /excel_export_explained.py: -------------------------------------------------------------------------------- 1 | # Import archicad connection (required), handle_dependencies is not necessary. 2 | from archicad import ACConnection, handle_dependencies 3 | # Import typing module list not necessary. 4 | from typing import List 5 | # Import os for file operations. Sys not used. 6 | import os, sys 7 | 8 | # Check if the importable (installed) if not returns an error. 9 | handle_dependencies('openpyxl') 10 | 11 | # Import Workobook from openpyxl for excel file operations. 12 | # https://openpyxl.readthedocs.io/en/stable/index.html 13 | from openpyxl import Workbook 14 | 15 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 16 | # 17 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 18 | # as a minimum because all utilities use the connection. 19 | conn = ACConnection.connect() 20 | assert conn 21 | 22 | # Create shorts of the commands, types and utilities. 23 | acc = conn.commands 24 | act = conn.types 25 | acu = conn.utilities 26 | 27 | # Getting the actual dirname as scriptFolder variable. 28 | # The use of realpath is to get the canonical path adn ignore symbolic links. 29 | scriptFolder = os.path.dirname(os.path.realpath(__file__)) 30 | 31 | # original comment -> ################################ CONFIGURATION ################################# 32 | # Getting into a dictionary the {element classification ID : guid} 33 | worksheetTitlesAndElements = { 34 | "Beams": acc.GetElementsByType("Beam"), 35 | "Walls": acc.GetElementsByType("Wall") 36 | } 37 | # Getting the built in property user ids of the required properties into a list. 38 | propertyUserIds = [ 39 | act.BuiltInPropertyUserId("General_ElementID"), 40 | act.BuiltInPropertyUserId("General_Height"), 41 | act.BuiltInPropertyUserId("General_Width"), 42 | act.BuiltInPropertyUserId("General_Thickness") 43 | ] 44 | outputFolder = scriptFolder 45 | # Define the output filename. 46 | outputFileName = "BeamAndWallGeometry.xlsx" 47 | # original comment -> ################################################################################ 48 | 49 | 50 | # This function fit the cells to the longest value in the columns. 51 | def AutoFitWorksheetColumns(ws): 52 | # Looping through the cells per column of each columns. 53 | for columnCells in ws.columns: 54 | # Getting the max string length of the filled cells using list comprehension. 55 | length = max(len(str(cell.value)) for cell in columnCells) 56 | # Setting the columns dimension to the max string length. 57 | # https://www.geeksforgeeks.org/python-adjusting-rows-and-columns-of-an-excel-file-using-openpyxl-module/ 58 | # The column letter is getting the letter (string) index of the cell column instead of the number, 59 | # this is the required parameter of the column_dimension method. 60 | ws.column_dimensions[columnCells[0].column_letter].width = length 61 | 62 | 63 | # This function prints out the worksheet content into the console. 64 | def PrintWorksheetContent(ws): 65 | # Looping through the columns. 66 | for columnCells in ws.columns: 67 | # Looping through the cells of the actual column. 68 | for cell in columnCells: 69 | # Print the worksheet title, the column letter, cell row number, cell value. 70 | print(f"{ws.title}!{cell.column_letter}{cell.row}={cell.value}") 71 | 72 | 73 | # This is the main function to fill out the excel worksheets. 74 | def FillExcelWorksheetWithPropertyValuesOfElements(ws, propertyIds: List[act.PropertyIdArrayItem], elements: List[act.ElementIdArrayItem]): 75 | # Getting the elements with the same classification id and their guids and 76 | # the required properties and their guids into a dcitionary. 77 | propertyValuesDictionary = acu.GetPropertyValuesDictionary(elements, propertyIds) 78 | # Zipping into a list the propertyids with their values. 79 | propertyDefinitionsDictionary = dict(zip(propertyIds, acc.GetDetailsOfProperties(propertyIds))) 80 | 81 | # Create the base table 82 | # Cell in the row 2 and column 1 will have the value of 'Element Guid' 83 | ws.cell(row=2, column=1).value = "Element Guid" 84 | # Continue from row 3. 85 | row = 3 86 | # Loop through the propertyValuesDictionary and prepare, fill out the cells. 87 | for element, valuesDictionary in propertyValuesDictionary.items(): 88 | # row 3 column 1 first element guid 89 | ws.cell(row=row, column=1).value = str(element.elementId.guid) 90 | # 'Go to' column 2 91 | column = 2 92 | # Loop through the valuesDictionary taking the ids, values and fill cells: 93 | for propertyId, propertyValue in valuesDictionary.items(): 94 | # If the ctual row is 3 (at the beginning): 95 | if row == 3: 96 | # The cell of the row 1 and column 2 will be the actual property id. 97 | ws.cell(row=1, column=column).value = str(propertyId.propertyId.guid) 98 | # Getting the property definition for the actual propertiId. 99 | propertyDefinition = propertyDefinitionsDictionary[propertyId].propertyDefinition 100 | # Row 2 column 3 write the 'group name / property definition name' string, e.g. 'General Parameters / Height'. 101 | ws.cell(row=2, column=column).value = f"{propertyDefinition.group.name} / {propertyDefinition.name}" 102 | # The actual row and column write the property value. 103 | # For the first iteration these are: row 3 column 2. 104 | ws.cell(row=row, column=column).value = propertyValue 105 | # 'Go to' next column. 106 | column += 1 107 | # 'Go to' next row. 108 | row += 1 109 | # Fit the columns widths to the max length cell values, calling the 'AutoFitWorksheetColumns' function. 110 | AutoFitWorksheetColumns(ws) 111 | # Print worksheet content into the console, calling the 'PrintWorksheetContent' function. 112 | PrintWorksheetContent(ws) 113 | 114 | # Getting the property ids (guid) using the propertyuserids. 115 | propertyIds = acc.GetPropertyIds(propertyUserIds) 116 | # Creating a workbook. 117 | wb = Workbook() 118 | # Select the active worksheet. 119 | ws = wb.active 120 | 121 | # Variable to know the number of the actual loop number. 122 | i = 0 123 | # Loop through 'worksheetTitlesAndElements' dictionary to prepare the excel sheets for each item. 124 | for title, elements in worksheetTitlesAndElements.items(): 125 | # If we are in the first iteration we have the first sheet active and give it a title 126 | # of the actual element claccification Id. 127 | if i == 0: 128 | ws.title = title 129 | # If we are not in the first iteration we create a new sheet and give it the title. 130 | else: 131 | ws = wb.create_sheet(title) 132 | # Getting all required data of the actual element for the excel workbook. 133 | # Arguments: worksheet, property Ids (guid), elements (guid) 134 | FillExcelWorksheetWithPropertyValuesOfElements(ws, propertyIds, elements) 135 | # Go to the next iteration 136 | i += 1 137 | 138 | # Prepare the created excel file's path with joining the Folder path and the filename. 139 | excelFilePath = os.path.join(outputFolder, outputFileName) 140 | # Save the workbook to the same folder as the script's. 141 | wb.save(excelFilePath) 142 | # Using the Archicad API 'OpenFile' utility open the excel file with the default application 143 | # for this type of files defined in the OS. 144 | acu.OpenFile(excelFilePath) 145 | 146 | # If the file saved successfully print out to the console the ok message. 147 | if os.path.exists(excelFilePath): 148 | print("Saved Excel") 149 | -------------------------------------------------------------------------------- /excel_import_explained.py: -------------------------------------------------------------------------------- 1 | # Import archicad connection (required), handle_dependencies is not necessary. 2 | from archicad import ACConnection, handle_dependencies 3 | # Import typing module (not necessary). 4 | from typing import List, Dict, Any 5 | # Import os for file operations. Sys not used. Uuid for uuid generation. 6 | import os, sys, uuid 7 | # Check if the importable (installed) if not returns an error. 8 | handle_dependencies('openpyxl') 9 | 10 | # Import Workobook from openpyxl for excel file operations. 11 | # https://openpyxl.readthedocs.io/en/stable/index.html 12 | from openpyxl import load_workbook 13 | 14 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 15 | # 16 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 17 | # as a minimum because all utilities use the connection. 18 | conn = ACConnection.connect() 19 | assert conn 20 | 21 | # Create shorts of the commands, types and utilities. 22 | acc = conn.commands 23 | act = conn.types 24 | acu = conn.utilities 25 | 26 | # Getting the actual dirname as scriptFolder variable. 27 | # The use of realpath is to get the canonical path adn ignore symbolic links. 28 | scriptFolder = os.path.dirname(os.path.realpath(__file__)) 29 | 30 | # original comment -> ################################ CONFIGURATION ################################# 31 | inputFolder = scriptFolder 32 | # Define the output filename. 33 | inputFileName = "BeamAndWallGeometry.xlsx" 34 | # original comment -> ################################################################################ 35 | 36 | # Prepare the created excel file's path with joining the Folder path and the filename. 37 | excelFilePath = os.path.join(inputFolder, inputFileName) 38 | # Load the excel file workbook as wb. 39 | wb = load_workbook(excelFilePath) 40 | # This list will be filled with the final element property value objects. 41 | elemPropertyValues = [] 42 | 43 | for sheet in wb.worksheets: 44 | # Worksheet package doc. 45 | # https://openpyxl.readthedocs.io/en/stable/api/openpyxl.worksheet.worksheet.html?highlight=max_worksheet 46 | maxCol = sheet.max_column 47 | maxRow = sheet.max_row 48 | # All the data from the excel workbook will be inserted into this dictionary. 49 | newPropertyValues = {} 50 | # element id list (from the excel workbook). 51 | elementIds = [] 52 | # property id list (from the excel workbook). 53 | propertyIds = [] 54 | # Loop through the columns from 2 to the last (maxCol). 55 | for col in range (2, maxCol + 1): 56 | # Append the propertyId guid converted by the uuid method 57 | # from the string of the property id contained by the cell. 58 | propertyIds.append(act.PropertyId(uuid.UUID(sheet.cell(1, col).value))) 59 | # Loop through the rows from 3 to the last (maxRow). 60 | for row in range (3, maxRow + 1): 61 | # Get the rows in the excel workbook for the actual sheet into a dictionary. 62 | # e.g. 0:{0:'a8138ba2-01da-4193-aa14-974e9ca78483', 1:'SW-001', 2:6, 3:0.3, 4:0.3} 63 | newPropertyValues[row-3] = {col-2: sheet.cell(row, col).value for col in range (2, maxCol + 1)} 64 | # Append the elementId guid converted by the uuid method 65 | # from the string of the element id contained by the cell. 66 | elementIds.append(act.ElementId(uuid.UUID(sheet.cell(row, 1).value))) 67 | 68 | # Getting the property values of the elements based on the actual worksheet. 69 | propertyValuesOfElements = acc.GetPropertyValuesOfElements(elementIds, propertyIds) 70 | 71 | # Create a loop for the number of the element times. 72 | for ii in range(len(newPropertyValues)): 73 | # Create a loop for the number of the actual element's number of property times. 74 | for jj in range(len(newPropertyValues[ii])): 75 | # Try except block wouldn't be necessary. 76 | try: 77 | # Getting the property value directly from the 'propertyValuesOfElements' list. 78 | propertyValue = propertyValuesOfElements[ii].propertyValues[jj].propertyValue 79 | # Changing the old property value to the new one taking from the 80 | # 'newPropertyValues' dictionary using the numeric indexes we gave as keys. 81 | propertyValue.value = newPropertyValues[ii][jj] 82 | # Give it a 'normal' status. 83 | propertyValue.status = "normal" 84 | # Finally using the elementsId for the actual element, propertyId of the actual property 85 | # and the prepared property value create and append the element property value of the 86 | # actual element actual property to the 'elemPropertyValues' list otside of the loops. 87 | elemPropertyValues.append(act.ElementPropertyValue(elementIds[ii], propertyIds[jj], propertyValue)) 88 | # If something went wrong continue. 89 | except: 90 | continue 91 | # Set the created element property values to the corresponding elements in the Archicad project. 92 | acc.SetPropertyValuesOfElements(elemPropertyValues) 93 | 94 | # original comment -> # Print the result 95 | # Get the element ids from the elemPropertyValues list. 96 | elementIds = [i.elementId for i in elemPropertyValues] 97 | # Get the property ids from a set (to get the unique guids only) generated from the 'lemPropertyValues' list. 98 | propertyIds = [act.PropertyId(guid) for guid in set(i.propertyId.guid for i in elemPropertyValues)] 99 | # Creae a property values dictionary from the elementids and propertyids. 100 | propertyValuesDictionary = acu.GetPropertyValuesDictionary(elementIds, propertyIds) 101 | # Loop through and taking each item of the 'propertyValuesDictionary'. 102 | for elementId, valuesDictionary in propertyValuesDictionary.items(): 103 | # For each element of the 'propertyValuesDictionary', loop through the values dictionary 104 | # and take each property ids and values and print them onto the console. 105 | for propertyId, value in valuesDictionary.items(): 106 | print(f"{elementId.guid} {propertyId.guid} {value}") 107 | -------------------------------------------------------------------------------- /parking_spaces_explained.py: -------------------------------------------------------------------------------- 1 | # Import archicad connection (required). 2 | from archicad import ACConnection 3 | # Import typing and string not essential for the code. 4 | from typing import List, Tuple, Iterable 5 | # Import itertools cycle method to use when we define the order of numbering in the rows. 6 | # It gives back the list over and over again. 7 | from itertools import cycle 8 | 9 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 10 | # 11 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 12 | # as a minimum because all utilities use the connection. 13 | conn = ACConnection.connect() 14 | # assert that the connection is alive 15 | assert conn 16 | 17 | # Create shorts of the commands, types and utilities. 18 | acc = conn.commands 19 | act = conn.types 20 | acu = conn.utilities 21 | 22 | # original comment -> ################################ CONFIGURATION ################################# 23 | # Getting the property Id (guid) for the General_ElementID property 24 | # in order to use this to identify 25 | # the property exactly when we communicate with the API 26 | # (this is a unique identifier like our social security number). 27 | propertyId = acu.GetBuiltInPropertyId('General_ElementID') 28 | # This will be the id prefix used in the element id string 29 | propertyValueStringPrefix = 'P ' 30 | 31 | # This is a method for collecting all the parking spaces from the Archicad pln file in a list. 32 | # We need the 'guid' of the classificationItem in order to uniquely identify the classification 33 | # based on we want to collect the elements with the Get elements by classification method. 34 | classificationItem = acu.FindClassificationItemInSystem( 35 | 'ARCHICAD Classification', 'Parking Space') 36 | 37 | # We collect the parking spaces in the element list using the chair classification 'guid' from above. 38 | # The elements list contains the 'guids' of the chairs. 39 | # The length of the list is the total number of parking spaces in the project. 40 | elements = acc.GetElementsByClassification( 41 | classificationItem.classificationItemId) 42 | 43 | # These variables are to consider some kind of tolerance in the 'z' and 'y' coordinate of the parking space positions 44 | # when we are sorting them by the level and the side of the building where they are. 45 | # For that particular example this wouldn't be necessary since all the parking spaces 46 | # are placed exactly on the same levels and they are sharing the exact same y coordinates. 47 | # These tolerances are useful, when we want to select some elements in a certain strip or area. 48 | # To consider some tolerances for example we can give upper and lower limit too. 49 | ROW_GROUPING_LIMIT = 0.25 50 | STORY_GROUPING_LIMIT = 1 51 | 52 | # With this function we generate a string property value for the 'General_ElementID' 53 | # since this is a string type property. 54 | # Takes as argument the storyIndex (1, 2), elemIndex (01, 02, etc.) 55 | # and using the propertyValueStringPrefix variable as the first character of the string. 56 | # The function returns a string e.g.: 'P 112' 57 | def GeneratePropertyValueString(storyIndex: int, elemIndex: int) -> str: 58 | # storyIndex 1 digits, elemIndex 2 digits and below 10 it starts with 0. 59 | return f"{propertyValueStringPrefix}{storyIndex:1d}{elemIndex:02d}" 60 | # original comment -> ################################################################################ 61 | 62 | # This function prepares NormalStringPropertyValue type from the string 63 | # generated by the function 'GeneratePropertyValueString'. 64 | # The function of GeneratePropertyValueString could be combined with this 65 | # since these two functions are working always together. 66 | def generatePropertyValue(storyIndex: int, elemIndex: int) -> act.NormalStringPropertyValue: 67 | return act.NormalStringPropertyValue(GeneratePropertyValueString(storyIndex, elemIndex)) 68 | 69 | # Create a list of the different levels and parking space rows. 70 | # Arguments: List of elements' zMin values, yMin values, tolerance limit. 71 | # Returns list of tuples(zMin, zMax) with the different levels or 72 | # (yMin, yMax) with the different row y coordinate. 73 | # The Min and Max values are always equal in our case. 74 | def createClusters(positions: Iterable[float], limit: float) -> List[Tuple[float, float]]: 75 | # When we are getting the parking spaces to the elements list 76 | # the order of the parking spaces is not following our particular logic. 77 | # For that reason 'positions' needs to be sorted to avoid duplicates in the 'clusters' list. 78 | positions = sorted(positions) 79 | # If there is no elements nothing to sort so return an empty list. 80 | # In our case we always have elemnts so this check is redundant. 81 | if len(positions) == 0: 82 | return [] 83 | # Create empty list for the end result 84 | clusters = [] 85 | # Make the 'position' list iterabel. The list is iterable so this is not required. 86 | posIter = iter(positions) 87 | # All postions are equel to the first position in the 'positions' list. 88 | # In our case we would only need 1 position i.e. firstPos. 89 | firstPos = lastPos = next(posIter) 90 | 91 | # Loop all the position and append to the 'clusters' list 92 | for pos in posIter: 93 | # the different levels or row y coordinate only. 94 | # If the actual position - lastPos which is the start at the beginning <= limit (could be == 0). 95 | if pos - lastPos <= limit: 96 | # Take the actual position as the last one and continue with the next position. 97 | lastPos = pos 98 | # Else (the actual position - last position > limit (could be > 0)). 99 | else: 100 | # Append the first and last position tuple to the clusters list. 101 | # Fisrt position and last position are always equal in our case. 102 | clusters.append((firstPos, lastPos)) 103 | # Update all the positions to be equal to pos and continue with the next position. 104 | firstPos = lastPos = pos 105 | # The last position will not reach the else statement because of the lack of next different value, 106 | # In order to be this value also added to the 'clusters' list we need to append this after the loop finishes. 107 | clusters.append((firstPos, lastPos)) 108 | return clusters 109 | 110 | 111 | # Getting all 3d bounding boxes of all the parking spaces. 112 | # The bounding box contains the x, y, z minimum and maximum values of the box 113 | # can be drawn around the element containging the whole element! 114 | # Returns a list. 115 | boundingBoxes = acc.Get3DBoundingBoxes(elements) 116 | # List of each elements and its bounding box follows. 117 | # Calling zip method on the elements list and the bounding box list. 118 | elementBoundingBoxes = list(zip(elements, boundingBoxes)) 119 | # We create the list of the different values of the levels where the parking spaces are located on. 120 | # Arguments: zMin values of the parking spaces, limit which is the tolerance of the level. 121 | zClusters = createClusters((bb.boundingBox3D.zMin for bb in boundingBoxes), STORY_GROUPING_LIMIT) 122 | 123 | # StoryIndex will increase when all parking zones on the actual story have their property values generated 124 | # and appended to the elemPropertyValues list. 125 | storyIndex = 0 126 | elemPropertyValues = [] 127 | # Loop through all the parking spaces taking the zMin and zMax. 128 | # These two values are the same practically and in that sense only the zMin positin would be enough. 129 | for (zMin, zMax) in zClusters: 130 | # Initialise elemIndex with 1 as the first parking space element on the actual floor. 131 | elemIndex = 1 132 | # Using list comprehension we prepare a list with the all the parking spaces on the same level. 133 | elemsOnStory = [e for e in elementBoundingBoxes if zMin <= e[1].boundingBox3D.zMin <= zMax] 134 | # Based on the above created list with all parking spaces on the actual floor 135 | # we are creating 'clusters' list with the row y coordinates. 136 | yClusters = createClusters((e[1].boundingBox3D.yMin for e in elemsOnStory), ROW_GROUPING_LIMIT) 137 | 138 | # This loop takes the y Min and Y Max coordinates and the row boolean list zipped and 139 | # prepares the final values of the parking space numbers. 140 | for ((yMin, yMax), reverseOrder) in zip(yClusters, cycle([False, True])): 141 | # With list comprehension we are taking the parking spaces on the actual level for the actual row only. 142 | elemsInRow = [e for e in elemsOnStory 143 | if yMin <= e[1].boundingBox3D.yMin <= yMax] 144 | 145 | # This loop generate the property values of the parking spaces on the actual floor and in the actual row 146 | # Taking the list of the elementsInRow sorted by the xMin values and ordered by the boolean value 147 | # which depends on the row. 148 | for (elem, bb) in sorted(elemsInRow, key=lambda e: e[1].boundingBox3D.xMin, reverse=reverseOrder): 149 | # Preparing and appending the property values to the final elemPropertyValues list 150 | # Archicad API type used: ElementPropertyValue() 151 | # Arguments: elementId, propertyId, 152 | # the string of the value created by the generatePropertyValue function 153 | # taking as arguments the storyIndex (level), elemIndex (index of the parking space number). 154 | elemPropertyValues.append(act.ElementPropertyValue( 155 | elem.elementId, propertyId, generatePropertyValue(storyIndex, elemIndex))) 156 | # Increase elemIndex to go to the next element. 157 | elemIndex += 1 158 | # Increase the storyIndex to go to the next story 159 | storyIndex += 1 160 | 161 | # Set the new property values. 162 | # Argument: elemPropertyValues list. 163 | acc.SetPropertyValuesOfElements(elemPropertyValues) 164 | 165 | # original comment -> # Print the result 166 | # Check and print the results: 167 | # Using the 'GetPropertyValuesOfElements' command with the 'elements' and propertyId list arguments 168 | # preapre a list with modified properties of the parking spaces. 169 | newValues = acc.GetPropertyValuesOfElements(elements, [propertyId]) 170 | # Using list comprehension preapre a list of tuples with elements ids and their property values respectively. 171 | elemAndValuePairs = [(elements[i].elementId.guid, v.propertyValue.value) for i in range(len(newValues)) for v in newValues[i].propertyValues] 172 | # Print the elem 'elemAndValuePairs' list sort by the property values. 173 | # Calling the sorted method on the list using sorting key to be the property value of the tuple. 174 | for elemAndValuePair in sorted(elemAndValuePairs, key=lambda p: p[1]): 175 | print(elemAndValuePair) 176 | -------------------------------------------------------------------------------- /room_report_explained.py: -------------------------------------------------------------------------------- 1 | # Import archicad connection (required), handle_dependencies is not necessary. 2 | from archicad import ACConnection, handle_dependencies 3 | # Import os for file operations, sys is unused, uuid for uuid generation. 4 | # Note: sys is not used in this code. 5 | import os, sys, uuid 6 | 7 | # Check if the importable (installed) if not returns an error 8 | handle_dependencies('openpyxl') 9 | 10 | # Import load_workbook from openpyxl for excel file operations. 11 | # https://openpyxl.readthedocs.io/en/stable/index.html 12 | # Note: Workbook class is not used in this code. 13 | from openpyxl import Workbook, load_workbook 14 | 15 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 16 | # 17 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 18 | # as a minimum because all utilities use the connection. 19 | conn = ACConnection.connect() 20 | assert conn 21 | 22 | # Create shorts of the commands, types and utilities. 23 | acc = conn.commands 24 | act = conn.types 25 | acu = conn.utilities 26 | 27 | # Getting the actual dirname as scriptFolder variable. 28 | # The use of realpath is to get the canonical path adn ignore symbolic links. 29 | scriptFolder = os.path.dirname(os.path.realpath(__file__)) 30 | 31 | # original comment -> ################################ CONFIGURATION ################################# 32 | # Define the output folder as cwd and output file in the cwd. 33 | outputFolder = scriptFolder 34 | outputFileName = "Room Report.xlsx" 35 | # Define the template folder as cwd and template file in the cwd. 36 | templateFolder = scriptFolder 37 | templateFileName = "RDS template.xlsx" 38 | # Create a dictionary with the cell index and initial values. 39 | cellAddressPropertyUserIdTable = { 40 | "C2": act.BuiltInPropertyUserId("Zone_ZoneName"), 41 | "G2": act.BuiltInPropertyUserId("Zone_ZoneNumber"), 42 | "G3": act.BuiltInPropertyUserId("Zone_ZoneCategoryCode"), 43 | "G6": act.BuiltInPropertyUserId("Zone_NetArea"), 44 | "G7": act.BuiltInPropertyUserId("General_NetVolume"), 45 | "G4": act.UserDefinedPropertyUserId(["WINDOW RATE (Expression)", "Window rate calculated"]), 46 | "G15": act.UserDefinedPropertyUserId(["ZONES", "Temperature Requirement"]), 47 | "G16": act.UserDefinedPropertyUserId(["ZONES", "Illuminance Requirement"]) 48 | } 49 | # Define the classification system. 50 | # To get the classificationSystemName we can use the following command: 51 | # acc.GetClassificationSystems(acc.GetClassificationSystemIds())[0].classificationSystem.name 52 | classificationSystemName = "ARCHICAD Classification" 53 | 54 | # Define the cells where we want to insert the different type of information. 55 | # C4 cell is the classification of the element 56 | insertClassificationTo = "C4" 57 | # C6-C16 related zones (connected zones) 58 | insertRelatedZonesTo = ["C" + str(row) for row in range(6, 16)] 59 | # B20-B57 equipment names 60 | insertEquipmentNamesTo = ["B" + str(row) for row in range(20, 57)] 61 | # D20-D57 equipment quantities 62 | insertEquipmentQuantitiesTo = ["D" + str(row) for row in range(20, 57)] 63 | # F20-F57 opening names 64 | insertOpeningNamesTo = ["F" + str(row) for row in range(20, 57)] 65 | # G20-G57 opening element ids 66 | insertOpeningElementIDsTo = ["G" + str(row) for row in range(20, 57)] 67 | # original comment -> ################################################################################ 68 | 69 | # This functions returns a dictionary with the elements guids : elements' details' ids, key:value pairs. 70 | # Taking as argument: elements guid list. 71 | def getElementsClassificationDictionary(elements): 72 | # Getting the guids of the elements (zones) classification 73 | classificationIdObjects = acc.GetClassificationsOfElements( 74 | elements, [acu.FindClassificationSystem(classificationSystemName)]) 75 | 76 | # This function takes out the element's classification id from the ClassificationIdsOrErrorsWrapper object. 77 | def unwrapId(classification): 78 | if classification.classificationIds[0].classificationId.classificationItemId: 79 | return classification.classificationIds[0].classificationId.classificationItemId 80 | # If there is no id it generates one. 81 | else: 82 | return act.ClassificationItemId(uuid.uuid1()) 83 | 84 | # List with the elements' classification ids (in our case these are the same for all three elements) 85 | classificationItemIds = [unwrapId(c) for c in classificationIdObjects] 86 | # Get the details of the classifications (guid, id, name, description). 87 | classificationDetails = acc.GetDetailsOfClassificationItems(classificationItemIds) 88 | 89 | # Getting the classification id from the classification details. 90 | def unwrapDetail(details): 91 | # If there is no attribute, meaning there is an 'error' attribute, use "" id. 92 | if hasattr(details, "error"): 93 | return "" 94 | return details.classificationItem.id 95 | 96 | return dict(zip(elements, [unwrapDetail(c) for c in classificationDetails])) 97 | 98 | # This function returns the elements of the 'ElementsWrapper' created by the 'GetElementsRelatedToZones' command. 99 | def unwrapElements(elementsWrapper): 100 | return elementsWrapper.elements 101 | 102 | # This function preapres the adjacent rooms dictionary 103 | # with the room guid:[List of the adjacent rooms guid] key:value pairs 104 | def getAdjacentRooms(rooms): 105 | # Getting the adjacent elements (walls) of the zones. 106 | rawBoundaryObjects = acc.GetElementsRelatedToZones(rooms, ["Wall"]) 107 | # Creating a dictionary of the rooms (guids) and their adjacent walls. 108 | # We are using the 'map' method to map (convert) the 'ElementsWrapper' to the 'unwrapElements' function 109 | # in order to get the elements (containing the guids). 110 | boundaryObjects = dict(zip(rooms, list(map(unwrapElements, rawBoundaryObjects)))) 111 | 112 | # This function is getting the guids out of the elements created above. 113 | def getGuid(elementIdArrayItem: act.ElementIdArrayItem) -> str: 114 | return str(elementIdArrayItem.elementId.guid) 115 | 116 | # Creating a dictionary of the rooms and a list of its adjacent walls ids as key:value pairs. 117 | # This step could be handled directly in the unwrapElements function 118 | # to return 'elementsWrapper.elements[i].elementId.guid' using list comprehension or dict comprehension. 119 | boundaryObjectsIds = dict({k: list(map(getGuid, v)) 120 | for k, v in boundaryObjects.items()}) 121 | # Create adjacent rooms dictionary. 122 | adjacentRooms = {} 123 | # This loop is checking all the rooms if they are adjacent: 124 | # 1. Taking the first room. 125 | # 2. Preparing an empty list in the adjacentRooms list for this room adjacent rooms. 126 | # 3. Taking all the rooms except the same as room1 of the rooms 127 | # 4. With the set.instercetion method checking all the two actual room boundary walls if there is any parity 128 | # 5. If yes the two rooms are adjacent so append room 2 to the room1 adjacent rooms list 129 | # 6. Go to next room in the rooms list 130 | for room1 in rooms: 131 | adjacentRooms[room1] = [] 132 | for room2 in rooms: 133 | if room1 is not room2 and set(boundaryObjectsIds[room2]).intersection(set(boundaryObjectsIds[room1])): 134 | adjacentRooms[room1].append(room2) 135 | 136 | return adjacentRooms 137 | 138 | # This function is getting all the library parts' names in every room. 139 | # Returns a dictionary with room ElementIdArrayItem (guid): List of the library parts' names in the room key:value pairs. 140 | def getObjectLibPartsInRooms(rooms): 141 | # Get the all the elements in the room. 142 | elementInRooms = acc.GetElementsRelatedToZones(rooms, ["Object"]) 143 | # Creating a dictionary with the room ElementIdArrayItem (guid): List of the library parts' guids in the room key:value pairs. 144 | roomElements = dict(zip(rooms, list(map(unwrapElements, elementInRooms)))) 145 | # Getting the guid of the 'General_LibraryPartName'. 146 | libPartNamePropertyId = acu.GetBuiltInPropertyId('General_LibraryPartName') 147 | # Getting the property values ('General_LibraryPartName') of all the items in all the rooms. 148 | # Note: Prepare 1 list containing all the elements of a nested list 149 | # we can sum all the nested lists with an empty list. 150 | propertyValuesDictionary = acu.GetPropertyValuesDictionary(sum(roomElements.values(), []), [libPartNamePropertyId]) 151 | return dict({room: [propertyValuesDictionary[e][libPartNamePropertyId] for e in roomElements[room]] for room in rooms}) 152 | 153 | # This function is getting all the openings' names in every room. 154 | # Returns a dictionary with room ElementIdArrayItem (guid): [List of the openings' names, General elementID zipped as tuples] 155 | # in the room key:value pairs. 156 | def getOpeningsInRooms(rooms): 157 | # Get the all the elements in the room. 158 | elementInRooms = acc.GetElementsRelatedToZones(rooms, ["Door", "Window", "Skylight", "Opening"]) 159 | # Creating a dictionary with the room ElementIdArrayItem (guid): List of the openings' guids in the room key:value pairs. 160 | roomElements = dict(zip(rooms, list(map(unwrapElements, elementInRooms)))) 161 | # Getting the guid of the 'General_LibraryPartName'. 162 | libPartNamePropertyId = acu.GetBuiltInPropertyId('General_LibraryPartName') 163 | # Getting the guid of the 'General_ElementID'. 164 | elementIdPropertyId = acu.GetBuiltInPropertyId('General_ElementID') 165 | # Getting the property values ('General_LibraryPartName' and 'General_ElementID') of all the items in all the rooms. 166 | # Note: Prepare 1 list containing all the elements of a nested list 167 | # we can sum all the nested lists with an empty list. 168 | propertyValuesDictionary = acu.GetPropertyValuesDictionary(sum(roomElements.values(), []), [libPartNamePropertyId, elementIdPropertyId]) 169 | return dict({room: list(zip([propertyValuesDictionary[e][libPartNamePropertyId] for e in roomElements[room]], 170 | [propertyValuesDictionary[e][elementIdPropertyId] for e in roomElements[room]])) 171 | for room in rooms}) 172 | 173 | # Create the WorkBookFiller class which will be handling all the excel file operations. 174 | class WorkBookFiller: 175 | # Init the class with requiredd arguments: templatePath, and rooms. 176 | def __init__(self, templatePath, rooms): 177 | self.templatePath = templatePath 178 | self.rooms = rooms 179 | # Create the 'cellValuesForRoom' dictionary to write all single type details of the room. e.g. {'C4':{room:id}}. 180 | self.cellValuesForRoom = {} 181 | # Create the 'cellValueRangeForRoom' dictionary to write all list type details of the room. e.g. {['C6'-'C15']]{room:[list of adjacent rooms]}} 182 | self.cellValueRangeForRoom = {} 183 | self.zoneNumberPropertyId = acu.GetBuiltInPropertyId('Zone_ZoneNumber') 184 | self.zoneNamePropertyId = acu.GetBuiltInPropertyId('Zone_ZoneName') 185 | self.propertyValuesDictionary = acu.GetPropertyValuesDictionary(self.rooms, [self.zoneNumberPropertyId, self.zoneNamePropertyId]) 186 | self.rooms = sorted(self.rooms, key=lambda r: self.propertyValuesDictionary[r][self.zoneNumberPropertyId]) 187 | 188 | # This function does all the excel file operations using the openpyxl module. 189 | def SaveWorkbook(self, outputPath): 190 | # Initialise the workbook. 191 | workbook = self._initWorkBook() 192 | # Fill out the cells in the workbook 193 | self._fillWorkbook(workbook) 194 | # Save the workbook to the outputPath 195 | workbook.save(outputPath) 196 | 197 | # This function prepares every celladdress data with all the rooms related property. 198 | # Arguments: dictionary of celladdresses as keys and propertyids as values. 199 | def InsertPropertyValuesTo(self, cellAddressPropertyIdTable): 200 | # Create a dictionary with room: all celladdress property ids key:value pairs. 201 | propertyValuesDictionary = acu.GetPropertyValuesDictionary(self.rooms, list(cellAddressPropertyIdTable.values())) 202 | # Take each celladdress with its property id and add to each cell to the 'cellValuesForRoom' dictionary: 203 | # the key is the cellAddress, the value is a dictionary where the keys are the rooms and the values are their propertyvalues 204 | # if the propertyvalue is in the valuedictionary of the actual room. 205 | for cellAddress, propertyId in cellAddressPropertyIdTable.items(): 206 | self.cellValuesForRoom[cellAddress] = {room: valuesDictionary[propertyId] for room, valuesDictionary in propertyValuesDictionary.items() if propertyId in valuesDictionary} 207 | 208 | # This function creates a dictionary {room:id}. 209 | # Inserting it to the 'self.cellValueRangeForRoom' with the proper celladdress key ['C4']. 210 | def InsertClassificationTo(self, cellAddress): 211 | self.cellValuesForRoom[cellAddress] = getElementsClassificationDictionary(self.rooms) 212 | 213 | # This function creates a dictionary {room: [strings contains number and value]}. 214 | # Inserting it to the 'self.cellValueRangeForRoom' with the proper celladdress keys list: {room: [strings contains number and value]}. 215 | def InsertRelatedZonesTo(self, cellAddresses): 216 | # Getting the adjacent rooms dictionary (room: adjacent rooms list) 217 | adjacentRooms = getAdjacentRooms(self.rooms) 218 | adjacentRoomIds = {} 219 | # Fill the adjacentRoomIds dictionary with room:[list of strings conatins the adjacent rooms number and name separeted with '-']. 220 | for k, v in adjacentRooms.items(): 221 | adjacentRoomIds[k] = [self.propertyValuesDictionary[item][self.zoneNumberPropertyId] + " - " + self.propertyValuesDictionary[item][self.zoneNamePropertyId] for item in v] 222 | 223 | # Add CellAddresses ['C6'-'C15'] as key and adjacentRoomIds {room:[number-name]}. 224 | self.cellValueRangeForRoom[tuple(cellAddresses)] = adjacentRoomIds 225 | 226 | # This function creates two dictionaries {room: the Library parts' names} and {room: library parts' quantities per room}. 227 | # Inserting these to the 'self.cellValueRangeForRoom' with the proper celladdress list as keys: {room:names}, {room:qtyties} respectively. 228 | def InsertObjectLibPartsTo(self, namesCellAddresses, countsCellAddresses): 229 | # Use the function 'getObjectLibPartsInRooms' and get a dictionary {room: [library parts' names]}. 230 | libpartsInRooms = getObjectLibPartsInRooms(self.rooms) 231 | libpartNamesInRooms = {} 232 | libpartCountsInRooms = {} 233 | # k is the room guid, v is the list with the library parts names in the room. 234 | for k, v in libpartsInRooms.items(): 235 | # Making an alphabetically sorted list using set which eliminates all duplicates. 236 | # The list will contain the unique library part names only. 237 | libpartNamesInRooms[k] = sorted(list(set(v))) 238 | # Count the different library parts in the room, 239 | # using list comprehension and 'v'as the list of all lements name and the unique libpart names list. 240 | libpartCountsInRooms[k] = [v.count(libpartName) for libpartName in libpartNamesInRooms[k]] 241 | 242 | # Add CellAddresses ['B20'-'B56'] as key and another dictionary {room:List of unique library parts' names} as value. 243 | self.cellValueRangeForRoom[tuple(namesCellAddresses)] = libpartNamesInRooms 244 | # Add CellAddresses ['D20'-'D56'] as key and another dictionary {room: List of library part quantities} as value. 245 | self.cellValueRangeForRoom[tuple(countsCellAddresses)] = libpartCountsInRooms 246 | 247 | # This function creates two dictionaries {room: the openings' names} and {room: openings' General_ElementIDs}. 248 | # Inserting these to the 'self.cellValueRangeForRoom' with the proper celladdress list as keys: {room:names}, {room:ids} respectively. 249 | def InsertOpeningsTo(self, namesCellAddresses, idsCellAddresses): 250 | # Use the function 'getOpeningsInRooms' and get a dictionary {room: [opening names, General_ElementIDs zipped as tuples]}. 251 | openingsInRooms = getOpeningsInRooms(self.rooms) 252 | libpartNamesInRooms = {} 253 | elementIdsInRooms = {} 254 | # k is the room guid, v is the list with the opening names and their Genral element ids. 255 | for k, v in openingsInRooms.items(): 256 | # Making a list sorted by General_ElementID. 257 | openings = sorted(v, key=lambda t: t[1]) 258 | # Getting the names of the openings to a list 259 | # and add to the 'libpartNamesInRooms' dictionary as value with the room guid as key. 260 | libpartNamesInRooms[k] = [t[0] for t in openings] 261 | # Getting the Genral element ids of the openings to a list 262 | # and add to the 'elementIdsInRooms' dictionaryas value with the room guid as key. 263 | elementIdsInRooms[k] = [t[1] for t in openings] 264 | 265 | # Add CellAddresses ['F20'-'F56'] as key and another dictionary {room:List of all the openings' names} as value. 266 | self.cellValueRangeForRoom[tuple(namesCellAddresses)] = libpartNamesInRooms 267 | # Add CellAddresses ['G20'-'G56'] as key and another dictionary {room:List of all the openings' General_ElementIDs} as value. 268 | self.cellValueRangeForRoom[tuple(idsCellAddresses)] = elementIdsInRooms 269 | 270 | # This function load the template workbook for editing. Returns the loaded workbook. 271 | def _initWorkBook(self): 272 | # Using the load_workbook method to load workbook. 273 | # (https://openpyxl.readthedocs.io/en/stable/tutorial.html#loading-from-a-file) 274 | workbook = load_workbook(self.templatePath) 275 | # The workbook will not be a template. 276 | workbook.template = False 277 | return workbook 278 | 279 | def _fillWorkbook(self, workbook): 280 | # Get the first worksheet of the template workbook. 281 | base = workbook.active 282 | # The main loop to create all worksheets for the rooms and fill out all the cells with data. 283 | for room in self.rooms: 284 | # Getting the zone name of the actual room into the 'roomName' variable. 285 | roomName = self.propertyValuesDictionary[room][self.zoneNamePropertyId].replace("/", "-") 286 | # Getting the zone number of the actual room into the 'roomId' variable. 287 | roomId = self.propertyValuesDictionary[room][self.zoneNumberPropertyId].replace("/", "-") 288 | # If room is among the rooms (this if statement wouldn't be necessary for this code): 289 | if self.propertyValuesDictionary[room][self.zoneNumberPropertyId]: 290 | # Copy the base worksheet in the same workbook. 291 | worksheet = workbook.copy_worksheet(base) 292 | # Set the title as a string of the actual room id and room name. e.g. '01 Bedroom' 293 | worksheet.title = f"{roomId} {roomName}" 294 | # Looping through cellValuesForRoom dictionary. 295 | for cellAddress, cellValueForRoom in self.cellValuesForRoom.items(): 296 | if room in cellValueForRoom: 297 | # Write the actual room id in the 'C4' cell. 298 | worksheet[cellAddress] = cellValueForRoom[room] 299 | # Print the result to the concole. 300 | print(f"{worksheet.title}!{cellAddress}={cellValueForRoom[room]}") 301 | # Looping through cellValueRangeForRoom dictionary. 302 | for cellAddressrange, cellvalue in self.cellValueRangeForRoom.items(): 303 | # Looping through the actual zipped 'cellAddressrange' and 'cellvalue[room]'. 304 | # Taking the cell address range (e.g. ['B20'-'B56']) 305 | # and zipping to the cellvalues of the actual room (Names of the library parts). 306 | # Note: Zip function is zipping till the last element of the shortest list. 307 | # The length of the zipped list of tuples will be the same as the shortest list. 308 | for cellAddress, value in zip(cellAddressrange, cellvalue[room]): 309 | # Write to the actual cells the actual values. 310 | worksheet[cellAddress] = value 311 | # Print the result to the concole. 312 | print(f"{worksheet.title}!{cellAddress}={value}") 313 | # Remove the template worksheet from the file. 314 | workbook.remove(base) 315 | 316 | # We start from here: 317 | # Create the templatepath variable path with filename with the os.path.join method. 318 | templatePath = os.path.join(templateFolder, templateFileName) 319 | # Create the main class. 320 | # Arguments: templatePath, rooms = every 'Zone' type elements. 321 | wbFiller = WorkBookFiller(templatePath, acc.GetElementsByType("Zone")) 322 | 323 | # Fill the 'self.cellValuesForRoom' dictionary with the celladdress and 324 | # the corresponding propertyvalue of the room if it exist. 325 | # We need the PropertyIds for this function since we have 'UserIds' defined in the 'cellAddressPropertyUserIdTable'. 326 | wbFiller.InsertPropertyValuesTo(dict(zip( 327 | list(cellAddressPropertyUserIdTable.keys()), 328 | acc.GetPropertyIds(list(cellAddressPropertyUserIdTable.values())) 329 | ))) 330 | 331 | # Insert related zones to the 'self.cellValueRangeForRoom' dictionary. 332 | wbFiller.InsertRelatedZonesTo(insertRelatedZonesTo) 333 | # Insert related library parts per room to the 'self.cellValueRangeForRoom' dictionary. 334 | wbFiller.InsertObjectLibPartsTo(insertEquipmentNamesTo, insertEquipmentQuantitiesTo) 335 | # Insert related openings per room to the 'self.cellValueRangeForRoom' dictionary. 336 | wbFiller.InsertOpeningsTo(insertOpeningNamesTo, insertOpeningElementIDsTo) 337 | # Insert related classification per room to the 'self.cellValueRangeForRoom' dictionary. 338 | wbFiller.InsertClassificationTo(insertClassificationTo) 339 | 340 | # Create the output path with joining the iutputfolder and output filename. 341 | outputPath = os.path.join(outputFolder, outputFileName) 342 | # This function not only saves the file but calling the other functions to do all excel operations. 343 | wbFiller.SaveWorkbook(outputPath) 344 | # Using the Archicad API 'OpenFile' utility open the excel file with the default application 345 | # for this type of files defined in the OS. 346 | acu.OpenFile(outputPath) 347 | 348 | # If the file saved successfully print out to the console the ok message. 349 | if os.path.exists(outputPath): 350 | print("Saved Room Report") 351 | -------------------------------------------------------------------------------- /unused_items_in_view_map_explained.py: -------------------------------------------------------------------------------- 1 | # Import archicad connection (required), handle_dependencies is not necessary. 2 | from archicad import ACConnection 3 | 4 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 5 | # 6 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 7 | # as a minimum because all utilities use the connection. 8 | conn = ACConnection.connect() 9 | assert conn 10 | 11 | # Create shorts of the commands, types and utilities. 12 | acc = conn.commands 13 | act = conn.types 14 | acu = conn.utilities 15 | 16 | # original comment -> ################################ CONFIGURATION ################################# 17 | # These vairables can be changed based on our needs. 18 | # Move the unused navigator items to an other folder True/False. 19 | moveToFolder = True 20 | # The name of the folder of the unused views (string). 21 | folderName = '-- UnusedViews --' 22 | # Rename the previous unused views folder True/False. 23 | renameFolderFromPreviousRun = True 24 | # The name of the folder dor the previous run (string). 25 | folderNameForPreviousRun = '-- Previous UnusedViews --' 26 | # original comment -> ################################################################################ 27 | 28 | # This function returns True if 'sourceNavigatorItemId' does exist. 29 | def isLinkNavigatorItem(item : act.NavigatorItem): 30 | return item.sourceNavigatorItemId is not None 31 | 32 | # Getting the ##'LayoutBook' navigator item tree, 33 | # with the 'GetNavigatorItemTree' using the 'NavigatorTreeId' type. 34 | layoutBookTree = acc.GetNavigatorItemTree(act.NavigatorTreeId('LayoutBook')) 35 | # Getting all navigator items with 'sourceNavigatorItemId' in the links list. 36 | links = acu.FindInNavigatorItemTree(layoutBookTree.rootItem, isLinkNavigatorItem) 37 | 38 | # In all publisher sets loop through 39 | for publisherSetName in acc.GetPublisherSetNames(): 40 | # From 'PublisherSets'/actual publisher set getting the navigator item tree. 41 | publisherSetTree = acc.GetNavigatorItemTree(act.NavigatorTreeId('PublisherSets', publisherSetName)) 42 | # Adding the actual publisher set navigator items whith 'sourceNavigatorItemId' to the links list. 43 | links += acu.FindInNavigatorItemTree(publisherSetTree.rootItem, isLinkNavigatorItem) 44 | 45 | # Getting all unique source links' guids to this set from the links list 46 | # using list comprehension. 47 | sourcesOfLinks = set(link.sourceNavigatorItemId.guid for link in links) 48 | 49 | # Getting the navigator item tree of the 'ViewMap'. 50 | viewMapTree = acc.GetNavigatorItemTree(act.NavigatorTreeId('ViewMap')) 51 | # Getting unused view tree items out of the viewMapTree if 52 | # their name is no 'folderName' and not 'folderNameForPreviousRun' 53 | # and not its navigator id is not in the source links ('sourcesOfLinks'). 54 | unusedViewTreeItems = acu.FindInNavigatorItemTree(viewMapTree.rootItem, 55 | lambda node: node.name != folderName and node.name != folderNameForPreviousRun 56 | # With this 'FindInNavigatorItemTree' nested 'FindInNavigatorItemTree' in our particular case 57 | # the code does not take the nodes and children nodes and so on into the list since we are 58 | # negating hence both our out, however if we would want the same as inclusive both the father and children nodes 59 | # would be included in the list. This would cause a bit of confusion. 60 | # In general the better way would be to start with the 'sourcesOfLinks' list check and then the folders check 61 | # to avoid the above confusion. 62 | # In the code below this confusion is handled with a for loop so it works fine in any case. 63 | and not acu.FindInNavigatorItemTree(node, lambda i: i.navigatorItemId.guid in sourcesOfLinks) 64 | ) 65 | # The filtered list will contain only the 'father' not used elements itself. 66 | unusedViewTreeItemsFiltered = [] 67 | # Loop through the 'unusedViewTreeItems' list 68 | # to collect the 'father' elements from the 'unusedViewTreeItems' list. 69 | for ii in unusedViewTreeItems: 70 | # Is it a child of an unused item? start value is False. 71 | isChildOfUnused = False 72 | # Loop through 'unusedViewTreeItems' list. 73 | for jj in unusedViewTreeItems: 74 | # If the actual element from the first loop is not the same as the actual element of the second loop 75 | # and their navigatorItemIds are equal, It means that it is a child element of a not used 'father element'. 76 | if ii != jj and acu.FindInNavigatorItemTree(jj, lambda node: node.navigatorItemId.guid == ii.navigatorItemId.guid): 77 | # Is it a child of an unused? Yes. 78 | isChildOfUnused = True 79 | # Leave it and go to the next element. 80 | break 81 | # If not False means it is True. 82 | if not isChildOfUnused: 83 | # It is a 'father' element so append to the filtered list. 84 | unusedViewTreeItemsFiltered.append(ii) 85 | # Overwrite the old 'unusedViewTreeItems' list with the filtered one. 86 | unusedViewTreeItems = unusedViewTreeItemsFiltered 87 | 88 | # Rename the name of the items in the viewMapTree to folderName. 89 | folderFromPreviousRun = acu.FindInNavigatorItemTree(viewMapTree.rootItem, lambda i: i.name == folderName) 90 | # If 'folderFromPreviousRun' exist (not empty) and 'renameFolderFromPreviousRun' is True. 91 | if folderFromPreviousRun and renameFolderFromPreviousRun: 92 | # Rename the first element of the 'folderFromPreviousRun' to folderNameForPreviousRun. 93 | acc.RenameNavigatorItem(folderFromPreviousRun[0].navigatorItemId, newName=folderNameForPreviousRun) 94 | 95 | # The base case is None. 96 | unusedViewsFolder = None 97 | 98 | # If 'moveToFolder' is True. 99 | if moveToFolder: 100 | # If 'renameFolderFromPreviousRun' is False and 'folderFromPreviousRun' is not empty. 101 | if not renameFolderFromPreviousRun and folderFromPreviousRun: 102 | # The unused views folder to be the folder with folderName. 103 | unusedViewsFolder = folderFromPreviousRun[0].navigatorItemId 104 | else: 105 | # Create unused views folder with the name of folderName. 106 | unusedViewsFolder = acc.CreateViewMapFolder(act.FolderParameters(folderName)) 107 | 108 | # Loop through the sorted 'unusedViewTreeItems'. 109 | for item in sorted(unusedViewTreeItems, key=lambda i: i.prefix + i.name): 110 | # Try - except block not necessary. 111 | try: 112 | # If 'moveToFolder' and 'unusedViewsFolder' are True and exist. 113 | if moveToFolder and unusedViewsFolder: 114 | # Move the unused navigator item to the 'unusedViewsFolder'. 115 | acc.MoveNavigatorItem(item.navigatorItemId, unusedViewsFolder) 116 | # Print out onto the concole the item prefix, item.name and the item itself. 117 | print(f"{item.prefix} {item.name}\n\t{item}") 118 | except: 119 | continue 120 | -------------------------------------------------------------------------------- /zone_numbering_explained.py: -------------------------------------------------------------------------------- 1 | # import archicad connection (required) 2 | from archicad import ACConnection 3 | # import typing and string not essential for the code 4 | from typing import List, Tuple, Iterable 5 | # import itertools cycle method but in this particular code 6 | # it wouldnt be necessary to use cycle. 7 | from itertools import cycle 8 | 9 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 10 | # 11 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 12 | # as a minimum because all utilities use the connection. 13 | conn = ACConnection.connect() 14 | # assert that the connection is alive 15 | assert conn 16 | 17 | # Create shorts of the commands, types and utilities. 18 | acc = conn.commands 19 | act = conn.types 20 | acu = conn.utilities 21 | 22 | # original comment -> ################################ CONFIGURATION ################################# 23 | # Getting the property Id (guid) for the General_ElementID property 24 | # in order to use this to identify 25 | # the property exactly when we communicate with the API 26 | # (this is a unique identifier like our social security number). 27 | propertyId = acu.GetBuiltInPropertyId('Zone_ZoneNumber') 28 | 29 | propertyValueStringPrefix = '' 30 | # We collect all the 'Zone' element into the 'elements' list 31 | # using the 'Zone' type with the GetElementsByType command. 32 | # The elements list contains the 'guids' of all the the zones in the project. 33 | elements = acc.GetElementsByType('Zone') 34 | 35 | # These variables are to consider some kind of tolerance in the 'z' and ''y coordinate of the zone positions 36 | # when we are sorting them by the level and the side of the building where they are. 37 | # For that particular example this wouldn't be necessary since all the zones are placed exactly on the same levels 38 | # and they are sharing the exact same y coordinates. 39 | # These tolerances are useful, when we want to select some elements in a certain strip or area. 40 | # To consider some tolerances for example we can give upper and lower limit too. 41 | ROW_GROUPING_LIMIT = 0.25 42 | STORY_GROUPING_LIMIT = 1 43 | 44 | # With this function we generate a string property value for the 'Zone_ZoneNumber' 45 | # since this is a string type property. 46 | # Takes as argument the storyIndex (1, 2), elemIndex (01, 02, etc.) and 47 | # returns a string e.g.: '112' 48 | def GeneratePropertyValueString(storyIndex: int, elemIndex: int) -> str: 49 | # storyIndex 1 digits, elemIndex 2 digits and below 10 it starts with 0. 50 | return f"{propertyValueStringPrefix}{storyIndex:1d}{elemIndex:02d}" 51 | # original comment -> ################################################################################ 52 | 53 | # This function prepares NormalStringPropertyValue type from the string 54 | # generated by the function 'GeneratePropertyValueString'. 55 | # The function of GeneratePropertyValueString could be combined with this 56 | # since these two functions are working always together. 57 | def generatePropertyValue(storyIndex: int, elemIndex: int) -> act.NormalStringPropertyValue: 58 | return act.NormalStringPropertyValue(GeneratePropertyValueString(storyIndex, elemIndex)) 59 | 60 | # Create a list of the different levels and building sides. 61 | # Arguments: List of elements' zMin values, yMin values, tolerance limit. 62 | # Returns list of tuples(zMin, zMax) with the different levels or 63 | # (yMin, yMax) with the different side y coordinate. 64 | # The Min and Max values are always equal in our case. 65 | def createClusters(positions: Iterable[float], limit: float) -> List[Tuple[float, float]]: 66 | # When we are getting the zones to the elements list 67 | # the order of the zones is not following our particular logic. 68 | # For that reason 'positions' needs to be sorted to avoid duplicates in the 'clusters' list. 69 | positions = sorted(positions) 70 | # If there is no elements nothing to sort so return an empty list. 71 | # In our case we always have elemnts so this check is redundant. 72 | if len(positions) == 0: 73 | return [] 74 | # Create empty list for the end result 75 | clusters = [] 76 | # Make the 'position' list iterabel. The list is iterable so this is not required. 77 | posIter = iter(positions) 78 | # All postions are equel to the first position in the 'positions' list. 79 | # In our case we would only need 1 position i.e. firstPos. 80 | firstPos = lastPos = next(posIter) 81 | 82 | # Loop all the position and append to the 'clusters' list 83 | for pos in posIter: 84 | # the different levels or side y coordinate only. 85 | # If the actual position - lastPos which is the start at the beginning <= limit (could be == 0). 86 | if pos - lastPos <= limit: 87 | # Take the actual position as the last one and continue with the next position. 88 | lastPos = pos 89 | # Else (the actual position - last position > limit (could be > 0)). 90 | else: 91 | # Append the first and last position tuple to the clusters list. 92 | # Fisrt position and last position are always equal in our case. 93 | clusters.append((firstPos, lastPos)) 94 | # Update all the positions to be equal to pos and continue with the next position. 95 | firstPos = lastPos = pos 96 | # The last position will not reach the else statement because of the lack of next different value, 97 | # In order to be this value also added to the 'clusters' list we need to append this after the loop finishes. 98 | clusters.append((firstPos, lastPos)) 99 | return clusters 100 | 101 | # Getting all 3d bounding boxes of all the zones. 102 | # The bounding box contains the x, y, z minimum and maximum values of the box 103 | # can be drawn around the element containging the whole element! 104 | # Returns a list. 105 | boundingBoxes = acc.Get3DBoundingBoxes(elements) 106 | # List of each elements and its bounding box follows. 107 | # Calling zip method on the elements list and the bounding box list. 108 | elementBoundingBoxes = list(zip(elements, boundingBoxes)) 109 | # We create the list of the different values of the levels where the zones are located on. 110 | # Arguments: zMin values of the zones, limit which is the tolerance of the level. 111 | zClusters = createClusters((bb.boundingBox3D.zMin for bb in boundingBoxes), STORY_GROUPING_LIMIT) 112 | 113 | # StoryIndex will increase when all zones on the actual level have their property values generated 114 | # and appended to the elemPropertyValues list. 115 | storyIndex = 0 116 | elemPropertyValues = [] 117 | # Loop through all the zones taking the zMin and zMax. 118 | # These two values are the same practically and in that sense only the zMin positin would be enough. 119 | for (zMin, zMax) in zClusters: 120 | # Initialise elemIndex with 1 as the first zone element on the actual floor 121 | elemIndex = 1 122 | # Using list comprehension we prepare a list with the all the zones on the same level 123 | elemsOnStory = [e for e in elementBoundingBoxes if zMin <= e[1].boundingBox3D.zMin <= zMax] 124 | # Based on the above created list with all the zones on the actual floor 125 | # we are creating 'clusters' list with the side y coordinates. 126 | yClusters = createClusters((e[1].boundingBox3D.yMin for e in elemsOnStory), ROW_GROUPING_LIMIT) 127 | 128 | # This loop takes the y Min and Y Max coordinates and the side boolean list zipped and 129 | # prepares the final values of the zone numbers. 130 | for ((yMin, yMax), reverseOrder) in zip(yClusters, cycle([False, True])): 131 | # With list comprehension we are taking the zones on the actual level for the actual side only. 132 | elemsInRow = [e for e in elemsOnStory 133 | if yMin <= e[1].boundingBox3D.yMin <= yMax] 134 | 135 | # This loop generate the property values of the zones on the actual level and actual side 136 | # Taking the list of the elementsInRow sorted by the xMin values and ordered by the boolean value 137 | # which depends on the side. 138 | for (elem, bb) in sorted(elemsInRow, key=lambda e: e[1].boundingBox3D.xMin, reverse=reverseOrder): 139 | # Preparing and appending the property values to the final elemPropertyValues list 140 | # Archicad API type used: ElementPropertyValue() 141 | # Arguments: elementId, propertyId, 142 | # the string of the value created by the generatePropertyValue function 143 | # taking as arguments the storyIndex (level), elemIndex (index number). 144 | elemPropertyValues.append(act.ElementPropertyValue( 145 | elem.elementId, propertyId, generatePropertyValue(storyIndex, elemIndex))) 146 | # Increase elemIndex to go to the next element. 147 | elemIndex += 1 148 | # Increase the storyIndex to go to the next story 149 | storyIndex += 1 150 | 151 | # Set the new property values. 152 | # Argument: elemPropertyValues list. 153 | acc.SetPropertyValuesOfElements(elemPropertyValues) 154 | 155 | # original comment -> # Print the result 156 | # Check and print the results: 157 | # Using the 'GetPropertyValuesOfElements' command with the 'elements' and propertyId list arguments 158 | # preapre a list with the zones modified properties. 159 | newValues = acc.GetPropertyValuesOfElements(elements, [propertyId]) 160 | # Using list comprehension preapre a list of tuples with elements ids and their property values respectively. 161 | elemAndValuePairs = [(elements[i].elementId.guid, v.propertyValue.value) for i in range(len(newValues)) for v in newValues[i].propertyValues] 162 | # Print the elem 'elemAndValuePairs' list sort by the property values. 163 | # Calling the sorted method on the list using sorting key to be the property value of the tuple. 164 | for elemAndValuePair in sorted(elemAndValuePairs, key=lambda p: p[1]): 165 | print(elemAndValuePair) 166 | -------------------------------------------------------------------------------- /zone_overall_dimensions_explained.py: -------------------------------------------------------------------------------- 1 | # import archicad connection (required) 2 | from archicad import ACConnection 3 | 4 | # Establish the connection with the Archicad software, Archicad must be open and the pln file must be open too. 5 | # 6 | # We can use the acu.OpenFile() utility but we need an established connection first so we need to open Archicad and a new plan 7 | # as a minimum because all utilities use the connection. 8 | conn = ACConnection.connect() 9 | # assert that the connection is alive 10 | assert conn 11 | 12 | # Create shorts of the commands, types and utilities. 13 | acc = conn.commands 14 | act = conn.types 15 | acu = conn.utilities 16 | 17 | # original comment -> ################################ CONFIGURATION ################################# 18 | # Getting the property Id (guid) of the user defined 'Zone Overall' property 19 | # from the Zones group in order to use this to identify 20 | # the property exactly when we communicate with the API 21 | # (this is a unique identifier like our social security number). 22 | propertyId = acu.GetUserDefinedPropertyId("ZONES", "Zone Overall") 23 | # We collect all the 'Zone' element into the 'elements' list 24 | # using the 'Zone' type with the GetElementsByType command. 25 | # The elements list contains the 'guids' of all the the zones in the project. 26 | elements = acc.GetElementsByType('Zone') 27 | 28 | # With this function we generate a string property value 29 | # since the user defined 'Zone Overall' is a string type property. 30 | # Takes as argument the width and height 31 | # returns a string: width x height or height x width 32 | # taking the highest of these two first. 33 | def GeneratePropertyValueString(width: float, height: float) -> str: 34 | # original comment -> # show highest value first - office preference. 35 | if width > height: 36 | return f"{width:.2f} x {height:.2f}" 37 | else: 38 | return f"{height:.2f} x {width:.2f}" 39 | # original comment -> ################################################################################ 40 | 41 | # This function prepares NormalStringPropertyValue type from the string 42 | # generated by the function 'GeneratePropertyValueString'. 43 | # The function of GeneratePropertyValueString could be combined with this 44 | # since these two functions are working always together. 45 | def generatePropertyValue(width: float, height: float) -> act.NormalStringPropertyValue: 46 | return act.NormalStringPropertyValue(GeneratePropertyValueString(width, height)) 47 | 48 | 49 | # Getting all 2d bounding boxes of all the zones. 50 | # The 2d bounding box contains the x, y minimum and maximum values of the box 51 | # can be drawn around the element containging the whole element! 52 | # Returns a list. 53 | # original comment -> # collect all the data 54 | boundingBoxes = acc.Get2DBoundingBoxes(elements) 55 | 56 | # Dictionary of each element and its bounding box follows. 57 | # Calling zip method on the elements list and the bounding box list. 58 | # original comment -> # bind bounding boxes to element ids 59 | elementBoundingBoxDict = dict(zip(elements, boundingBoxes)) 60 | 61 | # Initialise the elemntPropertyValues list to collect all the final elementproperty values. 62 | # original comment -> # calculated the widths and heights 63 | elemPropertyValues = [] 64 | # This for loop is filling up the above created list calculating the width and height. 65 | for key,value in elementBoundingBoxDict.items(): 66 | # width = xMax - xMin 67 | width = abs(value.boundingBox2D.xMax - value.boundingBox2D.xMin) 68 | # height = yMax - yMin 69 | height = abs(value.boundingBox2D.yMax - value.boundingBox2D.yMin) 70 | 71 | # Generate the property value calling the generatePropertyValue function. 72 | newPropertyValue = generatePropertyValue(width, height) 73 | # Generate and append the element property values. 74 | # ElementPropertyValue type takes arguments: 75 | # elementId, propertyId, property value 76 | elemPropertyValues.append(act.ElementPropertyValue(key.elementId, propertyId, newPropertyValue)) 77 | 78 | # original comment -> # set the new property values 79 | # Argument: elemPropertyValues list. (It takes only list even if we have one element) 80 | acc.SetPropertyValuesOfElements(elemPropertyValues) 81 | --------------------------------------------------------------------------------