mabuseif commited on
Commit
681eb4b
·
verified ·
1 Parent(s): 65de910

Update app/components.py

Browse files
Files changed (1) hide show
  1. app/components.py +434 -343
app/components.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
- BuildSustain - Building Components Module
3
 
4
  This module handles the definition of building envelope components (walls, roofs, floors,
5
- windows, doors, skylights) for the BuildSustain application. It allows users to
6
  assign constructions and fenestrations to specific components, define their areas,
7
  orientations, and other relevant properties.
8
 
@@ -24,7 +24,8 @@ logger = logging.getLogger(__name__)
24
 
25
  # Define constants
26
  COMPONENT_TYPES = ["Wall", "Roof", "Floor", "Window", "Door", "Skylight"]
27
- ORIENTATION_OPTIONS = ["A (North)", "B (South)", "C (East)", "D (West)", "Horizontal"]
 
28
 
29
  def display_components_page():
30
  """
@@ -32,6 +33,7 @@ def display_components_page():
32
  This is the main function called by main.py when the Building Components page is selected.
33
  """
34
  st.title("Building Components")
 
35
 
36
  # Display help information in an expandable section
37
  with st.expander("Help & Information"):
@@ -40,12 +42,20 @@ def display_components_page():
40
  # Initialize components in session state if not present
41
  initialize_components()
42
 
 
 
 
 
 
 
 
 
43
  # Create tabs for different component types
44
  tabs = st.tabs(COMPONENT_TYPES)
45
 
46
  for i, component_type in enumerate(COMPONENT_TYPES):
47
  with tabs[i]:
48
- display_component_tab(component_type)
49
 
50
  # Navigation buttons
51
  col1, col2 = st.columns(2)
@@ -71,268 +81,428 @@ def initialize_components():
71
  "doors": [],
72
  "skylights": []
73
  }
74
-
75
- # Initialize component editor state
76
- if "component_editor" not in st.session_state:
77
- st.session_state.component_editor = {
78
- "type": COMPONENT_TYPES[0],
79
- "name": "",
80
- "construction": "",
81
- "fenestration": "",
82
- "area": 10.0,
83
- "orientation": ORIENTATION_OPTIONS[0],
84
- "tilt": 90.0,
85
- "parent_component": "",
86
- "edit_mode": False,
87
- "original_id": ""
88
- }
89
 
90
- def display_component_tab(component_type: str):
91
  """
92
  Display the content for a specific component type tab.
93
 
94
  Args:
95
- component_type: The type of component (e.g., "Wall", "Window")
96
  """
97
- st.subheader(f"{component_type}s")
 
98
 
99
  # Get components of this type
100
- component_key = component_type.lower() + "s"
101
- components = st.session_state.project_data["components"].get(component_key, [])
102
-
103
- # Display existing components
104
- if components:
105
- display_component_list(component_type, components)
106
- else:
107
- st.info(f"No {component_type.lower()}s added yet. Use the editor below to add components.")
108
-
109
- # Component Editor
110
- st.markdown("---")
111
- st.subheader(f"{component_type} Editor")
112
- display_component_editor(component_type)
113
-
114
- def display_component_list(component_type: str, components: List[Dict[str, Any]]):
115
- """
116
- Display the list of existing components for a given type.
117
 
118
- Args:
119
- component_type: The type of component
120
- components: List of component dictionaries
121
- """
122
- # Create a DataFrame for display
123
- data = []
124
- for i, comp in enumerate(components):
125
- record = {
126
- "#": i + 1,
127
- "Name": comp["name"],
128
- "Area (m²)": comp["area"],
129
- "Orientation": comp["orientation"],
130
- "Tilt (°) ": comp["tilt"]
131
- }
132
-
133
- if component_type in ["Wall", "Roof", "Floor"]:
134
- record["Construction"] = comp["construction"]
135
- else:
136
- record["Fenestration"] = comp["fenestration"]
137
- record["Parent Component"] = comp.get("parent_component", "N/A")
138
-
139
- data.append(record)
140
 
141
- df = pd.DataFrame(data)
142
- st.dataframe(df, use_container_width=True, hide_index=True)
143
-
144
- # Edit and delete options
145
- col1, col2 = st.columns(2)
146
 
147
  with col1:
148
- selected_index = st.selectbox(
149
- f"Select {component_type} # to Edit",
150
- range(1, len(components) + 1),
151
- key=f"edit_{component_type}_selector"
152
- )
153
 
154
- if st.button(f"Edit {component_type}", key=f"edit_{component_type}_button"):
155
- # Load component data into editor
156
- component_data = components[selected_index - 1]
157
- st.session_state.component_editor = {
158
- "type": component_type,
159
- "name": component_data["name"],
160
- "construction": component_data.get("construction", ""),
161
- "fenestration": component_data.get("fenestration", ""),
162
- "area": component_data["area"],
163
- "orientation": component_data["orientation"],
164
- "tilt": component_data["tilt"],
165
- "parent_component": component_data.get("parent_component", ""),
166
- "edit_mode": True,
167
- "original_id": component_data["id"]
168
- }
169
- st.success(f"{component_type} \'{component_data['name']}\' loaded for editing.")
170
- st.rerun()
171
 
172
  with col2:
173
- selected_index_delete = st.selectbox(
174
- f"Select {component_type} # to Delete",
175
- range(1, len(components) + 1),
176
- key=f"delete_{component_type}_selector"
177
- )
178
 
179
- if st.button(f"Delete {component_type}", key=f"delete_{component_type}_button"):
180
- # Delete component
181
- component_key = component_type.lower() + "s"
182
- deleted_component = st.session_state.project_data["components"][component_key].pop(selected_index_delete - 1)
183
- st.success(f"{component_type} \'{deleted_component['name']}\' deleted.")
184
- logger.info(f"Deleted {component_type} \'{deleted_component['name']}\'")
185
- st.rerun()
186
-
187
- def display_component_editor(component_type: str):
188
- """
189
- Display the editor form for a specific component type.
190
-
191
- Args:
192
- component_type: The type of component
193
- """
194
- # Get available constructions and fenestrations
195
- available_constructions = get_available_constructions()
196
- available_fenestrations = get_available_fenestrations()
197
- available_walls = get_available_walls()
198
-
199
- # Check if the editor is currently set to this component type
200
- if st.session_state.component_editor["type"] != component_type and not st.session_state.component_editor["edit_mode"]:
201
- reset_component_editor(component_type)
202
-
203
- with st.form(f"{component_type}_editor_form"):
204
- # Component name
205
- name = st.text_input(
206
- "Component Name",
207
- value=st.session_state.component_editor["name"],
208
- help="Enter a unique name for this component."
209
- )
210
 
211
- # Create two columns for layout
212
- col1, col2 = st.columns(2)
 
 
213
 
214
- with col1:
215
- # Construction or Fenestration selection
216
- if component_type in ["Wall", "Roof", "Floor"]:
217
- construction = st.selectbox(
218
- "Construction",
219
- list(available_constructions.keys()),
220
- index=list(available_constructions.keys()).index(st.session_state.component_editor["construction"]) if st.session_state.component_editor["construction"] in available_constructions else 0,
221
- help="Select the construction assembly for this component."
222
- )
223
- fenestration = ""
224
- else:
225
- fenestration = st.selectbox(
226
- "Fenestration",
227
- list(available_fenestrations.keys()),
228
- index=list(available_fenestrations.keys()).index(st.session_state.component_editor["fenestration"]) if st.session_state.component_editor["fenestration"] in available_fenestrations else 0,
229
- help="Select the fenestration type for this component."
230
- )
231
- construction = ""
232
 
233
  # Area
234
  area = st.number_input(
235
  "Area (m²)",
236
  min_value=0.1,
237
  max_value=10000.0,
238
- value=float(st.session_state.component_editor["area"]),
239
  format="%.2f",
240
  help="Surface area of the component in square meters."
241
  )
242
-
243
- with col2:
244
- # Orientation
245
- orientation = st.selectbox(
246
- "Orientation",
247
- ORIENTATION_OPTIONS,
248
- index=ORIENTATION_OPTIONS.index(st.session_state.component_editor["orientation"]) if st.session_state.component_editor["orientation"] in ORIENTATION_OPTIONS else 0,
249
- help="Orientation of the component relative to the building orientation angle."
250
- )
251
 
