UsmanGoraya commited on
Commit
ac4e80c
·
verified ·
1 Parent(s): f0c2150

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -86
app.py CHANGED
@@ -3,10 +3,11 @@ import ezdxf
3
  import io
4
  import pandas as pd
5
  import matplotlib.pyplot as plt
 
6
  from fpdf import FPDF
7
  from tempfile import NamedTemporaryFile
8
- import matplotlib.patches as patches
9
  import tempfile
 
10
 
11
  # Constants
12
  BRICK_VOLUME_CFT = (9/12) * (4.5/12) * (3/12)
@@ -14,7 +15,7 @@ CEMENT_SAND_RATIO = 1 / 6
14
  SAND_RATIO = 5 / 6
15
  CEMENT_DENSITY_KG_PER_CFT = 1440 / 35.3147
16
  CEMENT_BAG_WEIGHT_KG = 50
17
- DEFAULT_WALL_THICKNESS = 0.75
18
 
19
  st.set_page_config(page_title="Building Estimator from CAD", layout="wide")
20
  st.title("🏗️ Auto Estimation from AutoCAD (.dxf) Drawing")
@@ -23,55 +24,82 @@ uploaded_file = st.file_uploader("Upload your DXF file", type=["dxf"])
23
 
24
  @st.cache_data
25
  def extract_geometry(file_bytes):
26
- try:
27
- text_buffer = io.TextIOWrapper(file_bytes, encoding='utf-8', errors='ignore')
28
- doc = ezdxf.read(text_buffer)
29
- except Exception as e:
30
- st.error(f"Error reading DXF file: {e}")
31
- return [], 0.75, [], 1, []
32
-
33
  msp = doc.modelspace()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  rooms = []
35
  room_shapes = []
36
- openings = []
37
- floors = 1
38
- wall_thickness = DEFAULT_WALL_THICKNESS
39
-
40
- for entity in msp:
41
- if entity.dxftype() == "TEXT":
42
- content = entity.dxf.text.lower()
43
- if "wall thickness" in content:
44
- try:
45
- wall_thickness = float(content.split(":")[1].strip())
46
- except:
47
- continue
48
- elif "floor" in content:
49
- try:
50
- floors = int(content.split(":")[1].strip())
51
- except:
52
- continue
53
-
54
- elif entity.dxftype() == "LWPOLYLINE":
55
- if entity.closed and len(entity) == 4:
56
- points = entity.get_points()
57
- x_vals = [p[0] for p in points]
58
- y_vals = [p[1] for p in points]
59
- length = abs(max(x_vals) - min(x_vals)) / 12
60
- width = abs(max(y_vals) - min(y_vals)) / 12
61
- height = 10
62
- if length > 2 and width > 2:
63
- rooms.append((length, width, height))
64
- room_shapes.append((min(x_vals), min(y_vals), max(x_vals), max(y_vals)))
65
- else:
66
- openings.append(("opening", length, height))
67
-
68
- return rooms, wall_thickness, openings, floors, room_shapes
69
-
70
- def estimate(rooms, wall_thickness, openings, floors):
 
 
71
  wall_volume = sum(2 * (l + w) * h * wall_thickness for l, w, h in rooms)
72
- opening_volume = sum(l * h * wall_thickness for _, l, h in openings)
73
- beam_volume = sum((l + 1) * 0.75 * 0.75 for _, l, _ in openings)
74
- net_volume = (wall_volume - opening_volume - beam_volume) * floors
75
 
76
  number_of_bricks = round((net_volume / BRICK_VOLUME_CFT) * 1.05)
77
  mortar_volume = net_volume * 0.25
@@ -108,41 +136,49 @@ def draw_plan_image(room_shapes):
108
  plt.close(fig)
109
  return temp_file.name
110
 
111
- def generate_pdf(data_dict, calc_details, room_shapes):
112
  image_path = draw_plan_image(room_shapes)
