From 98924d44a15e439060bd15e4e100e1c828cf4fd4 Mon Sep 17 00:00:00 2001 From: Youen Date: Fri, 3 Nov 2023 17:43:49 +0100 Subject: [PATCH] =?UTF-8?q?Ajout=20d'un=20script=20pour=20g=C3=A9n=C3=A9re?= =?UTF-8?q?r=20des=20dessins=202D=20pour=20tous=20les=20tubes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/export-all-parts.py | 18 +-- tools/generate-2d-drawings.py | 279 ++++++++++++++++++++++++++++++++++ tools/generate-2d-drawings.sh | 16 ++ tools/utils/vspt_coroutine.py | 38 +++++ tools/utils/vspt_freecad.py | 6 + 5 files changed, 348 insertions(+), 9 deletions(-) create mode 100644 tools/generate-2d-drawings.py create mode 100755 tools/generate-2d-drawings.sh create mode 100644 tools/utils/vspt_coroutine.py create mode 100644 tools/utils/vspt_freecad.py diff --git a/tools/export-all-parts.py b/tools/export-all-parts.py index 1b25d8e..3521202 100644 --- a/tools/export-all-parts.py +++ b/tools/export-all-parts.py @@ -1,5 +1,9 @@ from pathlib import Path import os +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), 'utils')) +import vspt_freecad import FreeCAD as App import Import @@ -9,10 +13,6 @@ project_folder = os.getcwd() output_folder = project_folder + '/dist/STEP' assemblies_output_folder = project_folder + '/dist/FCStd' -def close_all_docs(): - while len(FreeCAD.listDocuments().values()) > 0: - FreeCAD.closeDocument(list(FreeCAD.listDocuments().values())[0].Name) - def convert_file(file_name, output_format): doc = App.open(project_folder + '/' + file_name) @@ -35,7 +35,7 @@ def convert_file(file_name, output_format): if 'experimental' in main_object.Label or 'expérimental' in main_object.Label: print('Document ' + doc.Name + ' is marked as experimental and will be ignored') - close_all_docs() + vspt_freecad.close_all_docs() return secondary_objects = [] @@ -70,7 +70,7 @@ def convert_file(file_name, output_format): else: Import.export([main_object], output_path) - close_all_docs() + vspt_freecad.close_all_docs() def export_assembly(doc, file_name, output_format): root_objects = [] @@ -115,7 +115,7 @@ def convert_assembly(file_name, output_format): print("Exporting assembly " + file_name + "...") doc = App.open(project_folder + '/' + file_name) export_assembly(doc, file_name, output_format) - close_all_docs() + vspt_freecad.close_all_docs() def export_configuration(file_name, config_name, output_filename, output_format): print('Generating assembly for configuration '+config_name+'...') @@ -131,7 +131,7 @@ def export_configuration(file_name, config_name, output_filename, output_format) #doc.saveAs(assemblies_output_folder + '/' + output_filename + '.FCStd') - close_all_docs() + vspt_freecad.close_all_docs() try: folders = [ @@ -158,6 +158,6 @@ except Exception as e: print(e) # exit FreeCAD -close_all_docs() +vspt_freecad.close_all_docs() FreeCADGui.getMainWindow().close() diff --git a/tools/generate-2d-drawings.py b/tools/generate-2d-drawings.py new file mode 100644 index 0000000..9a1550b --- /dev/null +++ b/tools/generate-2d-drawings.py @@ -0,0 +1,279 @@ +import os +import sys +import asyncio +import math + +sys.path.append(os.path.join(os.path.dirname(__file__), 'utils')) +import vspt_freecad +import vspt_coroutine + +verbose = False + +project_folder = os.getcwd() + +async def generate_2d_drawing(file_name): + doc = App.open(project_folder + '/' + file_name) + + page_name = doc.Name + '_Drawing' + + if doc.getObject(page_name) is not None: + print('2D drawing already exists - skipped') + return + + template_file_name = project_folder + '/lib/A4_Landscape_VSPT.svg' + + root_objects = [] + main_object = None + + for obj in doc.Objects: + if len(obj.Parents) == 0: + root_objects.append(obj) + if obj.Label == doc.Name: + main_object = obj + + if main_object is None and len(root_objects) == 1: + main_object = root_objects[0] + + if main_object is None: + raise Exception("Can't find main object in file " + doc.FileName + " (found " + str(len(root_objects)) + " root object(s), none named like the document " + doc.Name + ")") + + code_obj = doc.getObjectsByLabel('Code_Tube_Draft') + if len(code_obj) == 1: + code_obj = code_obj[0] + else: + code_obj = None + + sources = [main_object] + + bound_box = main_object.Shape.BoundBox + proj_size = [0, 0, 0] # size of the original part front view after projection at scale 1:1 + if bound_box.XLength > bound_box.YLength: + if bound_box.XLength > bound_box.ZLength: + main_axis = 0 + proj_size[0] = bound_box.XLength + proj_size[1] = bound_box.ZLength + proj_size[2] = bound_box.YLength + else: + main_axis = 2 + proj_size[0] = bound_box.ZLength + proj_size[1] = bound_box.XLength + proj_size[2] = bound_box.YLength + else: + if bound_box.YLength > bound_box.ZLength: + main_axis = 1 + proj_size[0] = bound_box.YLength + proj_size[1] = bound_box.ZLength + proj_size[2] = bound_box.XLength + else: + main_axis = 2 + proj_size[0] = bound_box.ZLength + proj_size[1] = bound_box.XLength + proj_size[2] = bound_box.YLength + + if verbose: print("Adding drawing page..."); + + page = doc.addObject('TechDraw::DrawPage', page_name) + template = doc.addObject('TechDraw::DrawSVGTemplate', 'Template') + + template.Template = template_file_name + page.Template = template + + if verbose: print("Computing best scale..."); + scale_denominators = [4.0, 5.0, 6.0, 8.0, 10.0] + scale_numerator = 1.0 + scale_denominator = scale_denominators[0] + proj_total_size = [proj_size[0] + proj_size[2], proj_size[1] + proj_size[2]] # projected size of all views (without spacing) at scale 1:1 + spacingX = 20.0 + spacingY = 50.0 + maxSizeX = 280.0 + maxSizeY = 160.0 + for denom in scale_denominators: + scale_denominator = denom + if proj_total_size[0]*scale_numerator/denom + spacingX <= maxSizeX and proj_total_size[1]*scale_numerator/denom + spacingY <= maxSizeY: + break + + if verbose: print("Adding projection group..."); + + projGroup = doc.addObject('TechDraw::DrawProjGroup', doc.Name + '_ProjGroup') + page.addView(projGroup) + projGroup.ScaleType = 'Custom' + projGroup.Scale = scale_numerator/scale_denominator + projGroup.spacingX = 20.0 + projGroup.spacingY = 50.0 + projGroup.Source = sources + projGroup.addProjection('Front') + if main_axis == 0: + projGroup.Anchor.Direction = App.Vector(0,1,0) + projGroup.Anchor.XDirection = App.Vector(-1,0,0) + projGroup.Anchor.RotationVector = App.Vector(-1,0,0) + elif main_axis == 1: + projGroup.Anchor.Direction = App.Vector(1,0,0) + projGroup.Anchor.XDirection = App.Vector(0,1,0) + projGroup.Anchor.RotationVector = App.Vector(0,1,0) + elif main_axis == 2: + projGroup.Anchor.Direction = App.Vector(0,1,0) + projGroup.Anchor.XDirection = App.Vector(0,0,1) + projGroup.Anchor.RotationVector = App.Vector(0,0,1) + projGroup.addProjection('Top') + projGroup.addProjection('Left') + projGroup.X = 130.0 + projGroup.Y = 150.0 + + texts = page.Template.EditableTexts + texts['SCALE'] = str(int(scale_numerator+0.5))+':'+str(int(scale_denominator+0.5)) + try: + texts['PM'] = main_object.Assembly_handbook_Material + except: + pass + texts['PN'] = doc.Name + texts['TITLELINE-1'] = doc.Name + page.Template.EditableTexts = texts + + async def addDimensions(): + for view in projGroup.Views: + if verbose: print("View: " + view.Label + "...") + + edges = [] + visibleEdges = view.getVisibleEdges() + edgeIdx = 0 + lowestEdgeName = '' + lowestEdgePos = 1000000 + while True: + try: + edge = view.getEdgeByIndex(edgeIdx) + except: + break + edges.append(edge) + + if edge.BoundBox.YLength < 0.01 and edge.BoundBox.Center.y < lowestEdgePos: + lowestEdgePos = edge.BoundBox.Center.y + lowestEdgeName = 'Edge' + str(edgeIdx) + + edgeIdx = edgeIdx + 1 + + vertices = [] + vertIdx = 0 + while True: + try: + vert = view.getVertexByIndex(vertIdx) + except: + break + vertices.append(vert) + vertIdx = vertIdx + 1 + + def getFeatureName(edge): + if edge.Curve.TypeId == 'Part::GeomCircle': + vertIdx = 0 + c = edge.BoundBox.Center + closestDist = 100000000 + closestVert = None + for vert in vertices: + dx = vert.X - c.x + dy = vert.Y - c.y + dist = math.sqrt(dx*dx + dy*dy) + if dist < closestDist: + closestDist = dist + closestVert = vert + vertIdx = vertIdx + 1 + if closestVert is not None: + return 'Vertex' + str(vertices.index(closestVert)) + else: + return '' + else: + return 'Edge'+str(edges.index(edge)) + + if verbose: print("Listing features...") + features = [] + for edge in edges: + if (edge.Curve.TypeId == 'Part::GeomLine' and edge.BoundBox.XLength <= 0.01) or (edge.Curve.TypeId == 'Part::GeomCircle' and abs(edge.Curve.Radius * 2.0 - edge.BoundBox.XLength) < 0.001 and abs(edge.Curve.Radius * 2.0 - edge.BoundBox.YLength) < 0.001): + featureName = getFeatureName(edge) + if featureName == '': + continue + + pos = edge.BoundBox.Center.x + duplicate = False + for otherFeature in features: + if abs(otherFeature[0] - pos) < 0.1: + duplicate = True + break + if not duplicate: + features.append((pos, edge, featureName)) + features.sort(key=lambda e: e[0]) + + def addDimension(edgeA, edgeB, posY): + dim = doc.addObject('TechDraw::DrawViewDimension','Dimension') + dim.Type = 'DistanceX' + dim.References2D = [(view, (getFeatureName(edgeA), getFeatureName(edgeB)))] + visibleEdgeA = visibleEdges[edges.index(edgeA)] + visibleEdgeB = visibleEdges[edges.index(edgeB)] + dim.X = (visibleEdgeA.BoundBox.Center.x + visibleEdgeB.BoundBox.Center.x) * 0.5 + dim.Y = posY + page.addView(dim) + + if edgeB.Curve.TypeId == 'Part::GeomCircle': + if abs(edgeB.BoundBox.XLength - 6.5) > 0.01: + dim = doc.addObject('TechDraw::DrawViewDimension','Dimension') + dim.Type = 'Diameter' + dim.References2D = [(view, ('Edge'+str(edges.index(edgeB)),))] + dim.X = visibleEdgeB.BoundBox.Center.x + 6.0 + dim.Y = -6.0 + page.addView(dim) + + if abs(edgeB.BoundBox.Center.y) > 0.01 and lowestEdgeName != '': + dim = doc.addObject('TechDraw::DrawViewDimension','Dimension') + dim.Type = 'DistanceY' + dim.References2D = [(view, (getFeatureName(edgeB),lowestEdgeName))] + dim.X = visibleEdgeB.BoundBox.Center.x + 2.0 + dim.Y = -6.0 + page.addView(dim) + + if verbose: print("Adding dimensions...") + + if len(features) >= 2: + if projGroup.Views.index(view) != 0: + addDimension(features[0][1], features[len(features)-1][1], -25.0) + + if len(features) > 2: + for featureIdx in range(0, len(features) - 1): + if featureIdx == 0 or features[featureIdx][1].Curve.TypeId != 'Part::GeomLine': + addDimension(features[featureIdx][1], features[featureIdx + 1][1], 15.0) + + if verbose: print("Adding secondary objects...") + if code_obj is not None: + projGroup.Source = projGroup.Source + [code_obj] + + page.recompute(True) + await vspt_coroutine.get_main_loop().wait(1) + await addDimensions() + + if verbose: print("Saving...") + page.recompute(True) + page.ViewObject.Visibility = False # don't save the document with the page open or it will automatically reopen on load + doc.save() + + if verbose: print("Closing...") + vspt_freecad.close_all_docs() + +async def run(): + try: + folders = [ + 'tubes' + ] + + for folder in folders: + files = os.listdir(project_folder + '/' + folder) + for source_file in files: + if not source_file.endswith('.FCStd'): continue + source_path = folder + '/' + source_file + print(source_path) + await generate_2d_drawing(source_path) + + # exit FreeCAD + vspt_freecad.close_all_docs() + FreeCADGui.getMainWindow().close() + + except Exception as e: + print(e) + +vspt_coroutine.get_main_loop().create_task(run()) + diff --git a/tools/generate-2d-drawings.sh b/tools/generate-2d-drawings.sh new file mode 100755 index 0000000..9e74862 --- /dev/null +++ b/tools/generate-2d-drawings.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +# Set the path to your FreeCAD executable here +FREECAD=~/dev/FreeCAD-asm3-Daily-Conda-Py3.10-20221128-glibc2.12-x86_64.AppImage + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +cd $SCRIPT_DIR/.. + +# Check script syntax before starting freecad +python3 -m py_compile tools/generate-2d-drawings.py + +# Start freecad to run the script. We must start freecad with GUI. We start it hidden in a virtual framebuffer (xvfb) so that it can run cleanly in the background. +xvfb-run $FREECAD tools/generate-2d-drawings.py +#$FREECAD tools/generate-2d-drawings.py diff --git a/tools/utils/vspt_coroutine.py b/tools/utils/vspt_coroutine.py new file mode 100644 index 0000000..cdb032f --- /dev/null +++ b/tools/utils/vspt_coroutine.py @@ -0,0 +1,38 @@ +import asyncio +from PySide.QtCore import QTimer + +class EventLoop: + loop = None + + def __init__(self): + self.loop = asyncio.new_event_loop() + + def create_task(self, coro): + self.loop.create_task(coro) + self.update() + + def update(self): + self.loop.stop() + self.loop.run_forever() + + async def wait(self, time_milliseconds): + #print("waiting " + str(time_milliseconds) + "ms...") + currentLoop = self + fut = self.loop.create_future() + def callback(): + #print("wait callback") + fut.set_result(True) + currentLoop.update() + QTimer.singleShot(time_milliseconds, callback) + await fut + #print("end wait") + +main_loop = None + +def get_main_loop(): + global main_loop + if main_loop is None: + #print("Creating main loop") + main_loop = EventLoop() + return main_loop + diff --git a/tools/utils/vspt_freecad.py b/tools/utils/vspt_freecad.py new file mode 100644 index 0000000..96af865 --- /dev/null +++ b/tools/utils/vspt_freecad.py @@ -0,0 +1,6 @@ +import FreeCAD as App + +def close_all_docs(): + #print("close_all_docs") + while len(App.listDocuments().values()) > 0: + App.closeDocument(list(App.listDocuments().values())[0].Name)