252
- # Tilt
253
- tilt = st.number_input(
254
- "Tilt (°) ",
255
- min_value=0.0,
256
- max_value=180.0,
257
- value=float(st.session_state.component_editor["tilt"]),
258
- format="%.1f",
259
- help="Tilt angle of the component relative to horizontal (0° = horizontal, 90° = vertical)."
260
- )
261
-
262
- # Parent component (for windows, doors, skylights)
263
- parent_component = ""
264
- if component_type in ["Window", "Door", "Skylight"]:
265
- parent_component = st.selectbox(
266
- "Parent Component (Wall/Roof)",
267
- list(available_walls.keys()),
268
- index=list(available_walls.keys()).index(st.session_state.component_editor["parent_component"]) if st.session_state.component_editor["parent_component"] in available_walls else 0,
269
- help="Select the wall or roof component this fenestration belongs to."
270
- )
271
-
272
- # Form submission buttons
273
- col1, col2 = st.columns(2)
274
-
275
- with col1:
276
- submit_button = st.form_submit_button("Save Component")
277
-
278
- with col2:
279
- clear_button = st.form_submit_button("Clear Form")
280
-
281
- # Handle form submission
282
- if submit_button:
283
- # Validate inputs
284
- validation_errors = validate_component(
285
- component_type, name, construction, fenestration, area, orientation, tilt,
286
- parent_component, st.session_state.component_editor["edit_mode"],
287
- st.session_state.component_editor["original_id"]
288
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
- if validation_errors:
291
- # Display validation errors
292
- for error in validation_errors:
293
- st.error(error)
294
- else:
295
  # Create component data
296
  component_data = {
297
- "id": st.session_state.component_editor["original_id"] if st.session_state.component_editor["edit_mode"] else str(uuid.uuid4()),
298
  "name": name,
299
- "type": component_type,
300
- "area": area,
301
- "orientation": orientation,
302
- "tilt": tilt
303
  }
304
 
305
- if component_type in ["Wall", "Roof", "Floor"]:
 
 
 
 
 
 
 
 
 
306
  component_data["construction"] = construction
 
 
307
  else:
308
  component_data["fenestration"] = fenestration
309
  component_data["parent_component"] = parent_component
 
 
 
 
 
 
 
310
 
311
  # Handle edit mode
312
- component_key = component_type.lower() + "s"
313
- if st.session_state.component_editor["edit_mode"]:
314
- # Find and update the component
315
- components = st.session_state.project_data["components"][component_key]
316
- for i, comp in enumerate(components):
317
- if comp["id"] == st.session_state.component_editor["original_id"]:
318
- components[i] = component_data
319
- break
320
- st.success(f"{component_type} \'{name}\' updated successfully.")
321
- logger.info(f"Updated {component_type} \'{name}\'")
322
  else:
323
- # Add new component
324
- st.session_state.project_data["components"][component_key].append(component_data)
325
- st.success(f"{component_type} \'{name}\' added successfully.")
326
- logger.info(f"Added new {component_type} \'{name}\'")
327
 
328
- # Reset editor
329
- reset_component_editor(component_type)
330
- st.rerun()
331
-
332
- # Handle clear button
333
- if clear_button:
334
- reset_component_editor(component_type)
335
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  def get_available_constructions() -> Dict[str, Any]:
338
  """
@@ -343,13 +513,10 @@ def get_available_constructions() -> Dict[str, Any]:
343
  """
344
  available_constructions = {}
345
 
346
- # Add library constructions
347
- if "constructions" in st.session_state.project_data and "library" in st.session_state.project_data["constructions"]:
348
- available_constructions.update(st.session_state.project_data["constructions"]["library"])
349
-
350
- # Add project constructions
351
- if "constructions" in st.session_state.project_data and "project" in st.session_state.project_data["constructions"]:
352
- available_constructions.update(st.session_state.project_data["constructions"]["project"])
353
 
354
  return available_constructions
355
 
@@ -362,109 +529,39 @@ def get_available_fenestrations() -> Dict[str, Any]:
362
  """
363
  available_fenestrations = {}
364
 
365
- # Add library fenestrations
366
- if "fenestrations" in st.session_state.project_data and "library" in st.session_state.project_data["fenestrations"]:
367
- available_fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
368
-
369
- # Add project fenestrations
370
- if "fenestrations" in st.session_state.project_data and "project" in st.session_state.project_data["fenestrations"]:
371
- available_fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
372
 
373
  return available_fenestrations
374
 
375
- def get_available_walls() -> Dict[str, Any]:
376
  """
377
- Get all available wall components.
378
-
379
- Returns:
380
- Dict of wall name to wall properties
381
- """
382
- available_walls = {}
383
- if "components" in st.session_state.project_data and "walls" in st.session_state.project_data["components"]:
384
- for wall in st.session_state.project_data["components"]["walls"]:
385
- available_walls[wall["name"]] = wall
386
- return available_walls
387
-
388
- def validate_component(
389
- component_type: str, name: str, construction: str, fenestration: str, area: float,
390
- orientation: str, tilt: float, parent_component: str, edit_mode: bool, original_id: str
391
- ) -> List[str]:
392
- """
393
- Validate component inputs.
394
 
395
  Args:
396
- component_type: Type of component
397
- name: Component name
398
- construction: Selected construction name
399
- fenestration: Selected fenestration name
400
- area: Component area
401
- orientation: Component orientation
402
- tilt: Component tilt angle
403
- parent_component: Parent component name (for fenestrations)
404
- edit_mode: Whether in edit mode
405
- original_id: Original ID if in edit mode
406
-
407
  Returns:
408
- List of validation error messages, empty if all inputs are valid
409
  """
410
- errors = []
411
-
412
- # Validate name
413
- if not name or name.strip() == "":
414
- errors.append("Component name is required.")
415
-
416
- # Check for name uniqueness within the same component type
417
- component_key = component_type.lower() + "s"
418
- components = st.session_state.project_data["components"].get(component_key, [])
419
-
420
- for comp in components:
421
- if comp["name"] == name and (not edit_mode or comp["id"] != original_id):
422
- errors.append(f"{component_type} name \'{name}\' already exists.")
423
- break
424
-
425
- # Validate construction or fenestration selection
426
- if component_type in ["Wall", "Roof", "Floor"] and not construction:
427
- errors.append("Construction selection is required.")
428
- elif component_type in ["Window", "Door", "Skylight"] and not fenestration:
429
- errors.append("Fenestration selection is required.")
430
-
431
- # Validate area
432
- if area <= 0:
433
- errors.append("Area must be greater than zero.")
434
 
435
- # Validate orientation
436
- if orientation not in ORIENTATION_OPTIONS:
437
- errors.append("Please select a valid orientation.")
 
 
438
 
439
- # Validate tilt
440
- if tilt < 0 or tilt > 180:
441
- errors.append("Tilt angle must be between 0° and 180°.")
 
 
442
 
443
- # Validate parent component for fenestrations
444
- if component_type in ["Window", "Door", "Skylight"] and not parent_component:
445
- errors.append("Parent component selection is required for fenestrations.")
446
-
447
- return errors
448
-
449
- def reset_component_editor(component_type: str):
450
- """
451
- Reset the component editor to default values for the given type.
452
-
453
- Args:
454
- component_type: The type of component
455
- """
456
- st.session_state.component_editor = {
457
- "type": component_type,
458
- "name": "",
459
- "construction": "",
460
- "fenestration": "",
461
- "area": 10.0,
462
- "orientation": ORIENTATION_OPTIONS[0],
463
- "tilt": 90.0 if component_type == "Wall" else 0.0,
464
- "parent_component": "",
465
- "edit_mode": False,
466
- "original_id": ""
467
- }
468
 
469
  def display_components_help():
470
  """
@@ -473,32 +570,26 @@ def display_components_help():
473
  st.markdown("""
474
  ### Building Components Help
475
 
476
- This section allows you to define the individual components of your building envelope, such as walls, roofs, floors, windows, doors, and skylights.
477
 
478
  **Key Concepts:**
479
 
480
- * **Component**: A specific part of the building envelope (e.g., "North Wall", "Living Room Window").
481
- * **Construction**: The multi-layer assembly assigned to opaque components (walls, roofs, floors).
482
- * **Fenestration**: The glazing or door system assigned to transparent or operable components (windows, doors, skylights).
483
- * **Area**: The surface area of the component in square meters.
484
- * **Orientation**: The direction the component faces relative to the building orientation angle (A=North, B=South, C=East, D=West, Horizontal).
485
- * **Tilt**: The angle of the component relative to horizontal (0° = horizontal, 90° = vertical).
486
- * **Parent Component**: For fenestrations, the wall or roof component they are part of.
487
 
488
  **Workflow:**
489
 
490
- 1. Select the tab for the component type you want to define (e.g., "Walls").
491
- 2. Use the editor to add new components:
492
- * Give the component a unique name.
493
- * Select the appropriate construction (for walls, roofs, floors) or fenestration (for windows, doors, skylights) from your project library.
494
- * Enter the area, orientation, and tilt.
495
- * For fenestrations, select the parent wall or roof component.
496
- 3. Save the component.
497
- 4. Repeat for all building envelope components.
498
- 5. Edit or delete components as needed using the controls above the editor.
499
 
500
- **Important:**
501
 
502
- * Ensure all constructions and fenestrations you need are defined in the Material Library and Construction pages before defining components.
503
- * The accuracy of your load calculations depends heavily on the correct definition of these components.
 
 
504
  """)
 
1
  """
2
+ HVAC Load Calculator - Building Components Module
3
 
4
  This module handles the definition of building envelope components (walls, roofs, floors,
5
+ windows, doors, skylights) for the HVAC Load Calculator application. It allows users to
6
  assign constructions and fenestrations to specific components, define their areas,
7
  orientations, and other relevant properties.
8
 
 
24
 
25
  # Define constants
26
  COMPONENT_TYPES = ["Wall", "Roof", "Floor", "Window", "Door", "Skylight"]
27
+ ORIENTATION_OPTIONS = ["A (North)", "B (South)", "C (East)", "D (West)", "Custom"]
28
+ ORIENTATION_MAP = {'A (North)': 0.0, 'B (South)': 180.0, 'C (East)': 90.0, 'D (West)': 270.0, 'Custom': None}
29
 
30
  def display_components_page():
31
  """
 
33
  This is the main function called by main.py when the Building Components page is selected.
34
  """
35
  st.title("Building Components")
36
+ st.write("Define walls, roofs, floors, windows, doors, and skylights based on ASHRAE 2005 Handbook of Fundamentals.")
37
 
38
  # Display help information in an expandable section
39
  with st.expander("Help & Information"):
 
42
  # Initialize components in session state if not present
43
  initialize_components()
44
 
45
+ # Check if rerun is pending
46
+ if 'components_rerun_pending' not in st.session_state:
47
+ st.session_state.components_rerun_pending = False
48
+
49
+ if st.session_state.components_rerun_pending:
50
+ st.session_state.components_rerun_pending = False
51
+ st.rerun()
52
+
53
  # Create tabs for different component types
54
  tabs = st.tabs(COMPONENT_TYPES)
55
 
56
  for i, component_type in enumerate(COMPONENT_TYPES):
57
  with tabs[i]:
58
+ display_component_tab(component_type.lower() + "s")
59
 
60
  # Navigation buttons
61
  col1, col2 = st.columns(2)
 
81
  "doors": [],
82
  "skylights": []
83
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ def display_component_tab(comp_type: str):
86
  """
87
  Display the content for a specific component type tab.
88
 
89
  Args:
90
+ comp_type: The type of component (e.g., "walls", "windows")
91
  """
92
+ # Get singular form for display
93
+ comp_singular = comp_type[:-1].capitalize()
94
 
95
  # Get components of this type
96
+ components = st.session_state.project_data["components"].get(comp_type, [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ # Get available items for this component type
99
+ if comp_type in ["walls", "roofs", "floors"]:
100
+ available_items = get_available_constructions()
101
+ else: # Windows, doors, skylights
102
+ available_items = get_available_fenestrations()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ # Split the display into two columns
105
+ col1, col2 = st.columns([3, 2])
 
 
 
106
 
107
  with col1:
108
+ st.subheader(f"Saved {comp_type.capitalize()}")
 
 
 
 
109
 
110
+ if components:
111
+ # Display components in a table format based on component type
112
+ if comp_type in ["walls", "windows"]:
113
+ display_wall_window_table(comp_type, components)
114
+ elif comp_type in ["roofs", "skylights"]:
115
+ display_roof_skylight_table(comp_type, components)
116
+ elif comp_type == "floors":
117
+ display_floor_table(comp_type, components)
118
+ else: # doors
119
+ display_door_table(comp_type, components)
120
+ else:
121
+ st.write(f"No {comp_type} defined.")
 
 
 
 
 
122
 
123
  with col2:
124
+ st.subheader(f"{comp_singular} Editor/Creator")
 
 
 
 
125
 
126
+ # Check if we have an editor state for this component type
127
+ editor_key = f"{comp_type}_editor"
128
+ if editor_key not in st.session_state:
129
+ st.session_state[editor_key] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ # Check if we have an action state for this component type
132
+ action_key = f"{comp_type}_action"
133
+ if action_key not in st.session_state:
134
+ st.session_state[action_key] = {"action": None, "id": None}
135
 
136
+ # Display the editor form
137
+ with st.form(f"{comp_type}_editor_form", clear_on_submit=True):
138
+ editor_state = st.session_state.get(editor_key, {})
139
+ is_edit = editor_state.get("is_edit", False)
140
+
141
+ # Component name
142
+ name = st.text_input(
143
+ "Name",
144
+ value=editor_state.get("name", ""),
145
+ help="Enter a unique name for this component."
146
+ )
 
 
 
 
 
 
 
147
 
148
  # Area
149
  area = st.number_input(
150
  "Area (m²)",
151
  min_value=0.1,
152
  max_value=10000.0,
153
+ value=float(editor_state.get("area", 10.0)),
154
  format="%.2f",
155
  help="Surface area of the component in square meters."
156
  )
 
 
 
 
 
 
 
 
 
157
 
158
+ # Component-specific inputs
159
+ if comp_type in ["walls", "windows", "doors"]:
160
+ # Elevation (orientation)
161
+ elevation = st.selectbox(
162
+ "Elevation",
163
+ ORIENTATION_OPTIONS,
164
+ index=ORIENTATION_OPTIONS.index(editor_state.get("elevation", ORIENTATION_OPTIONS[0])) if editor_state.get("elevation") in ORIENTATION_OPTIONS else 0,
165
+ help="Orientation of the component relative to the building orientation angle."
166
+ )
167
+
168
+ # Custom rotation if elevation is custom
169
+ rotation = 0.0
170
+ if elevation == "Custom":
171
+ rotation = st.number_input(
172
+ "Rotation (°)",
173
+ min_value=0.0,
174
+ max_value=359.9,
175
+ value=float(editor_state.get("rotation", 0.0)),
176
+ format="%.1f",
177
+ help="Custom rotation angle in degrees (0° = North, 90° = East, 180° = South, 270° = West)."
178
+ )
179
+ else:
180
+ rotation = ORIENTATION_MAP[elevation]
181
+
182
+ # Tilt
183
+ tilt = st.number_input(
184
+ "Tilt (°)",
185
+ min_value=0.0,
186
+ max_value=180.0,
187
+ value=float(editor_state.get("tilt", 90.0)),
188
+ format="%.1f",
189
+ help="Tilt angle of the component relative to horizontal (0° = horizontal, 90° = vertical)."
190
+ )
191
+
192
+ elif comp_type in ["roofs", "skylights"]:
193
+ # Orientation
194
+ orientation = st.number_input(
195
+ "Orientation (°)",
196
+ min_value=0.0,
197
+ max_value=359.9,
198
+ value=float(editor_state.get("orientation", 0.0)),
199
+ format="%.1f",
200
+ help="Orientation angle in degrees (0° = North, 90° = East, 180° = South, 270° = West)."
201
+ )
202
+
203
+ # Tilt
204
+ tilt = st.number_input(
205
+ "Tilt (°)",
206
+ min_value=0.0,
207
+ max_value=180.0,
208
+ value=float(editor_state.get("tilt", 30.0)),
209
+ format="%.1f",
210
+ help="Tilt angle of the component relative to horizontal (0° = horizontal, 90° = vertical)."
211
+ )
212
+
213
+ # Construction or fenestration selection
214
+ if comp_type in ["walls", "roofs", "floors"]:
215
+ construction = st.selectbox(
216
+ "Construction",
217
+ list(available_items.keys()),
218
+ index=list(available_items.keys()).index(editor_state.get("construction", "")) if editor_state.get("construction", "") in available_items else 0,
219
+ help="Select the construction assembly for this component."
220
+ )
221
+ else: # Windows, doors, skylights
222
+ # For fenestrations, we need to select a parent component
223
+ parent_components = get_parent_components(comp_type)
224
+
225
+ if not parent_components:
226
+ st.error(f"No parent components available. Please create walls/roofs first.")
227
+
228
+ parent_component = st.selectbox(
229
+ "Parent Component",
230
+ list(parent_components.keys()),
231
+ index=list(parent_components.keys()).index(editor_state.get("parent_component", "")) if editor_state.get("parent_component", "") in parent_components else 0,
232
+ help="Select the wall or roof component this fenestration belongs to."
233
+ )
234
+
235
+ fenestration = st.selectbox(
236
+ "Fenestration",
237
+ list(available_items.keys()),
238
+ index=list(available_items.keys()).index(editor_state.get("fenestration", "")) if editor_state.get("fenestration", "") in available_items else 0,
239
+ help="Select the fenestration type for this component."
240
+ )
241
+
242
+ # Submit buttons
243
+ col1, col2 = st.columns(2)
244
+ with col1:
245
+ submit_label = "Update" if is_edit else "Add"
246
+ submit = st.form_submit_button(f"{submit_label} {comp_singular}")
247
+
248
+ with col2:
249
+ cancel = st.form_submit_button("Cancel")
250
 
251
+ # Handle form submission
252
+ if submit:
 
 
 
253
  # Create component data
254
  component_data = {
255
+ "id": str(uuid.uuid4()),
256
  "name": name,
257
+ "area": area
 
 
 
258
  }
259
 
260
+ if comp_type in ["walls", "windows", "doors"]:
261
+ component_data["elevation"] = elevation
262
+ component_data["rotation"] = rotation
263
+ component_data["tilt"] = tilt
264
+
265
+ elif comp_type in ["roofs", "skylights"]:
266
+ component_data["orientation"] = orientation
267
+ component_data["tilt"] = tilt
268
+
269
+ if comp_type in ["walls", "roofs", "floors"]:
270
  component_data["construction"] = construction
271
+ # Get U-value from construction
272
+ component_data["u_value"] = available_items[construction].get("u_value", 0.0)
273
  else:
274
  component_data["fenestration"] = fenestration
275
  component_data["parent_component"] = parent_component
276
+ # Get U-value and other properties from fenestration
277
+ component_data["u_value"] = available_items[fenestration].get("u_value", 0.0)
278
+ if comp_type == "doors":
279
+ component_data["solar_absorptivity"] = available_items[fenestration].get("solar_absorptivity", 0.7)
280
+ else:
281
+ component_data["shgc"] = available_items[fenestration].get("shgc", 0.7)
282
+ component_data["visible_transmittance"] = available_items[fenestration].get("visible_transmittance", 0.7)
283
 
284
  # Handle edit mode
285
+ if is_edit:
286
+ index = editor_state.get("index", 0)
287
+ st.session_state.project_data["components"][comp_type][index] = component_data
288
+ st.success(f"{comp_singular} '{name}' updated!")
 
 
 
 
 
 
289
  else:
290
+ st.session_state.project_data["components"][comp_type].append(component_data)
291
+ st.success(f"{comp_singular} '{name}' added!")
 
 
292
 
293
+ # Clear editor state
294
+ st.session_state[editor_key] = {}
295
+ st.session_state[action_key] = {"action": "save", "id": str(uuid.uuid4())}
296
+ st.session_state.components_rerun_pending = True
297
+
298
+ elif cancel:
299
+ # Clear editor state
300
+ st.session_state[editor_key] = {}
301
+ st.session_state[action_key] = {"action": "cancel", "id": str(uuid.uuid4())}
302
+ st.session_state.components_rerun_pending = True
303
+
304
+ def display_wall_window_table(comp_type: str, components: List[Dict[str, Any]]):
305
+ """Display a table of walls or windows with appropriate columns."""
306
+ # Create column headers
307
+ cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1])
308
+ cols[0].write("**Name**")
309
+ cols[1].write("**U-Value**")
310
+ cols[2].write("**Area (m²)**")
311
+ cols[3].write("**Elevation**")
312
+ cols[4].write("**Rotation (°)**")
313
+ cols[5].write("**Tilt (°)**")
314
+ cols[6].write("**Edit**")
315
+ cols[7].write("**Delete**")
316
+
317
+ # Display each component
318
+ for idx, comp in enumerate(components):
319
+ cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1])
320
+ cols[0].write(comp["name"])
321
+ cols[1].write(f"{comp.get('u_value', 0.0):.3f}")
322
+ cols[2].write(f"{comp['area']:.2f}")
323
+ cols[3].write(comp.get("elevation", "N/A"))
324
+ cols[4].write(f"{comp.get('rotation', 0.0):.1f}")
325
+ cols[5].write(f"{comp.get('tilt', 90.0):.1f}")
326
+
327
+ # Edit button
328
+ edit_key = f"edit_{comp_type}_{comp['name']}_{idx}"
329
+ with cols[6].container():
330
+ if st.button("Edit", key=edit_key):
331
+ editor_data = {
332
+ "index": idx,
333
+ "name": comp["name"],
334
+ "area": comp["area"],
335
+ "elevation": comp.get("elevation", ORIENTATION_OPTIONS[0]),
336
+ "rotation": comp.get("rotation", 0.0),
337
+ "tilt": comp.get("tilt", 90.0),
338
+ "is_edit": True
339
+ }
340
+
341
+ if comp_type in ["walls", "roofs", "floors"]:
342
+ editor_data["construction"] = comp.get("construction", "")
343
+ else: # Windows, doors, skylights
344
+ editor_data["fenestration"] = comp.get("fenestration", "")
345
+ editor_data["parent_component"] = comp.get("parent_component", "")
346
+
347
+ st.session_state[f"{comp_type}_editor"] = editor_data
348
+ st.session_state[f"{comp_type}_action"] = {"action": "edit", "id": str(uuid.uuid4())}
349
+ st.session_state.components_rerun_pending = True
350
+
351
+ # Delete button
352
+ delete_key = f"delete_{comp_type}_{comp['name']}_{idx}"
353
+ with cols[7].container():
354
+ if st.button("Delete", key=delete_key):
355
+ st.session_state.project_data["components"][comp_type].pop(idx)
356
+ st.success(f"{comp_type[:-1].capitalize()} '{comp['name']}' deleted!")
357
+ st.session_state[f"{comp_type}_action"] = {"action": "delete", "id": str(uuid.uuid4())}
358
+ st.session_state.components_rerun_pending = True
359
+
360
+ def display_roof_skylight_table(comp_type: str, components: List[Dict[str, Any]]):
361
+ """Display a table of roofs or skylights with appropriate columns."""
362
+ # Create column headers
363
+ cols = st.columns([2, 1, 1, 1, 1, 1, 1])
364
+ cols[0].write("**Name**")
365
+ cols[1].write("**U-Value**")
366
+ cols[2].write("**Area (m²)**")
367
+ cols[3].write("**Orientation (°)**")
368
+ cols[4].write("**Tilt (°)**")
369
+ cols[5].write("**Edit**")
370
+ cols[6].write("**Delete**")
371
+
372
+ # Display each component
373
+ for idx, comp in enumerate(components):
374
+ cols = st.columns([2, 1, 1, 1, 1, 1, 1])
375
+ cols[0].write(comp["name"])
376
+ cols[1].write(f"{comp.get('u_value', 0.0):.3f}")
377
+ cols[2].write(f"{comp['area']:.2f}")
378
+ cols[3].write(f"{comp.get('orientation', 0.0):.1f}")
379
+ cols[4].write(f"{comp.get('tilt', 0.0):.1f}")
380
+
381
+ # Edit button
382
+ edit_key = f"edit_{comp_type}_{comp['name']}_{idx}"
383
+ with cols[5].container():
384
+ if st.button("Edit", key=edit_key):
385
+ editor_data = {
386
+ "index": idx,
387
+ "name": comp["name"],
388
+ "area": comp["area"],
389
+ "orientation": comp.get("orientation", 0.0),
390
+ "tilt": comp.get("tilt", 0.0),
391
+ "is_edit": True
392
+ }
393
+
394
+ if comp_type in ["roofs"]:
395
+ editor_data["construction"] = comp.get("construction", "")
396
+ else: # skylights
397
+ editor_data["fenestration"] = comp.get("fenestration", "")
398
+ editor_data["parent_component"] = comp.get("parent_component", "")
399
+
400
+ st.session_state[f"{comp_type}_editor"] = editor_data
401
+ st.session_state[f"{comp_type}_action"] = {"action": "edit", "id": str(uuid.uuid4())}
402
+ st.session_state.components_rerun_pending = True
403
+
404
+ # Delete button
405
+ delete_key = f"delete_{comp_type}_{comp['name']}_{idx}"
406
+ with cols[6].container():
407
+ if st.button("Delete", key=delete_key):
408
+ st.session_state.project_data["components"][comp_type].pop(idx)
409
+ st.success(f"{comp_type[:-1].capitalize()} '{comp['name']}' deleted!")
410
+ st.session_state[f"{comp_type}_action"] = {"action": "delete", "id": str(uuid.uuid4())}
411
+ st.session_state.components_rerun_pending = True
412
+
413
+ def display_floor_table(comp_type: str, components: List[Dict[str, Any]]):
414
+ """Display a table of floors with appropriate columns."""
415
+ # Create column headers
416
+ cols = st.columns([2, 1, 1, 1, 1])
417
+ cols[0].write("**Name**")
418
+ cols[1].write("**U-Value**")
419
+ cols[2].write("**Area (m²)**")
420
+ cols[3].write("**Edit**")
421
+ cols[4].write("**Delete**")
422
+
423
+ # Display each component
424
+ for idx, comp in enumerate(components):
425
+ cols = st.columns([2, 1, 1, 1, 1])
426
+ cols[0].write(comp["name"])
427
+ cols[1].write(f"{comp.get('u_value', 0.0):.3f}")
428
+ cols[2].write(f"{comp['area']:.2f}")
429
+
430
+ # Edit button
431
+ edit_key = f"edit_{comp_type}_{comp['name']}_{idx}"
432
+ with cols[3].container():
433
+ if st.button("Edit", key=edit_key):
434
+ editor_data = {
435
+ "index": idx,
436
+ "name": comp["name"],
437
+ "area": comp["area"],
438
+ "construction": comp.get("construction", ""),
439
+ "is_edit": True
440
+ }
441
+
442
+ st.session_state[f"{comp_type}_editor"] = editor_data
443
+ st.session_state[f"{comp_type}_action"] = {"action": "edit", "id": str(uuid.uuid4())}
444
+ st.session_state.components_rerun_pending = True
445
+
446
+ # Delete button
447
+ delete_key = f"delete_{comp_type}_{comp['name']}_{idx}"
448
+ with cols[4].container():
449
+ if st.button("Delete", key=delete_key):
450
+ st.session_state.project_data["components"][comp_type].pop(idx)
451
+ st.success(f"{comp_type[:-1].capitalize()} '{comp['name']}' deleted!")
452
+ st.session_state[f"{comp_type}_action"] = {"action": "delete", "id": str(uuid.uuid4())}
453
+ st.session_state.components_rerun_pending = True
454
+
455
+ def display_door_table(comp_type: str, components: List[Dict[str, Any]]):
456
+ """Display a table of doors with appropriate columns."""
457
+ # Create column headers
458
+ cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1])
459
+ cols[0].write("**Name**")
460
+ cols[1].write("**U-Value**")
461
+ cols[2].write("**Solar Abs.**")
462
+ cols[3].write("**Elevation**")
463
+ cols[4].write("**Rotation (°)**")
464
+ cols[5].write("**Tilt (°)**")
465
+ cols[6].write("**Edit**")
466
+ cols[7].write("**Delete**")
467
+
468
+ # Display each component
469
+ for idx, comp in enumerate(components):
470
+ cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1])
471
+ cols[0].write(comp["name"])
472
+ cols[1].write(f"{comp.get('u_value', 0.0):.3f}")
473
+ cols[2].write(f"{comp.get('solar_absorptivity', 0.7):.2f}")
474
+ cols[3].write(comp.get("elevation", "N/A"))
475
+ cols[4].write(f"{comp.get('rotation', 0.0):.1f}")
476
+ cols[5].write(f"{comp.get('tilt', 90.0):.1f}")
477
+
478
+ # Edit button
479
+ edit_key = f"edit_{comp_type}_{comp['name']}_{idx}"
480
+ with cols[6].container():
481
+ if st.button("Edit", key=edit_key):
482
+ editor_data = {
483
+ "index": idx,
484
+ "name": comp["name"],
485
+ "area": comp["area"],
486
+ "elevation": comp.get("elevation", ORIENTATION_OPTIONS[0]),
487
+ "rotation": comp.get("rotation", 0.0),
488
+ "tilt": comp.get("tilt", 90.0),
489
+ "fenestration": comp.get("fenestration", ""),
490
+ "parent_component": comp.get("parent_component", ""),
491
+ "is_edit": True
492
+ }
493
+
494
+ st.session_state[f"{comp_type}_editor"] = editor_data
495
+ st.session_state[f"{comp_type}_action"] = {"action": "edit", "id": str(uuid.uuid4())}
496
+ st.session_state.components_rerun_pending = True
497
+
498
+ # Delete button
499
+ delete_key = f"delete_{comp_type}_{comp['name']}_{idx}"
500
+ with cols[7].container():
501
+ if st.button("Delete", key=delete_key):
502
+ st.session_state.project_data["components"][comp_type].pop(idx)
503
+ st.success(f"{comp_type[:-1].capitalize()} '{comp['name']}' deleted!")
504
+ st.session_state[f"{comp_type}_action"] = {"action": "delete", "id": str(uuid.uuid4())}
505
+ st.session_state.components_rerun_pending = True
506
 
507
  def get_available_constructions() -> Dict[str, Any]:
508
  """
 
513
  """
514
  available_constructions = {}
515
 
516
+ # Add constructions from session state
517
+ if "constructions" in st.session_state.project_data:
518
+ for construction in st.session_state.project_data["constructions"]:
519
+ available_constructions[construction["name"]] = construction
 
 
 
520
 
521
  return available_constructions
522
 
 
529
  """
530
  available_fenestrations = {}
531
 
532
+ # Add fenestrations from session state
533
+ if "materials" in st.session_state.project_data:
534
+ for material in st.session_state.project_data["materials"]:
535
+ if material.get("type") in ["window", "door", "skylight"]:
536
+ available_fenestrations[material["name"]] = material
 
 
537
 
538
  return available_fenestrations
539
 
540
+ def get_parent_components(comp_type: str) -> Dict[str, Any]:
541
  """
542
+ Get appropriate parent components for a fenestration type.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
544
  Args:
545
+ comp_type: The type of fenestration component
546
+
 
 
 
 
 
 
 
 
 
547
  Returns:
548
+ Dict of parent component name to parent component properties
549
  """
550
+ parent_components = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
+ if comp_type == "windows" or comp_type == "doors":
553
+ # Windows and doors can only be on walls
554
+ if "walls" in st.session_state.project_data["components"]:
555
+ for wall in st.session_state.project_data["components"]["walls"]:
556
+ parent_components[wall["name"]] = wall
557
 
558
+ elif comp_type == "skylights":
559
+ # Skylights can only be on roofs
560
+ if "roofs" in st.session_state.project_data["components"]:
561
+ for roof in st.session_state.project_data["components"]["roofs"]:
562
+ parent_components[roof["name"]] = roof
563
 
564
+ return parent_components
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
  def display_components_help():
567
  """
 
570
  st.markdown("""
571
  ### Building Components Help
572
 
573
+ This section allows you to define the building envelope components using the constructions and fenestrations you've created.
574
 
575
  **Key Concepts:**
576
 
577
+ * **Walls, Roofs, Floors**: Opaque components that use constructions from the Construction section.
578
+ * **Windows, Doors, Skylights**: Transparent components that use fenestrations from the Material Library section.
579
+ * **Orientation**: Direction the component faces (A=North, B=South, C=East, D=West).
580
+ * **Tilt**: Angle of the component relative to horizontal (0° = horizontal, 90° = vertical).
581
+ * **Parent Component**: The wall or roof that contains a window, door, or skylight.
 
 
582
 
583
  **Workflow:**
584
 
585
+ 1. First define opaque components (walls, roofs, floors) using constructions.
586
+ 2. Then define transparent components (windows, doors, skylights) and assign them to parent components.
587
+ 3. Ensure all major building envelope components are included for accurate load calculations.
 
 
 
 
 
 
588
 
589
+ **Tips:**
590
 
591
+ * For walls, use the elevation options (A, B, C, D) to easily align with the building orientation.
592
+ * For roofs and skylights, specify the orientation angle directly.
593
+ * Ensure the total area of windows and doors doesn't exceed the area of their parent walls.
594
+ * Ensure the total area of skylights doesn't exceed the area of their parent roofs.
595
  """)