113
-
114
  pdf = FPDF()
115
  pdf.add_page()
116
- pdf.set_font("Arial", 'B', size=14)
117
  pdf.cell(200, 10, "Estimation Report", ln=True, align='C')
118
  pdf.ln(5)
119
 
120
- pdf.set_font("Arial", 'B', size=12)
121
  pdf.cell(200, 10, "Summary of Quantities", ln=True)
122
- pdf.set_font("Arial", '', size=11)
123
  for key, value in data_dict.items():
124
  pdf.cell(200, 10, f"{key}: {value}", ln=True)
125
 
126
- pdf.ln(8)
127
- pdf.set_font("Arial", 'B', size=12)
128
- pdf.cell(200, 10, "Step-by-Step Calculations with Formulas", ln=True)
 
 
129
 
130
- pdf.set_font("Arial", '', size=10)
 
 
 
 
 
 
 
 
 
131
  pdf.multi_cell(0, 8, f"""
132
  1. Wall Volume = 2 × (L + W) × H × t = {round(calc_details['Wall Volume (cft)'], 2)} cft
133
  2. Opening Volume = L × H × t = {round(calc_details['Opening Volume (cft)'], 2)} cft
134
  3. Beam Volume = (L+1) × 0.75 × 0.75 = {round(calc_details['Beam Volume (cft)'], 2)} cft
135
- 4. Net Volume = (Wall - Opening - Beam) × Floors = {round(calc_details['Net Volume (cft)'], 2)} cft
136
- 5. Brick Volume = 9\" × 4.5\" × 3\" = {round(calc_details['Brick Volume (cft)'], 4)} cft
137
  6. Bricks = Net Volume / Brick Vol × 1.05 = {data_dict['Bricks Required']}
138
  7. Mortar = Net Volume × 0.25 = {round(calc_details['Mortar Volume (cft)'], 2)} cft
139
  8. Cement = Mortar × 1/6 = {round(calc_details['Cement Volume (cft)'], 2)} cft
140
  9. Sand = Mortar × 5/6 = {round(calc_details['Sand Volume (cft)'], 2)} cft
141
  10. Cement Bags = Cement / Bag Volume = {data_dict['Cement Bags']}
142
  """)
143
-
144
  pdf.ln(5)
145
- pdf.set_font("Arial", 'B', size=12)
146
  pdf.cell(200, 10, "2D Plan with Dimensions (in ft)", ln=True)
147
  pdf.image(image_path, x=10, y=None, w=180)
148
 
@@ -150,50 +186,41 @@ def generate_pdf(data_dict, calc_details, room_shapes):
150
  pdf.output(tmp.name)
151
  return tmp.name
152
 
153
- def plot_rooms(shapes):
154
- fig, ax = plt.subplots()
155
- for x0, y0, x1, y1 in shapes:
156
- width = (x1 - x0)
157
- height = (y1 - y0)
158
- rect = plt.Rectangle((x0, y0), width, height, fill=False, edgecolor='blue', linewidth=2)
159
- ax.add_patch(rect)
160
- ax.set_title("🗏️ 2D Floor Plan")
161
- ax.set_aspect("equal")
162
- ax.axis("off")
163
- st.pyplot(fig)
164
-
165
  if uploaded_file:
166
  file_bytes = io.BytesIO(uploaded_file.read())
167
- rooms, wall_thickness, openings, floors, room_shapes = extract_geometry(file_bytes)
168
 
169
- st.success(f"✔️ Parsed {len(rooms)} rooms and {len(openings)} openings across {floors} floor(s).")
170
- st.write(f"📏 Wall Thickness: {wall_thickness} ft")
171
 
172
- bricks, sand, cement, calc_details = estimate(rooms, wall_thickness, openings, floors)
173
 
174
- st.subheader("📊 Room Dimensions")
175
- df_rooms = pd.DataFrame(rooms, columns=["Length (ft)", "Width (ft)", "Height (ft)"])
176
- st.dataframe(df_rooms)
177
 
