From 5208577d8614999bab52676dd92844cbc6787fe3 Mon Sep 17 00:00:00 2001 From: brothermechanic Date: Wed, 5 Apr 2023 11:26:09 +0000 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BC=D0=BF=D0=BE=D1=80=D1=82=20=D1=84?= =?UTF-8?q?=D1=80=D0=B5=D0=B9=D0=BC=D0=B0=20(=D0=BE=D0=B1=D1=8A=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B0=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BE=D1=81=D0=B5=D0=B9)=20=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=87=D0=B5=D1=81=D1=82=D0=B2=D0=B5=20=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=BC=D0=B0=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cg/blender/import_fcstd/README.md | 9 +-- cg/blender/import_fcstd/__init__.py | 12 ---- .../{importer.py => import_cad_objects.py} | 63 +++++++------------ .../import_fcstd/import_coordinate_point.py | 46 ++++++++++++++ cg/blender/import_fcstd/is_object_solid.py | 4 +- cg/blender/import_fcstd/materials.py | 1 - cg/blender/remesh/__init__.py | 4 +- cg/blender/utils/sdf_mesh_selector.py | 5 -- cg/pipeline/freecad_to_asset.py | 54 +++++++++++++--- 9 files changed, 122 insertions(+), 76 deletions(-) rename cg/blender/import_fcstd/{importer.py => import_cad_objects.py} (84%) create mode 100644 cg/blender/import_fcstd/import_coordinate_point.py diff --git a/cg/blender/import_fcstd/README.md b/cg/blender/import_fcstd/README.md index 75a7c67..9a6d195 100644 --- a/cg/blender/import_fcstd/README.md +++ b/cg/blender/import_fcstd/README.md @@ -1,13 +1,14 @@ ## import-fcstd ### Импорт .FCStd сцены FreeCAD в Blender. -Автор оригинальной версии сценария yorikvanhavre: [github](https://gist.github.com/yorikvanhavre/680156f59e2b42df8f5f5391cae2660b) - -Представлен в качестве модуля `importer.py` c подмодулями. -Импортирует все видимые solid объекты в качестве mesh в Blender сцену. +Представлен в качестве модуля `import_cad_objects` c подмодулями. +По умолчанию, импортирует из FreeCAD cцены все видимые solid объекты в Blender сцену. Задает имена mesh на основе solid объектов. Импортирует локальные координаты и задает их mesh объектам. Импортирует FEM материалы (если они есть) и задает их mesh объектам. Может работать как в качестве модуля, так и внутри blender сцены. +Так же пресдставлен модуль импорта точек координат `import_coordinate_point`. +Модуль может использоваться для передачи осей координат и позиций из FreeCAD. +Например локальные координаты подобъекта плоскости, точки или задание локального нуля. diff --git a/cg/blender/import_fcstd/__init__.py b/cg/blender/import_fcstd/__init__.py index e208cb7..b64814f 100644 --- a/cg/blender/import_fcstd/__init__.py +++ b/cg/blender/import_fcstd/__init__.py @@ -1,14 +1,2 @@ # -*- coding: utf-8 -*- -# Original code by (C) 2019 yorikvanhavre -# Copyright (C) 2023 Ilia Kurochkin -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. __version__ = "0.1" diff --git a/cg/blender/import_fcstd/importer.py b/cg/blender/import_fcstd/import_cad_objects.py similarity index 84% rename from cg/blender/import_fcstd/importer.py rename to cg/blender/import_fcstd/import_cad_objects.py index 3d8cae3..6249358 100644 --- a/cg/blender/import_fcstd/importer.py +++ b/cg/blender/import_fcstd/import_cad_objects.py @@ -11,7 +11,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. - +import time import FreeCAD import logging import xml @@ -29,10 +29,10 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def importer(filename, +def obj_importer(filename, + tessellation, update=False, placement=True, - tessellation=10.0, skiphidden=True, scale=0.001, select=True, @@ -40,33 +40,8 @@ def importer(filename, """Reads a FreeCAD .FCStd file and creates Blender objects""" - #path = '/usr/lib64/freecad/lib64' - TRIANGULATE = False # set to True to triangulate all faces (will loose multimaterial info) + TRIANGULATE = True # set to True to triangulate all faces (will loose multimaterial info) - ''' - try: - # append the FreeCAD path specified in addon preferences - user_preferences = bpy.context.preferences - addon_prefs = user_preferences.addons[__name__].preferences - path = addon_prefs.filepath - if path: - if os.path.isfile(path): - path = os.path.dirname(path) - logger.debug("Configured FreeCAD path:", path) - sys.path.append(path) - else: - logger.debug("FreeCAD path is not configured in preferences") - import FreeCAD - except: - logger.debug("Unable to import the FreeCAD Python module. Make sure it is installed on your system") - logger.debug("and compiled with Python3 (same version as Blender).") - logger.debug("It must also be found by Python, you might need to set its path in this Addon preferences") - logger.debug("(User preferences->Addons->expand this addon).") - if report: - report({'ERROR'},"Unable to import the FreeCAD Python module. Check Addon preferences.") - return {'CANCELLED'} - # check if we have a GUI document - ''' guidata = {} zdoc = zipfile.ZipFile(filename) if zdoc: @@ -91,7 +66,7 @@ def importer(filename, cols.append((buf[i*4+3],buf[i*4+2],buf[i*4+1],buf[i*4])) guidata[key]["DiffuseColor"] = cols zdoc.close() - #logger.debug ("guidata:",guidata) + doc = FreeCAD.open(filename) docname = doc.Name if not doc: @@ -99,7 +74,6 @@ def importer(filename, if report: report({'ERROR'},"Unable to open the given FreeCAD file") return {'CANCELLED'} - #logger.debug ("Transferring",len(doc.Objects),"objects to Blender") # import some FreeCAD modules needed below. After "import FreeCAD" these modules become available import Part @@ -131,17 +105,20 @@ def importer(filename, verts = [] edges = [] faces = [] - matindex = [] # face to material relationship - faceedges = [] # a placeholder to store edges that belong to a face + matindex = [] # face to material relationship + faceedges = [] # a placeholder to store edges that belong to a face if obj.isDerivedFrom("Part::Feature"): + # !!! # create mesh from shape shape = obj.Shape if placement: + # !!! placement = obj.Placement shape = obj.Shape.copy() shape.Placement = placement.inverse().multiply(shape.Placement) if shape.Faces: + # !!! if TRIANGULATE: # triangulate and make faces rawdata = shape.tessellate(tessellation) @@ -153,9 +130,12 @@ def importer(filename, for e in face.Edges: faceedges.append(e.hashCode()) else: + # !!! # write FreeCAD faces as polygons when possible + time_start = time.time() for face in shape.Faces: if (len(face.Wires) > 1) or (not isinstance(face.Surface,Part.Plane)) or hascurves(face): + # !!! # face has holes or is curved, so we need to triangulate it rawdata = face.tessellate(tessellation) for v in rawdata[0]: @@ -170,6 +150,7 @@ def importer(filename, faces.append(nf) matindex.append(len(rawdata[1])) else: + # !!! f = [] ov = face.OuterWire.OrderedVertexes for v in ov: @@ -188,11 +169,13 @@ def importer(filename, matindex.append(1) for e in face.Edges: faceedges.append(e.hashCode()) + logger.debug('faces time is %s', (time.time() - time_start)) for edge in shape.Edges: + # !!! # Treat remaining edges (that are not in faces) if not (edge.hashCode() in faceedges): if hascurves(edge): - dv = edge.discretize(9) #TODO use tessellation value + dv = edge.discretize(9) # TODO use tessellation value for i in range(len(dv)-1): dv1 = [dv[i].x,dv[i].y,dv[i].z] dv2 = [dv[i+1].x,dv[i+1].y,dv[i+1].z] @@ -216,12 +199,13 @@ def importer(filename, mesh = obj.Mesh if placement: placement = obj.Placement - mesh = obj.Mesh.copy() # in meshes, this zeroes the placement + mesh = obj.Mesh.copy() # in meshes, this zeroes the placement t = mesh.Topology verts = [[v.x,v.y,v.z] for v in t[0]] faces = t[1] if verts and (faces or edges): + # !!! # create or update object with mesh and material data bobj = None bmat = None @@ -230,14 +214,16 @@ def importer(filename, for o in bpy.data.objects: if o.data.name == obj.Name: bobj = o - logger.debug("Replacing existing object:",obj.Label) + logger.debug('Replacing existing %s', obj.Label) bmesh = bpy.data.meshes.new(name=obj.Name) bmesh.from_pydata(verts, edges, faces) bmesh.update() if bobj: + logger.debug('Updating the mesh of existing %s', obj.Label) # update only the mesh of existing object. Don't touch materials bobj.data = bmesh else: + # !!! # create new object bobj = bpy.data.objects.new(obj.Label, bmesh) if placement: @@ -251,6 +237,7 @@ def importer(filename, bobj.rotation_mode = m bobj.scale = (scale, scale, scale) if obj.Name in guidata: + # !!! # one material for the whole object for fem_mat in doc.Objects: set_fem_mat(obj, bobj, fem_mat) @@ -267,7 +254,3 @@ def importer(filename, logger.info("Import freecad scene finished without errors") return {'FINISHED'} - - -if __name__ == '__main__': - importer() diff --git a/cg/blender/import_fcstd/import_coordinate_point.py b/cg/blender/import_fcstd/import_coordinate_point.py new file mode 100644 index 0000000..f78119d --- /dev/null +++ b/cg/blender/import_fcstd/import_coordinate_point.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +#DESCRIPTION. +# Import from json exported FreeCAD's asm4 coordinates as Blender's empty object. +__version__ = "0.1" + +import logging +import bpy +import json + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def empty_importer(path_json): + with open(path_json) as f: + data = json.load(f) + + pivot_name = data['label'] + pivot_pose = data['placement'] + loc = tuple(pivot_pose['position'].values()) + fori = tuple(pivot_pose['orientation'].values()) + bori = (fori[3],)+fori[:3] + + bpy.ops.object.empty_add( + type='ARROWS', radius=0.01, align='WORLD', + location=(0, 0, 0), rotation=(0, 0, 0)) + pivot_obj = bpy.context.active_object # or bpy.context.object + pivot_obj.name = pivot_name + pivot_obj.rotation_mode = 'QUATERNION' + pivot_obj.location = loc + pivot_obj.rotation_quaternion = bori + + f.close() + logger.info('Point %s imported without errors', pivot_name) diff --git a/cg/blender/import_fcstd/is_object_solid.py b/cg/blender/import_fcstd/is_object_solid.py index 92c5564..12e93ee 100644 --- a/cg/blender/import_fcstd/is_object_solid.py +++ b/cg/blender/import_fcstd/is_object_solid.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# Original code by (C) 2019 yorikvanhavre # Copyright (C) 2023 Ilia Kurochkin # # This program is free software; you can redistribute it and/or modify @@ -11,6 +10,9 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. +# +#DESCRIPTION. +# Simple FreeCAD's object test for manifold mawater-tight surface. import FreeCAD diff --git a/cg/blender/import_fcstd/materials.py b/cg/blender/import_fcstd/materials.py index bca9e78..92defca 100644 --- a/cg/blender/import_fcstd/materials.py +++ b/cg/blender/import_fcstd/materials.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# Original code by (C) 2019 yorikvanhavre # Copyright (C) 2023 Ilia Kurochkin # # This program is free software; you can redistribute it and/or modify diff --git a/cg/blender/remesh/__init__.py b/cg/blender/remesh/__init__.py index 4d66b83..d5bf2b3 100644 --- a/cg/blender/remesh/__init__.py +++ b/cg/blender/remesh/__init__.py @@ -25,12 +25,10 @@ def asset_setup(transforms=True, sharpness=True, shading=True): bpy.context.view_layer.objects.active = ob if transforms: - # apply scale - apply_transforms(ob, location=False, rotation=False, scale=True) # remove doubles bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.remove_doubles(threshold = 0.0001) + bpy.ops.mesh.remove_doubles(threshold = 0.00001) bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.mesh.select_mode(type = 'FACE') bpy.ops.mesh.select_interior_faces() diff --git a/cg/blender/utils/sdf_mesh_selector.py b/cg/blender/utils/sdf_mesh_selector.py index 0016d28..89b59bf 100644 --- a/cg/blender/utils/sdf_mesh_selector.py +++ b/cg/blender/utils/sdf_mesh_selector.py @@ -12,12 +12,7 @@ import sys import xml.etree.ElementTree as ET import os sys.path.append('../') -from import_fcstd.importer import importer from mathutils import Matrix -from utils.remove_collections import remove_collections -from utils.cleanup_orphan_data import cleanup_orphan_data -from remesh import asset_setup -from export.stl import export_stl import bpy logger = logging.getLogger(__name__) diff --git a/cg/pipeline/freecad_to_asset.py b/cg/pipeline/freecad_to_asset.py index 01b71a7..4652d09 100644 --- a/cg/pipeline/freecad_to_asset.py +++ b/cg/pipeline/freecad_to_asset.py @@ -4,12 +4,15 @@ DESCRIPTION. Convert and setup FreeCAD solid objects to 3d assets mesh files. Support Blender compiled as a Python Module only! """ -__version__ = "0.1" +__version__ = "0.2" import logging +import os import sys sys.path.append('../blender/') -from import_fcstd.importer import importer +from import_fcstd.import_cad_objects import obj_importer +from import_fcstd.import_coordinate_point import empty_importer +from utils.apply_transforms import apply_transforms from utils.remove_collections import remove_collections from utils.cleanup_orphan_data import cleanup_orphan_data from utils.sdf_mesh_selector import sdf_mesh_selector @@ -17,27 +20,55 @@ from remesh import asset_setup from export.dae import export_dae from export.collision import export_col_stl import bpy +import mathutils logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) def freecad_asset_pipeline(fcstd_path, - mesh_export_path, - tessellation=10, + tessellation, + mesh_export_path=None, + json_path=None, blend_path=None, sdf_path=None): """ Setup FreeCAD scene to CG asset """ remove_collections() cleanup_orphan_data() - importer(fcstd_path, tessellation) + obj_importer(fcstd_path, tessellation) + + # apply scale to all objects + obs = bpy.context.selected_objects + for ob in obs: + bpy.ops.object.select_all(action='DESELECT') + ob.select_set(state=True) + bpy.context.view_layer.objects.active = ob + apply_transforms(ob, location=False, rotation=False, scale=True) + for ob in obs: + ob.select_set(state=True) + + if json_path is not None: + for point in os.listdir(json_path): + if point.endswith('.json'): + empty_importer(point) + if sdf_path is not None: sdf_mesh_selector(sdf_path) + asset_setup() + if blend_path is not None: bpy.ops.wm.save_as_mainfile(filepath=blend_path) - export_dae(mesh_export_path) - export_col_stl(mesh_export_path) + + # export all objects + if mesh_export_path is not None: + obs = bpy.context.selected_objects + for ob in obs: + ob.matrix_world = mathutils.Matrix() + for ob in obs: + ob.select_set(state=True) + export_dae(mesh_export_path) + export_col_stl(mesh_export_path) if __name__ == '__main__': @@ -47,10 +78,12 @@ if __name__ == '__main__': description='Convert and setup FreeCAD solid objects to 3d assets mesh files.') parser.add_argument( '--fcstd_path', type=str, help='Path to source FreeCAD scene', required=True) - parser.add_argument( - '--mesh_export_path', type=str, help='Path for export meshes', required=True) parser.add_argument( '--tessellation', type=int, help='Tessellation number', default=10, required=False) + parser.add_argument( + '--mesh_export_path', type=str, help='Path for export meshes', required=False) + parser.add_argument( + '--json_path', type=str, help='Path to DIR with coordinate points jsons', required=False) parser.add_argument( '--blend_path', type=str, help='Path for export blend assembly file', required=False) parser.add_argument( @@ -58,8 +91,9 @@ if __name__ == '__main__': args = parser.parse_args() freecad_asset_pipeline(args.fcstd_path, - args.mesh_export_path, args.tessellation, + args.mesh_export_path, + args.json_path, args.blend_path, args.sdf_path)