259 lines
9.0 KiB
Python
259 lines
9.0 KiB
Python
import json
|
|
import math
|
|
from dataclasses import dataclass
|
|
from typing import List, Tuple, Dict
|
|
import webbrowser
|
|
import os
|
|
|
|
@dataclass
|
|
class Window:
|
|
left: float # position from left edge
|
|
bottom: float # position from bottom edge
|
|
width: float
|
|
height: float
|
|
|
|
@dataclass
|
|
class Dimensions:
|
|
name: str
|
|
width: float
|
|
height: float
|
|
shape: str = "rectangular"
|
|
windows: List[Window] = None
|
|
heightFirstRow: float = None
|
|
|
|
def __post_init__(self):
|
|
if self.windows is None:
|
|
self.windows = []
|
|
|
|
@dataclass
|
|
class Board:
|
|
x: float # x position
|
|
y: float # y position
|
|
width: float
|
|
height: float
|
|
is_cut: bool
|
|
|
|
@dataclass
|
|
class BoardUsage:
|
|
full_boards: int
|
|
cut_boards: List[float] # List of cut board lengths in meters
|
|
last_row_height: float # Height of the last row in meters
|
|
waste: float # Total waste in square meters
|
|
board_positions: List[Board] # List of all boards with their positions
|
|
|
|
@dataclass
|
|
class WallResult:
|
|
wall: Dimensions
|
|
usage: BoardUsage
|
|
|
|
def load_config(filename: str) -> Tuple[List[Dimensions], Dimensions]:
|
|
"""Load walls and board dimensions from config file."""
|
|
with open(filename, 'r') as f:
|
|
config = json.load(f)
|
|
|
|
walls = []
|
|
for wall_config in config['walls']:
|
|
windows = []
|
|
if 'windows' in wall_config:
|
|
for w in wall_config['windows']:
|
|
windows.append(Window(
|
|
left=w['left'],
|
|
bottom=w['bottom'],
|
|
width=w['width'],
|
|
height=w['height']
|
|
))
|
|
|
|
wall = Dimensions(
|
|
name=wall_config['name'],
|
|
width=wall_config['width'],
|
|
height=wall_config['height'],
|
|
shape=wall_config.get('shape', 'rectangular'),
|
|
windows=windows,
|
|
heightFirstRow=wall_config.get('heightFirstRow', None)
|
|
)
|
|
walls.append(wall)
|
|
|
|
board = Dimensions(**config['board'])
|
|
return walls, board
|
|
|
|
def get_row_width(y: float, wall: Dimensions) -> float:
|
|
"""Calculate the width of the wall at a given height."""
|
|
if wall.shape == "rectangular":
|
|
return wall.width
|
|
elif wall.shape == "triangular":
|
|
# For equilateral triangle, calculate width at given height
|
|
# At y=0 (bottom), width is wall.width
|
|
# At y=wall.height (top), width is 0
|
|
return wall.width * (1 - y/wall.height)
|
|
return 0
|
|
|
|
def get_row_offset(y: float, wall: Dimensions) -> float:
|
|
"""Calculate the x-offset for a row at given height in triangular walls."""
|
|
if wall.shape != "triangular":
|
|
return 0
|
|
# For triangular shape, calculate offset based on similar triangles
|
|
# At y=0 (bottom), offset is 0
|
|
# At y=wall.height (top), offset is wall.width/2
|
|
return (y / wall.height) * (wall.width / 2)
|
|
|
|
def calculate_board_usage(wall: Dimensions, board: Dimensions) -> BoardUsage:
|
|
"""Calculate how many boards are needed to cover the wall."""
|
|
# Calculate the height of the first row if specified
|
|
first_row_height = wall.heightFirstRow if wall.heightFirstRow is not None else board.height
|
|
|
|
# Calculate remaining height after first row
|
|
remaining_height = wall.height - first_row_height
|
|
|
|
# Calculate number of full-height rows needed for remaining height
|
|
full_rows = math.floor(remaining_height / board.height)
|
|
|
|
# Calculate the height of the last row
|
|
last_row_height = remaining_height - (full_rows * board.height)
|
|
if last_row_height < 0.01: # If less than 1cm remaining, ignore it
|
|
last_row_height = 0
|
|
full_rows = math.floor(remaining_height / board.height)
|
|
|
|
rows = 1 + full_rows + (1 if last_row_height > 0 else 0) # First row + full rows + last row if needed
|
|
|
|
full_boards = 0
|
|
cut_boards = []
|
|
waste = 0
|
|
board_positions = []
|
|
|
|
# Process each row
|
|
for row in range(rows):
|
|
# Calculate y position from bottom
|
|
if row == 0:
|
|
current_y = 0
|
|
current_row_height = first_row_height
|
|
else:
|
|
current_y = first_row_height + ((row - 1) * board.height)
|
|
current_row_height = last_row_height if row == rows - 1 else board.height
|
|
|
|
# Calculate the width of the wall at this height
|
|
row_width = get_row_width(current_y, wall)
|
|
next_row_width = get_row_width(current_y + current_row_height, wall)
|
|
|
|
# For triangular shape, we need to handle trapezoidal sections
|
|
if wall.shape == "triangular":
|
|
# Use the wider base for board calculations to ensure coverage
|
|
row_width = max(row_width, next_row_width)
|
|
|
|
# Calculate x offset for triangular walls
|
|
x_offset = get_row_offset(current_y, wall)
|
|
current_x = x_offset
|
|
remaining_width = row_width
|
|
|
|
while remaining_width > 0:
|
|
if remaining_width >= board.width:
|
|
full_boards += 1
|
|
board_positions.append(Board(
|
|
x=current_x,
|
|
y=current_y,
|
|
width=board.width,
|
|
height=current_row_height,
|
|
is_cut=current_row_height != board.height
|
|
))
|
|
current_x += board.width
|
|
remaining_width -= board.width
|
|
# Calculate waste if board height is cut
|
|
if current_row_height != board.height:
|
|
waste += (board.width * (board.height - current_row_height))
|
|
else:
|
|
if remaining_width > 0:
|
|
cut_boards.append(remaining_width)
|
|
board_positions.append(Board(
|
|
x=current_x,
|
|
y=current_y,
|
|
width=remaining_width,
|
|
height=current_row_height,
|
|
is_cut=True
|
|
))
|
|
# Calculate waste considering both width and height cuts
|
|
width_waste = board.width - remaining_width
|
|
height_waste = board.height - current_row_height if current_row_height != board.height else 0
|
|
waste += (width_waste * current_row_height) + (remaining_width * height_waste)
|
|
remaining_width = 0
|
|
|
|
return BoardUsage(full_boards, cut_boards, last_row_height, waste, board_positions)
|
|
|
|
def generate_visualization_data(wall_results: List[WallResult], board: Dimensions) -> List[Dict]:
|
|
"""Generate data for the visualization of multiple walls."""
|
|
return [{
|
|
"wall": {
|
|
"name": result.wall.name,
|
|
"width": result.wall.width,
|
|
"height": result.wall.height,
|
|
"shape": result.wall.shape,
|
|
"windows": [
|
|
{
|
|
"left": w.left,
|
|
"bottom": w.bottom,
|
|
"width": w.width,
|
|
"height": w.height
|
|
}
|
|
for w in result.wall.windows
|
|
]
|
|
},
|
|
"board": {
|
|
"width": board.width,
|
|
"height": board.height
|
|
},
|
|
"boards": [
|
|
{
|
|
"x": b.x,
|
|
"y": b.y,
|
|
"width": b.width,
|
|
"height": b.height,
|
|
"is_cut": b.is_cut
|
|
}
|
|
for b in result.usage.board_positions
|
|
]
|
|
} for result in wall_results]
|
|
|
|
def main():
|
|
# Load configuration
|
|
walls, board = load_config('config.json')
|
|
|
|
# Calculate board usage for each wall
|
|
wall_results = []
|
|
total_full_boards = 0
|
|
total_cut_boards = 0
|
|
total_waste = 0
|
|
|
|
for wall in walls:
|
|
usage = calculate_board_usage(wall, board)
|
|
wall_results.append(WallResult(wall, usage))
|
|
|
|
total_full_boards += usage.full_boards
|
|
total_cut_boards += len(usage.cut_boards)
|
|
total_waste += usage.waste
|
|
|
|
# Print results for each wall
|
|
for result in wall_results:
|
|
print(f"\n{result.wall.name}:")
|
|
print(f"Wall dimensions: {result.wall.width}m x {result.wall.height}m")
|
|
print(f"Wall shape: {result.wall.shape}")
|
|
print(f"Full boards needed: {result.usage.full_boards}")
|
|
print(f"Cut boards needed: {len(result.usage.cut_boards)}")
|
|
print("Cut board lengths:", [f"{length:.2f}m" for length in result.usage.cut_boards])
|
|
print(f"Last row height: {result.usage.last_row_height:.2f}m")
|
|
print(f"Waste: {result.usage.waste:.2f} square meters")
|
|
|
|
# Print total summary
|
|
print("\nTotal Summary:")
|
|
print(f"Total full boards needed: {total_full_boards}")
|
|
print(f"Total cut boards needed: {total_cut_boards}")
|
|
print(f"Total waste: {total_waste:.2f} square meters")
|
|
print(f"Total boards needed: {total_full_boards + total_cut_boards}")
|
|
|
|
# Generate visualization data
|
|
viz_data = generate_visualization_data(wall_results, board)
|
|
with open('visualization_data.js', 'w') as f:
|
|
f.write(f"const wallsData = {json.dumps(viz_data, indent=2)};")
|
|
|
|
# Open visualization in browser
|
|
webbrowser.open('file://' + os.path.abspath('wall_visualization.html'))
|
|
|
|
if __name__ == "__main__":
|
|
main() |