178
- st.subheader("🧱 Estimation Result")
 
 
 
 
 
 
179
  df_result = pd.DataFrame({
180
  "Item": ["Bricks", "Sand (cft)", "Cement (bags)"],
181
  "Quantity": [bricks, round(sand, 2), cement]
182
  })
183
  st.dataframe(df_result)
184
 
185
- st.subheader("🖼️ 2D Floor Plan")
186
- plot_rooms(room_shapes)
187
-
188
  st.subheader("📄 Export")
189
  col1, col2 = st.columns(2)
190
  with col1:
191
  st.download_button("⬇️ Download Excel", df_result.to_csv(index=False).encode(), "estimates.csv", "text/csv")
192
  with col2:
193
- pdf_file = generate_pdf({
194
  "Bricks Required": f"{bricks:,}",
195
  "Sand Volume": f"{sand:.2f} cft",
196
  "Cement Bags": f"{cement} bags"
197
- }, calc_details, room_shapes)
198
- with open(pdf_file, "rb") as f:
199
  st.download_button("⬇️ Download PDF", f.read(), "estimates.pdf", "application/pdf")
 
3
  import io
4
  import pandas as pd
5
  import matplotlib.pyplot as plt
6
+ import matplotlib.patches as patches
7
  from fpdf import FPDF
8
  from tempfile import NamedTemporaryFile
 
9
  import tempfile
10
+ import numpy as np
11
 
12
  # Constants
13
  BRICK_VOLUME_CFT = (9/12) * (4.5/12) * (3/12)
 
15
  SAND_RATIO = 5 / 6
16
  CEMENT_DENSITY_KG_PER_CFT = 1440 / 35.3147
17
  CEMENT_BAG_WEIGHT_KG = 50
18
+ DEFAULT_WALL_HEIGHT = 10 # ft
19
 
20
  st.set_page_config(page_title="Building Estimator from CAD", layout="wide")
21
  st.title("🏗️ Auto Estimation from AutoCAD (.dxf) Drawing")
 
24
 
25
  @st.cache_data
26
  def extract_geometry(file_bytes):
27
+ doc = ezdxf.read(file_bytes)
 
 
 
 
 
 
28
  msp = doc.modelspace()
29
+
30
+ lines = []
31
+ polylines = []
32
+ texts = []
33
+
34
+ for e in msp:
35
+ if e.dxftype() == "LINE":
36
+ lines.append(((e.dxf.start.x, e.dxf.start.y), (e.dxf.end.x, e.dxf.end.y)))
37
+ elif e.dxftype() == "LWPOLYLINE":
38
+ polylines.append(e)
39
+ elif e.dxftype() == "TEXT":
40
+ texts.append(e.dxf.text.lower())
41
+
42
+ wall_segments = []
43
+ used = set()
44
+ tolerance = 0.1 # ft
45
+
46
+ # Group close parallel lines as walls
47
+ for i, (start1, end1) in enumerate(lines):
48
+ for j, (start2, end2) in enumerate(lines):
49
+ if i >= j:
50
+ continue
51
+ if np.allclose([start1[0], end1[0]], [start2[0], end2[0]], atol=tolerance) or \
52
+ np.allclose([start1[1], end1[1]], [start2[1], end2[1]], atol=tolerance):
53
+ dist = np.linalg.norm(np.array(start1) - np.array(start2))
54
+ if 0.6 < dist < 1: # Between 7in to 12in (i.e. wall thickness)
55
+ wall_segments.append((start1, end1, start2, end2))
56
+ used.add(i)
57
+ used.add(j)
58
+
59
+ wall_thicknesses = []
60
  rooms = []
61
  room_shapes = []
62
+ for p in polylines:
63
+ if p.closed:
64
+ pts = p.get_points()
65
+ xs = [pt[0] for pt in pts]
66
+ ys = [pt[1] for pt in pts]
67
+ min_x, max_x = min(xs), max(xs)
68
+ min_y, max_y = min(ys), max(ys)
69
+ l = (max_x - min_x) / 12
70
+ w = (max_y - min_y) / 12
71
+ if l > 1 and w > 1:
72
+ rooms.append((round(l, 2), round(w, 2), DEFAULT_WALL_HEIGHT))
73
+ room_shapes.append((min_x, min_y, max_x, max_y))
74
+
75
+ doors = []
76
+ windows = []
77
+ for p in polylines:
78
+ if p.closed:
79
+ pts = p.get_points()
80
+ xs = [pt[0] for pt in pts]
81
+ ys = [pt[1] for pt in pts]
82
+ min_x, max_x = min(xs), max(xs)
83
+ min_y, max_y = min(ys), max(ys)
84
+ l = (max_x - min_x) / 12
85
+ h = (max_y - min_y) / 12
86
+ if 2 <= l <= 5 and 6 <= h <= 8:
87
+ doors.append((l, h))
88
+ elif 2 <= l <= 6 and 2 <= h <= 5:
89
+ windows.append((l, h))
90
+
91
+ wall_thickness = 0.75 # default
92
+ if wall_segments:
93
+ pt1, _, pt2, _ = wall_segments[0]
94
+ wall_thickness = round(np.linalg.norm(np.array(pt1) - np.array(pt2)) / 12, 2)
95
+
96
+ return rooms, wall_thickness, doors, windows, room_shapes
97
+
98
+ def estimate(rooms, wall_thickness, doors, windows):
99
  wall_volume = sum(2 * (l + w) * h * wall_thickness for l, w, h in rooms)
100
+ opening_volume = sum(l * h * wall_thickness for l, h in doors + windows)
101
+ beam_volume = sum((l + 1) * 0.75 * 0.75 for l, h in doors + windows)
102
+ net_volume = wall_volume - opening_volume - beam_volume
103
 
104
  number_of_bricks = round((net_volume / BRICK_VOLUME_CFT) * 1.05)
105
  mortar_volume = net_volume * 0.25
 
136
  plt.close(fig)
137
  return temp_file.name
138
 
139
+ def generate_pdf(data_dict, calc_details, room_shapes, doors, windows):
140
  image_path = draw_plan_image(room_shapes)
 
141
  pdf = FPDF()
142
  pdf.add_page()
143
+ pdf.set_font("Arial", 'B', 14)
144
  pdf.cell(200, 10, "Estimation Report", ln=True, align='C')
145
  pdf.ln(5)
146
 
147
+ pdf.set_font("Arial", 'B', 12)
148
  pdf.cell(200, 10, "Summary of Quantities", ln=True)
149
+ pdf.set_font("Arial", '', 11)
150
  for key, value in data_dict.items():
151
  pdf.cell(200, 10, f"{key}: {value}", ln=True)
152
 
153
+ pdf.set_font("Arial", 'B', 12)
154
+ pdf.cell(200, 10, "Doors", ln=True)
155
+ pdf.set_font("Arial", '', 11)
156
+ for i, (l, h) in enumerate(doors, 1):
157
+ pdf.cell(200, 10, f"Door {i}: {l} ft x {h} ft", ln=True)
158
 
159
+ pdf.set_font("Arial", 'B', 12)
160
+ pdf.cell(200, 10, "Windows", ln=True)
161
+ pdf.set_font("Arial", '', 11)
162
+ for i, (l, h) in enumerate(windows, 1):
163
+ pdf.cell(200, 10, f"Window {i}: {l} ft x {h} ft", ln=True)
164
+
165
+ pdf.ln(6)
166
+ pdf.set_font("Arial", 'B', 12)
167
+ pdf.cell(200, 10, "Step-by-Step Calculations with Formulas", ln=True)
168
+ pdf.set_font("Arial", '', 10)
169
  pdf.multi_cell(0, 8, f"""
170
  1. Wall Volume = 2 × (L + W) × H × t = {round(calc_details['Wall Volume (cft)'], 2)} cft
171
  2. Opening Volume = L × H × t = {round(calc_details['Opening Volume (cft)'], 2)} cft
172
  3. Beam Volume = (L+1) × 0.75 × 0.75 = {round(calc_details['Beam Volume (cft)'], 2)} cft
173
+ 4. Net Volume = Wall - Opening - Beam = {round(calc_details['Net Volume (cft)'], 2)} cft
174
+ 5. Brick Volume = 9in × 4.5in × 3in = {round(calc_details['Brick Volume (cft)'], 4)} cft
175
  6. Bricks = Net Volume / Brick Vol × 1.05 = {data_dict['Bricks Required']}
176
  7. Mortar = Net Volume × 0.25 = {round(calc_details['Mortar Volume (cft)'], 2)} cft
177
  8. Cement = Mortar × 1/6 = {round(calc_details['Cement Volume (cft)'], 2)} cft
178
  9. Sand = Mortar × 5/6 = {round(calc_details['Sand Volume (cft)'], 2)} cft
179
  10. Cement Bags = Cement / Bag Volume = {data_dict['Cement Bags']}
180
  """)
 
181
  pdf.ln(5)
 
182
  pdf.cell(200, 10, "2D Plan with Dimensions (in ft)", ln=True)
183
  pdf.image(image_path, x=10, y=None, w=180)
184
 
 
186
  pdf.output(tmp.name)
187
  return tmp.name
188
 
189
+ # --- App Main Logic ---
 
 
 
 
 
 
 
 
 
 
 
190
  if uploaded_file:
191
  file_bytes = io.BytesIO(uploaded_file.read())
192
+ rooms, wall_thickness, doors, windows, room_shapes = extract_geometry(file_bytes)
193
 
194
+ st.success(f"✔️ Found {len(rooms)} room(s), {len(doors)} door(s), {len(windows)} window(s)")
195
+ st.write(f"📏 Wall Thickness (from drawing): {wall_thickness:.2f} ft")
196
 
197
+ bricks, sand, cement, calc_details = estimate(rooms, wall_thickness, doors, windows)
198
 
199
+ st.subheader("📐 Room Dimensions")
200
+ st.dataframe(pd.DataFrame(rooms, columns=["Length (ft)", "Width (ft)", "Height (ft)"]))
 
201
 
202
+ st.subheader("🚪 Doors")
203
+ st.dataframe(pd.DataFrame(doors, columns=["Width (ft)", "Height (ft)"]))
204
+
205
+ st.subheader("🪟 Windows")
206
+ st.dataframe(pd.DataFrame(windows, columns=["Width (ft)", "Height (ft)"]))
207
+
208
+ st.subheader("📊 Estimation Results")
209
  df_result = pd.DataFrame({
210
  "Item": ["Bricks", "Sand (cft)", "Cement (bags)"],
211
  "Quantity": [bricks, round(sand, 2), cement]
212
  })
213
  st.dataframe(df_result)
214
 
 
 
 
215
  st.subheader("📄 Export")
216
  col1, col2 = st.columns(2)
217
  with col1:
218
  st.download_button("⬇️ Download Excel", df_result.to_csv(index=False).encode(), "estimates.csv", "text/csv")
219
  with col2:
220
+ pdf_path = generate_pdf({
221
  "Bricks Required": f"{bricks:,}",
222
  "Sand Volume": f"{sand:.2f} cft",
223
  "Cement Bags": f"{cement} bags"
224
+ }, calc_details, room_shapes, doors, windows)
225
+ with open(pdf_path, "rb") as f:
226
  st.download_button("⬇️ Download PDF", f.read(), "estimates.pdf", "application/pdf")