Last active
June 4, 2025 01:07
-
-
Save arr2036/61fb61ec50c9091b566d08a5987a2187 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
SPDX-License-Identifier: MIT | |
Copyright (c) 2025 Arran Cudbard-Bell | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in | |
all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
""" | |
import math | |
import re | |
def rectangle_bay_areas(bay_length, ridge_length, joist_width, oc_spacing): | |
# Get the number of full bays | |
full_bays = int(ridge_length // oc_spacing) | |
# Because two half joists (either side of the centre) make a whole joist! | |
effective_width = oc_spacing - joist_width | |
# Create a list of bays with the area (which will be identical because this is a | |
# rectangle). Technically this is incorrect, because the facet might not be a perfect | |
# rectangle, but it's good enough. | |
areas = [{ "width" : effective_width, "area" : (bay_length * effective_width)}] * full_bays | |
remainder = ridge_length % oc_spacing | |
if remainder > joist_width: | |
effective_width = remainder - joist_width | |
areas.append({ "width" : effective_width, "area" : bay_length * (effective_width)}) | |
return areas | |
def triangle_bay_areas(max_height, ridge_length, joist_width, oc_spacing): | |
i = 0 | |
half_joist = joist_width / 2 | |
# Get the number of full bays | |
full_bays = int(ridge_length // oc_spacing) | |
# base_width is the width of the bay which is a constant | |
base_width = oc_spacing - joist_width | |
areas = [] | |
for i in range(full_bays): | |
# Our x values actually start half a joist in (from the facia), | |
# and then half a joist on centre meaning we add a whole joist. | |
x1 = (i * oc_spacing) + half_joist | |
x2 = x1 + (oc_spacing - joist_width) | |
# Get the height of the triangle at different points in the base | |
y1 = max_height * (1 - x1 / ridge_length) | |
y2 = max_height * (1 - x2 / ridge_length) | |
# Work out the average height of the bay across the slope. | |
# This lets us calculate the area of the bay as a whole without | |
# separately calculating the area of the triangular part of the bay | |
# and the rectangular part of the bay, because the outy bit | |
# (below the average point) is the same as the inny bit | |
# (above the average point). | |
avg_y = (y1 + y2) / 2 | |
areas.append({ "width" : base_width, "area" : avg_y * base_width }) | |
# Calculate the _next_ bay if we had a full bay, otherwise we calculate | |
# the initial bay. | |
if i > 0: | |
i = i + 1 | |
remainder = ridge_length % oc_spacing | |
if remainder > joist_width: | |
x1 = (i * oc_spacing) + half_joist | |
x2 = x1 + (remainder - joist_width) | |
base_width = x2 - x1 | |
if base_width > joist_width: | |
y1 = max_height * (1 - x1 / ridge_length) | |
y2 = max_height * (1 - x2 / ridge_length) | |
avg_y = (y1 + y2) / 2 | |
areas.append({ "width" : base_width, "area" : avg_y * base_width}) | |
return areas | |
def symmetrical_trapezoid_bay_areas(max_height, ridge_length, eave_length, joist_width, oc_spacing): | |
# This code is terrible at if the total length of the ridge is not a multiple | |
# of the OC value... You're probably better off calculating the areas separately. | |
base_length = ridge_length / 2 | |
if eave_length > 0: | |
base_length -= eave_length / 2 | |
langle_areas = triangle_bay_areas(max_height, base_length, joist_width, oc_spacing); | |
tangle_areas = rectangle_bay_areas(max_height, eave_length, joist_width, oc_spacing) | |
return langle_areas[::-1] + tangle_areas + langle_areas | |
def parse_length(text, default_unit='feet'): | |
""" | |
Convert a string representing feet and/or inches into total inches. | |
Supports floats. Unqualified numbers default to the given unit. | |
Parameters: | |
text (str): Input string (e.g., "5'10\"", "6.25", "5.5' 3.5\"") | |
default_unit (str): 'feet' or 'inches' | |
Returns: | |
float: Total inches | |
""" | |
feet = 0.0 | |
inches = 0.0 | |
text = text.strip() | |
# Match feet and inches with optional float values | |
match = re.match(r"""(?i) | |
^\s* | |
(?:(\d+(?:\.\d+)?)\s*')? # feet (optional, supports floats) | |
\s* | |
(?:(\d+(?:\.\d+)?)\s*(?:\"|in))? # inches (optional, supports floats) | |
\s*$ | |
""", text, re.VERBOSE) | |
if match: | |
feet_part, inches_part = match.groups() | |
if feet_part: | |
feet = float(feet_part) | |
if inches_part: | |
inches = float(inches_part) | |
else: | |
# Handle unqualified numeric input | |
try: | |
value = float(text) | |
if default_unit.lower() == 'feet': | |
feet = value | |
elif default_unit.lower() == 'inches': | |
inches = value | |
else: | |
raise ValueError(f"Unknown default unit: {default_unit!r}") | |
except ValueError: | |
raise ValueError(f"Could not parse length: {text!r}") | |
return (feet * 12) + inches | |
def prompt_with_retry(prompt, parser): | |
while True: | |
try: | |
return parser(input(prompt)) | |
except Exception as e: | |
print(f"Invalid input: {e}") | |
def main(): | |
# The PDF incorrectly calculates the area of a bay as 10.6 square feet | |
# it's actually 9.66 square feet. If we convert that to cube inches | |
# and divide the weight of wool they specify by 5.6lb we get | |
# lb_wool_per_cubic_inch = 0.0007315 | |
# The PDF also gives a figure of 0.53lb per sqft at 5.5" depth which gives | |
# the figure below. I believe this is what Havelock intended to use | |
# as the recommended density. | |
lb_wool_per_cubic_inch = 0.00066919 | |
# Feel free to change this if your joists are beefier. | |
joist_width = 1.5 | |
print("π π Roof Bay Area Havelock wool weight calculator ππ ") | |
oc_spacing = prompt_with_retry( | |
"Joist spacing on centre (in inches): ", | |
lambda text: parse_length(text, 'inches') | |
) | |
while True: | |
ridge_length = prompt_with_retry( | |
"Ridge length, excluding facias (<feet>'[<inches>\"]): ", | |
lambda text: parse_length(text, 'feet') | |
) | |
bay_length = prompt_with_retry( | |
"Longest side of longest bay (<feet>'[<inches>\"]): ", | |
lambda text: parse_length(text, 'feet') | |
) | |
bay_depth = prompt_with_retry( | |
"Bay depth (in inches): ", | |
lambda text: parse_length(text, 'inches') | |
) | |
facet_shape = prompt_with_retry( | |
"Facet shape (triangle|trapezoid|rectangle): ", | |
lambda text: { | |
"triangle": "triangle", | |
"trapezoid": "trapezoid", | |
"rectangle": "rectangle" | |
}[text.strip().lower()] | |
) | |
if facet_shape == "rectangle": | |
# Subtract the half joist at the beginning and half joist at the end | |
bays = rectangle_bay_areas(bay_length, ridge_length - joist_width, joist_width, oc_spacing) | |
elif facet_shape == "triangle": | |
# Subtract the half joist at the beginning | |
bays = triangle_bay_areas(bay_length, ridge_length - (joist_width / 2), joist_width, oc_spacing) | |
elif facet_shape == "trapezoid": | |
eave_length = prompt_with_retry( | |
"Eave length (<feet>'[<inches>\"]): ", | |
lambda text: parse_length(text, 'feet') | |
) | |
bays = symmetrical_trapezoid_bay_areas(bay_length, ridge_length, eave_length, joist_width, oc_spacing) | |
print(f"\nTotal number of bays: {len(bays)} (including partial)") | |
for i, bay in enumerate(bays, start=1): | |
sq_ft = bay["area"] / 144 | |
width = bay["width"] | |
cubic_inches = bay["area"] * bay_depth | |
lb_wool = cubic_inches * lb_wool_per_cubic_inch | |
print(f"Bay {i}: {width:.2f}\", {sq_ft:.2f} sq', {lb_wool:.2f} lbs of wool") | |
print("") | |
if __name__ == "__main__": | |
try: | |
main() | |
except KeyboardInterrupt: | |
exit |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment