This commit is contained in:
2025-05-08 08:29:11 +02:00
parent 03464ffafd
commit 5dd8869caf
3 changed files with 194 additions and 192 deletions

View File

@@ -20,32 +20,43 @@
# * *
# ***********************************************************************
import os
import platform
import subprocess
from typing import Dict, List
import FreeCAD
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import openpyxl
from openpyxl.styles import Alignment, Border, Side, PatternFill, Font
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
import PVPlantResources
import PVPlantSite
if FreeCAD.GuiUp:
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "PVPlant Mechanical BOQ"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
# Constants
SCALE = 0.001 # Millimeters to meters
THIN_BORDER = Border(
top=Side(border_style="thin", color="7DA4B8"),
left=Side(border_style="thin", color="7DA4B8"),
right=Side(border_style="thin", color="7DA4B8"),
bottom=Side(border_style="thin", color="7DA4B8")
)
HEADER_FILL = PatternFill("solid", fgColor="7DA4B8")
HEADER_FONT = Font(name='Arial', size=10, bold=True, color="FFFFFF")
DATA_FONT = Font(name='Arial', size=10)
CENTER_ALIGN = Alignment(horizontal="center", vertical="center")
# Estilos:
thin = Side(border_style="thin", color="7DA4B8")
double = Side(border_style="double", color="ff0000")
@@ -56,6 +67,16 @@ border_fat = Border(top=thin, left=thin, right=thin, bottom=thin)
scale = 0.001 # milimeters to meter
def open_file(path: str) -> None:
"""Open a file or directory using the system's default handler"""
if platform.system() == "Windows":
os.startfile(path)
elif platform.system() == "Darwin":
subprocess.Popen(["open", path])
else:
subprocess.Popen(["xdg-open", path])
def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment=None):
"""
Apply styles to a range of cells as if they were a single cell.
@@ -94,124 +115,119 @@ def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment
for c in row:
c.fill = fill
def spreadsheetBOQFrames(sheet, sel):
sheet['A1'] = 'Index'
sheet['B1'] = 'Frame'
sheet['C1'] = 'Frame Type'
sheet['D1'] = 'X'
sheet['E1'] = 'Y'
sheet['F1'] = 'Z'
sheet['G1'] = 'Angle N-S'
sheet['H1'] = 'Angle L-W'
sheet['I1'] = 'Nº Poles'
sheet.column_dimensions['A'].width = 8
sheet.column_dimensions['B'].width = 30
sheet.column_dimensions['C'].width = 20
sheet.column_dimensions['D'].width = 20
sheet.column_dimensions['E'].width = 20
sheet.column_dimensions['F'].width = 20
sheet.column_dimensions['G'].width = 15
sheet.column_dimensions['H'].width = 15
sheet.column_dimensions['I'].width = 15
sheet.row_dimensions[1].height = 40
def create_sheet_headers(sheet: Worksheet, headers: List[str], widths: Dict[str, float]) -> None:
"""Create standardized sheet headers."""
for col, header in enumerate(headers, start=1):
cell = sheet.cell(row=1, column=col, value=header)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = CENTER_ALIGN
style_range(sheet, 'A1:I1',
border=Border(top=thin, left=thin, right=thin, bottom=thin),
fill=PatternFill("solid", fgColor="7DA4B8"),
font=Font(name='Quicksand', size=10, b=True, color="FFFFFF"),
alignment=Alignment(horizontal="center", vertical="center"))
for col, width in widths.items():
sheet.column_dimensions[col].width = width
for ind in range(0, len(sel)):
row = ind + 2
sheet['A{0}'.format(row)] = ind + 1
sheet['B{0}'.format(row)] = sel[ind].Label
sheet['C{0}'.format(row)] = sel[ind].Setup.Label
sheet['D{0}'.format(row)] = sel[ind].Placement.Base.x * scale
sheet['E{0}'.format(row)] = sel[ind].Placement.Base.y * scale
sheet['R{0}'.format(row)] = sel[ind].Placement.Base.z * scale
sheet['G{0}'.format(row)] = sel[ind].Placement.Rotation.toEuler()[0]
sheet['H{0}'.format(row)] = sel[ind].Placement.Rotation.toEuler()[1]
sheet['I{0}'.format(row)] = sel[ind].Setup.NumberPole.Value
style_range(sheet, 'A' + str(row) + ':I' + str(row),
border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=10),
alignment=Alignment(horizontal="center", vertical="center"))
sheet.row_dimensions[1].height = 30
style_range(sheet, f'A1:{chr(64 + len(headers))}1',
border=THIN_BORDER,
fill=HEADER_FILL,
font=HEADER_FONT,
alignment=CENTER_ALIGN)
def spreadsheetBOQPoles(sheet, sel):
import MeshPart as mp
from Mechanical.Frame import PVPlantFrame
# Data:
terrain = PVPlantSite.get().Terrain.Mesh # Shape
def spreadsheetBOQFrames(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None:
"""Generate frames information sheet."""
headers = [
'Index', 'Frame', 'Frame Type', 'X (m)', 'Y (m)', 'Z (m)',
'Angle N-S', 'Angle L-W', 'Nº Poles'
]
widths = {'A': 8, 'B': 30, 'C': 20, 'D': 15, 'E': 15,
'F': 15, 'G': 15, 'H': 15, 'I': 10}
# Headers:
sheet['A1'] = 'Frame'
sheet['B1'] = 'Pole'
sheet['C1'] = 'Pole Type'
sheet['D1'] = 'X'
sheet['E1'] = 'Y'
sheet['F1'] = 'Z frame attach'
sheet['G1'] = 'Z aerial head'
sheet['H1'] = 'Pole length'
sheet['I1'] = 'Pole aerial length'
sheet['J1'] = 'Pole terrain enter length'
create_sheet_headers(sheet, headers, widths)
sheet.column_dimensions['A'].width = 30
sheet.column_dimensions['B'].width = 8
sheet.column_dimensions['C'].width = 20
sheet.column_dimensions['D'].width = 20
sheet.column_dimensions['E'].width = 20
sheet.column_dimensions['F'].width = 20
sheet.column_dimensions['G'].width = 20
sheet.column_dimensions['H'].width = 20
sheet.column_dimensions['I'].width = 20
sheet.column_dimensions['J'].width = 20
sheet.row_dimensions[1].height = 40
style_range(sheet, 'A1:J1',
border=Border(top=thin, left=thin, right=thin, bottom=thin),
fill=PatternFill("solid", fgColor="7DA4B8"),
font=Font(name='Quicksand', size=11, b=True, color="FFFFFF"),
alignment=Alignment(horizontal="center", vertical="center"))
sheet['A2'] = ""
for idx, obj in enumerate(selection, start=2):
placement = obj.Placement
sheet[f'A{idx}'] = idx - 1
sheet[f'B{idx}'] = obj.Label
sheet[f'C{idx}'] = obj.Setup.Label
sheet[f'D{idx}'] = placement.Base.x * SCALE
sheet[f'E{idx}'] = placement.Base.y * SCALE
sheet[f'F{idx}'] = placement.Base.z * SCALE
sheet[f'G{idx}'] = placement.Rotation.toEuler()[0]
sheet[f'H{idx}'] = placement.Rotation.toEuler()[1]
sheet[f'I{idx}'] = obj.Setup.NumberPole.Value
style_range(sheet, f'A{idx}:I{idx}',
border=THIN_BORDER,
font=DATA_FONT,
alignment=CENTER_ALIGN)
def spreadsheetBOQPoles(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None:
"""Generate poles information sheet."""
headers = [
'Frame', 'Pole', 'Pole Type', 'X (m)', 'Y (m)', 'Z Frame Attach (m)',
'Z Aerial Head (m)', 'Pole Length (m)', 'Aerial Length (m)',
'Terrain Enter Length (m)'
]
widths = {chr(65 + i): 20 for i in range(10)}
widths['A'] = 30
widths['B'] = 10
create_sheet_headers(sheet, headers, widths)
sheet.row_dimensions[2].height = 5
data = {"Frame": [],
#"FrameType": [],
"Pole": [],
"PoleType": [],
"PoleLength": [],
"Center": [],
"Head": []}
cnt = 0
for frame_ind, frame in enumerate(sel):
poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes
numpoles = int(frame.Setup.NumberPole.Value)
seq = frame.Setup.PoleSequence
if len(seq) < numpoles:
seq = PVPlantFrame.getarray(frame.Setup.PoleSequence, numpoles)
for pole_ind in range(numpoles):
pole = poles[pole_ind]
poletype = frame.Setup.PoleType[seq[pole_ind]]
data["Frame"].append(frame.Label)
#data["FrameType"].append(frame.Setup.Label)
data["Pole"].append(pole_ind + 1)
data["PoleType"].append(poletype.Label)
data["PoleLength"].append(int(poletype.Height))
data["Center"].append(pole.BoundBox.Center)
data["Head"].append(pole.BoundBox.ZMax)
cnt += 1
import MeshPart as mp
from Mechanical.Frame import PVPlantFrame
# Data:
terrain = PVPlantSite.get().Terrain.Mesh # Shape
poles_data = []
pts = mp.projectPointsOnMesh(data["Center"], terrain, FreeCAD.Vector(0, 0, 1))
#if cnt == len(pts):
data["Soil"] = pts
for frame in selection:
try:
poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes
num_poles = int(frame.Setup.NumberPole.Value)
sequence = frame.Setup.PoleSequence
if len(sequence) < num_poles:
sequence = PVPlantFrame.getarray(frame.Setup.PoleSequence, num_poles)
for pole_idx in range(num_poles):
pole = poles[pole_idx]
pole_type = frame.Setup.PoleType[sequence[pole_idx]]
center = pole.BoundBox.Center
poles_data.append({
'frame': frame.Label,
'number': pole_idx + 1,
'type': pole_type.Label,
'center': center,
'head_z': pole.BoundBox.ZMax,
'length': pole_type.Height.Value
})
except Exception as e:
FreeCAD.Console.PrintError(f"Error processing frame {frame.Label}: {str(e)}\n")
# Project points on terrain
try:
import MeshPart
points = [data['center'] for data in poles_data]
terrain_z = MeshPart.projectPointsOnMesh(points, terrain, FreeCAD.Vector(0, 0, 1))
for data, z in zip(poles_data, terrain_z):
data['terrain_z'] = z.z
except Exception as e:
FreeCAD.Console.PrintError(f"Terrain projection failed: {str(e)}\n")
for data in poles_data:
data['terrain_z'] = 0
# Write data to sheet
row = 3
group_from = row
f = data["Frame"][0]
for i in range(0, len(data["Frame"])):
if f != data["Frame"][i]:
print(poles_data[0])
f = poles_data[0]['frame']
for i, data in enumerate(poles_data):
if f != data["frame"]:
style_range(sheet, 'A' + str(group_from) + ':F' + str(row - 1),
border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=11, ),
@@ -221,27 +237,27 @@ def spreadsheetBOQPoles(sheet, sel):
border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=11, ),
alignment=Alignment(horizontal="center", vertical="center"))
#sheet['A{0}'.format(row)] = ""
# sheet['A{0}'.format(row)] = ""
sheet.row_dimensions[row].height = 5
row += 1
f = data["Frame"][i]
f = data["frame"]
group_from = row
sheet['A{0}'.format(row)] = data['Frame'][i]
sheet['B{0}'.format(row)] = data['Pole'][i]
sheet['C{0}'.format(row)] = data['PoleType'][i]
sheet['D{0}'.format(row)] = round(data['Center'][i].x, 0) * scale
sheet['A{0}'.format(row)] = data['frame']
sheet['B{0}'.format(row)] = data['number']
sheet['C{0}'.format(row)] = data['type']
sheet['D{0}'.format(row)] = round(data['center'].x, 0) * scale
sheet['D{0}'.format(row)].number_format = "0.000"
sheet['E{0}'.format(row)] = round(data['Center'][i].y, 0) * scale
sheet['E{0}'.format(row)] = round(data['center'].y, 0) * scale
sheet['E{0}'.format(row)].number_format = "0.000"
try:
sheet['F{0}'.format(row)] = round(data['Soil'][i].z, 0) * scale
sheet['F{0}'.format(row)] = round(data['terrain_z'].z, 0) * scale
sheet['F{0}'.format(row)].number_format = "0.000"
except:
pass
sheet['G{0}'.format(row)] = round(data['Head'][i]) * scale
sheet['G{0}'.format(row)] = round(data['head_z']) * scale
sheet['G{0}'.format(row)].number_format = "0.000"
sheet['H{0}'.format(row)] = data["PoleLength"][i] * scale
sheet['H{0}'.format(row)] = data["length"] * scale
sheet['H{0}'.format(row)].number_format = "0.000"
sheet['I{0}'.format(row)] = '=G{0}-F{0}'.format(row)
sheet['I{0}'.format(row)].number_format = "0.000"
@@ -250,7 +266,7 @@ def spreadsheetBOQPoles(sheet, sel):
style_range(sheet, 'A' + str(row) + ':J' + str(row),
border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=11,),
font=Font(name='Quicksand', size=11, ),
alignment=Alignment(horizontal="center", vertical="center"))
row += 1
@@ -302,37 +318,39 @@ class CommandBOQMechanical:
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Calcular el BOQ de la")}
def Activated(self):
# make file global:
#sel = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
sel = []
for obj in FreeCAD.ActiveDocument.Objects:
'''if not hasattr(obj, "Proxy"):
continue
if issubclass(obj.Proxy.__class__, PVPlantRack.Frame):
objects.append(obj)'''
if obj.Name.startswith("Tracker") and not obj.Name.startswith("TrackerSetup"):
sel.append(obj)
sel = sorted(sel, key=lambda x: x.Label)
if len(sel) > 0:
path = os.path.dirname(FreeCAD.ActiveDocument.FileName)
filename = os.path.join(path, "BOQMechanical.xlsx")
mywb = openpyxl.Workbook()
sheet = mywb.active
sheet.title = 'Frames information'
spreadsheetBOQFrames(sheet, sel)
doc = FreeCAD.ActiveDocument
sheet = mywb.create_sheet("Poles information")
spreadsheetBOQPoles(sheet, sel)
mywb.save(filename)
if not doc or not doc.FileName:
FreeCAD.Console.PrintError("Document must be saved first\n")
return
print("Se ha generado el BOQMechanical: ")
print(filename)
'''import sys
path = r'C:\Program Files (x86)\IronPython 2.7\Lib'
sys.path.append(path)'''
try:
sel = [obj for obj in FreeCAD.ActiveDocument.Objects if (hasattr(obj, "Proxy") and (obj.Proxy.Type == "Tracker"))]
import subprocess
subprocess.Popen('explorer ' + path)
if not sel:
FreeCAD.Console.PrintWarning("No valid trackers found\n")
return
path = os.path.dirname(doc.FileName)
filename = os.path.join(path, "BOQ_Mechanical.xlsx")
sel = sorted(sel, key=lambda x: x.Label)
wb = openpyxl.Workbook()
wb.remove(wb.active) # Remove default sheet
frames_sheet = wb.create_sheet("Frames Information")
spreadsheetBOQFrames(frames_sheet, sel)
poles_sheet = wb.create_sheet("Poles information")
spreadsheetBOQPoles(poles_sheet , sel)
wb.save(filename)
FreeCAD.Console.PrintMessage(f"Report generated: {filename}\n")
open_file(path)
except Exception as e:
FreeCAD.Console.PrintError(f"Error generating BOQ: {str(e)}\n")
def IsActive(self):
if FreeCAD.ActiveDocument:
@@ -340,5 +358,3 @@ class CommandBOQMechanical:
else:
return False
'''if FreeCAD.GuiUp:
FreeCADGui.addCommand('BOQMechanical', CommandBOQMechanical())'''

View File

@@ -20,47 +20,32 @@
# * *
# ***********************************************************************
import FreeCAD, Draft
import PVPlantSite
import copy
import FreeCAD
import Draft
import os
import platform
import subprocess
import openpyxl
from openpyxl.styles import Alignment, Border, Side, Font
from PVPlantPlacement import getCols
if FreeCAD.GuiUp:
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
import Part
import pivy
from pivy import coin
import os
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
__title__ = "PVPlant Trench"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
from PVPlantResources import DirIcons as DirIcons
from PVPlantResources import DirDocuments as DirDocuments
'''import os
import platform
import subprocess
def open_file(path):
"""Open a file or directory using the default system handler"""
if platform.system() == "Windows":
os.startfile(path)
elif platform.system() == "Darwin":
subprocess.Popen(["open", path])
else:
subprocess.Popen(["xdg-open", path])'''
subprocess.Popen(["xdg-open", path])
from PVPlantPlacement import getCols

View File

@@ -20,4 +20,5 @@ pyproj~=3.7.1
simplekml~=1.3.6
geojson~=3.1.0
certifi~=2023.11.17
SciPy~=1.11.4
SciPy~=1.11.4
ezdxf~=1.4.1