Repository: jesterKing/import_3dm Branch: master Commit: 77c53bf97d3c Files: 40 Total size: 99.5 KB Directory structure: gitextract_c7p8ffb8/ ├── .gitattributes ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── import_3dm/ │ ├── __init__.py │ ├── blender_manifest.toml │ ├── converters/ │ │ ├── __init__.py │ │ ├── annotation.py │ │ ├── curve.py │ │ ├── groups.py │ │ ├── instances.py │ │ ├── layers.py │ │ ├── material.py │ │ ├── pointcloud.py │ │ ├── rdk_manager.py │ │ ├── render_mesh.py │ │ ├── utils.py │ │ └── views.py │ ├── read3dm.py │ └── wheels/ │ ├── rhino3dm-8.17.0-cp311-cp311-linux_aarch64.whl │ ├── rhino3dm-8.17.0-cp311-cp311-linux_x86_64.whl │ ├── rhino3dm-8.17.0-cp311-cp311-macosx_13_0_universal2.whl │ ├── rhino3dm-8.17.0-cp311-cp311-win_amd64.whl │ ├── rhino3dm-8.17.0-cp313-cp313-linux_aarch64.whl │ ├── rhino3dm-8.17.0-cp313-cp313-linux_x86_64.whl │ ├── rhino3dm-8.17.0-cp313-cp313-macosx_13_0_universal2.whl │ └── rhino3dm-8.17.0-cp313-cp313-win_amd64.whl ├── requirements.txt └── test/ ├── pytest.ini_example ├── pytest_setup.md ├── test_import_3dm.py └── units/ ├── boxes_in_cm.3dm ├── boxes_in_ft.3dm ├── boxes_in_in.3dm ├── boxes_in_m.3dm ├── boxes_in_mm.3dm └── unit_conversion_testing.blend ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.whl -text ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Windows, Linux] - Blender version/build **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg MANIFEST *.zip *.tar.gz *-win*/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # vscode .vscode/ # MacOS .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Nathan Letwory Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PULL_REQUEST_TEMPLATE.md ================================================ # Title of the PR changeset (Fix certain issue, or Implement/Add feature) A short description of the changes ## detailed explanation * with bullet list * explain the major changes that this PR holds ## fixes / resolves Type here the issues that are fixed, i.e. Resolves or Fixes #issuenumber. If your PR addresses multiple issues mention each one on a line by its own with the proper verb. ================================================ FILE: README.md ================================================ Import Rhinoceros 3D files in Blender ===================================== This add-on uses the `rhino3dm.py` module (https://github.com/mcneel/rhino3dm) to read in 3dm files. Requirements ============ This add-on works with Blender 4.2 and later. Installation ============ On Windows and MacOS you need to download the correct ZIP archive from https://github.com/jesterKing/import_3dm/releases/latest . 1. Download ZIP archive 1. Open Blender preferences 1. Open Add-ons section 1. Click Install... button 1. Select the downloaded ZIP archive 1. Click Install 1. Enable the add-on ================================================ FILE: import_3dm/__init__.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. bl_info = { "name": "Import Rhinoceros 3D", "author": "Nathan 'jesterKing' Letwory, Joel Putnam, Tom Svilans, Lukas Fertig, Bernd Moeller", "version": (0, 0, 18), "blender": (4, 2, 0), "location": "File > Import > Rhinoceros 3D (.3dm)", "description": "This addon lets you import Rhinoceros 3dm files in Blender 4.2 and later", "warning": "The importer doesn't handle all data in 3dm files yet", "wiki_url": "https://github.com/jesterKing/import_3dm", "category": "Import-Export", } # with extentions bl_info is deleted, we keep a copy of the version bl_info_version = bl_info["version"][:] import bpy # ImportHelper is a helper class, defines filename and # invoke() function which calls the file selector. from bpy_extras.io_utils import ImportHelper, poll_file_object_drop from bpy.props import FloatProperty, StringProperty, BoolProperty, EnumProperty, IntProperty from bpy.types import Operator from typing import Any, Dict from .read3dm import read_3dm class Import3dm(Operator, ImportHelper): """Import Rhinoceros 3D files (.3dm). Currently does render meshes only, more geometry and data to follow soon.""" bl_idname = "import_3dm.some_data" # important since its how bpy.ops.import_3dm.some_data is constructed bl_label = "Import Rhinoceros 3D file" bl_options = {"REGISTER", "UNDO"} # ImportHelper mixin class uses this filename_ext = ".3dm" filter_glob: StringProperty( default="*.3dm", options={'HIDDEN'}, maxlen=1024, # Max internal buffer length, longer would be clamped. ) # type: ignore # List of operator properties, the attributes will be assigned # to the class instance from the operator settings before calling. import_hidden_objects: BoolProperty( name="Hidden Geometry", description="Import hidden geometry.", default=True, ) # type: ignore import_hidden_layers: BoolProperty( name="Hidden Layers", description="Import hidden layers.", default=True, ) # type: ignore import_layers_as_empties: BoolProperty( name="Layers as Empties", description="Import iayers as empties instead of groups.", default=True, ) # type: ignore import_annotations: BoolProperty( name="Annotations", description="Import annotations.", default=True, ) # type: ignore import_curves: BoolProperty( name="Curves", description="Import curves.", default=True, ) # type: ignore import_meshes: BoolProperty( name="Meshes", description="Import meshes.", default=True, ) # type: ignore import_subd: BoolProperty( name="SubD", description="Import SubDs.", default=True, ) # type: ignore import_extrusions: BoolProperty( name="Extrusions", description="Import extrusions.", default=True, ) # type: ignore import_brep: BoolProperty( name="BRep", description="Import B-Reps.", default=True, ) # type: ignore import_pointset: BoolProperty( name="PointSet", description="Import PointSets.", default=True, ) # type: ignore import_views: BoolProperty( name="Standard", description="Import standard views (Top, Front, Right, Perspective) as cameras.", default=False, ) # type: ignore import_named_views: BoolProperty( name="Named", description="Import named views as cameras.", default=True, ) # type: ignore import_groups: BoolProperty( name="Groups", description="Import groups as collections.", default=False, ) # type: ignore import_nested_groups: BoolProperty( name="Nested Groups", description="Recreate nested group hierarchy as collections.", default=False, ) # type: ignore import_instances: BoolProperty( name="Blocks", description="Import blocks as collection instances.", default=True, ) # type: ignore import_instances_grid_layout: BoolProperty( name="Grid Layout", description="Lay out block definitions in a grid ", default=False, ) # type: ignore import_instances_grid: IntProperty( name="Grid", description="Block layout grid size (in import units)", default=10, min=1, ) # type: ignore link_materials_to : EnumProperty( items=(("PREFERENCES", "Use Preferences", "Use the option defined in preferences."), ("OBJECT", "Object", "Link material to object."), ("DATA", "Object Data", "Link material to object data.")), name="Link To", description="Set how materials should be linked", default="PREFERENCES", ) # type: ignore update_materials: BoolProperty( name="Update Materials", description="Update existing materials. When unchecked create new materials if existing ones are found.", default=True, ) # type: ignore merge_by_distance: BoolProperty( name="Merge Vertices By Distance", description="Merge vertices based on their proximity.", default=False, ) # type: ignore merge_distance: FloatProperty( name="Merge Distance", description="Maximinum distance between elements to merge.", default=0.0001, min=0.0, subtype="DISTANCE" ) # type: ignore subD_level_viewport: IntProperty( name="SubD Levels Viewport", description="Number of subdivisions to perform in the 3D viewport.", default=2, min=0, max=6, ) # type: ignore subD_level_render: IntProperty( name="SubD Levels Render", description="Number of subdivisions to perform when rendering.", default=2, min=0, max=6, ) # type: ignore subD_boundary_smooth: EnumProperty( items=(("ALL", "All", "Smooth boundaries, including corners"), ("PRESERVE_CORNERS", "Keep Corners", "Smooth boundaries, but corners are kept sharp"),), name="SubD Boundary Smooth", description="Controls how open boundaries are smoothed", default="ALL", ) # type: ignore @classmethod def poll(cls, context: bpy.types.Context): return context.mode == "OBJECT" def execute(self, context : bpy.types.Context): options = self.as_keywords() # Single file import return read_3dm(context, self.filepath, options) def draw(self, _ : bpy.types.Context): layout = self.layout layout.label(text="Import .3dm v{}.{}.{}".format(bl_info_version[0], bl_info_version[1], bl_info_version[2])) box = layout.box() box.label(text="Objects") row = box.row() col = row.column() col.prop(self, "import_brep") col.prop(self, "import_extrusions") col.prop(self, "import_subd") col.prop(self, "import_meshes") col = row.column() col.prop(self, "import_curves") col.prop(self, "import_annotations") col.prop(self, "import_pointset") box = layout.box() box.label(text="Visibility") col = box.column() col.prop(self, "import_hidden_objects") col.prop(self, "import_hidden_layers") box = layout.box() box.label(text="Layers") row = box.row() row.prop(self, "import_layers_as_empties") box = layout.box() box.label(text="Views") row = box.row() row.prop(self, "import_views") row.prop(self, "import_named_views") box = layout.box() box.label(text="Groups") row = box.row() row.prop(self, "import_groups") row.prop(self, "import_nested_groups") box = layout.box() box.label(text="Blocks") col = box.column() col.prop(self, "import_instances") col.prop(self, "import_instances_grid_layout") col.prop(self, "import_instances_grid") box = layout.box() box.label(text="Materials") col = box.column() col.prop(self, "link_materials_to") col.prop(self, "update_materials") box = layout.box() box.label(text="Meshes & SubD") box.prop(self, "subD_level_viewport") box.prop(self, "subD_level_render") box.prop(self, "subD_boundary_smooth") box.prop(self, "merge_by_distance") col = box.column() col.enabled = self.merge_by_distance col.prop(self, "merge_distance") def invoke(self, context, event): self.files = [] return ImportHelper.invoke_popup(self, context) class IO_FH_3dm_import(bpy.types.FileHandler): bl_idname = "IO_FH_3dm_import" bl_label = "File handler for Rhinoceros 3D file import" bl_import_operator = "import_3dm.some_data" bl_file_extensions = ".3dm" @classmethod def poll_drop(cls, context): return poll_file_object_drop(context) # Only needed if you want to add into a dynamic menu def menu_func_import(self, _ : bpy.types.Context): self.layout.operator(Import3dm.bl_idname, text="Rhinoceros 3D (.3dm)") def register(): bpy.utils.register_class(Import3dm) bpy.utils.register_class(IO_FH_3dm_import) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) def unregister(): bpy.utils.unregister_class(Import3dm) bpy.utils.unregister_class(IO_FH_3dm_import) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) if __name__ == "__main__": register() # test call bpy.ops.import_3dm.some_data('INVOKE_DEFAULT') ================================================ FILE: import_3dm/blender_manifest.toml ================================================ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "import_3dm" version = "0.0.18" name = "Import Rhinoceros 3D" tagline = "Import Rhinoceros 3dm files in Blender" maintainer = "Nathan 'jesterKing' Letwory" # Supported types: "add-on", "theme" type = "add-on" # Optional: add-ons can list which resources they will require: # * "files" (for access of any filesystem operations) # * "network" (for internet access) # * "clipboard" (to read and/or write the system clipboard) # * "camera" (to capture photos and videos) # * "microphone" (to capture audio) # permissions = ["files", "network"] # Optional link to documentation, support, source files, etc # website = "http://extensions.blender.org/add-ons/my-example-package/" # Optional list defined by Blender and server, see: # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html tags = ["Import-Export",] blender_version_min = "4.2.0" # Optional: maximum supported Blender version # blender_version_max = "5.1.0" # License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) # https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html license = [ "MIT", ] # Optional: required by some licenses. # copyright = [ # "2002-2024 Developer Name", # "1998 Company Name", # ] # Optional list of supported platforms. If omitted, the extension will be available in all operating systems. platforms = ["windows-x64", "macos-arm64", "macos-x86_64", "linux-x64", "linux-arm64"] # Other supported platforms: "windows-arm64", "macos-x86_64" # Optional: bundle 3rd party Python modules. # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html wheels = [ "./wheels/rhino3dm-8.17.0-cp311-cp311-macosx_13_0_universal2.whl", "./wheels/rhino3dm-8.17.0-cp313-cp313-macosx_13_0_universal2.whl", "./wheels/rhino3dm-8.17.0-cp311-cp311-win_amd64.whl", "./wheels/rhino3dm-8.17.0-cp313-cp313-win_amd64.whl", "./wheels/rhino3dm-8.17.0-cp311-cp311-linux_x86_64.whl", "./wheels/rhino3dm-8.17.0-cp313-cp313-linux_x86_64.whl", "./wheels/rhino3dm-8.17.0-cp311-cp311-linux_aarch64.whl", "./wheels/rhino3dm-8.17.0-cp313-cp313-linux_aarch64.whl", ] # Optional: build setting. # https://docs.blender.org/manual/en/dev/advanced/command_line/extension_arguments.html#command-line-args-extensions # [build] paths_exclude_pattern = [ "/.git/", "__pycache__/" ] ================================================ FILE: import_3dm/converters/__init__.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import rhino3dm as r3d import bpy from bpy import context import uuid from typing import Any, Dict from .material import handle_materials, material_name, DEFAULT_RHINO_MATERIAL from .layers import handle_layers from .render_mesh import import_render_mesh from .curve import import_curve from .views import handle_views from .groups import handle_groups from .instances import import_instance_reference, handle_instance_definitions, populate_instance_definitions from .pointcloud import import_pointcloud from .annotation import import_annotation from . import utils ''' Dictionary mapping between the Rhino file types and importer functions ''' RHINO_TYPE_TO_IMPORT = { r3d.ObjectType.Brep : import_render_mesh, r3d.ObjectType.Extrusion : import_render_mesh, r3d.ObjectType.Mesh : import_render_mesh, r3d.ObjectType.SubD : import_render_mesh, r3d.ObjectType.Curve : import_curve, r3d.ObjectType.PointSet: import_pointcloud, r3d.ObjectType.Annotation: import_annotation, #r3d.ObjectType.InstanceReference : import_instance_reference } def initialize( context : bpy.types.Context ) -> None: utils.reset_all_dict(context) def cleanup() -> None: utils.clear_all_dict() # TODO: Decouple object data creation from object creation # and consolidate object-level conversion. def convert_object( context : bpy.types.Context, ob : r3d.File3dmObject, name : str, layer : bpy.types.Collection, rhinomat : bpy.types.Material, view_color, scale : float, options : Dict[str, Any]): """ Add a new object with given data, link to collection given by layer """ update_materials = options.get("update_materials", False) link_materials_to = options.get("link_materials_to", "PREFERENCES") data = None blender_object = None # Text curve is created by annotation import. # this needs to be added as an extra object # and parented to the annotation main import object text_curve = None text_object = None if ob.Geometry.ObjectType in RHINO_TYPE_TO_IMPORT: data = RHINO_TYPE_TO_IMPORT[ob.Geometry.ObjectType](context, ob, name, scale, options) if ob.Geometry.ObjectType == r3d.ObjectType.Annotation: text_curve = data[1] data = data[0] mat_from_object = ob.Attributes.MaterialSource == r3d.ObjectMaterialSource.MaterialFromObject tags = utils.create_tag_dict(ob.Attributes.Id, ob.Attributes.Name) if data is not None: data.materials.clear() data.materials.append(rhinomat) blender_object = utils.get_or_create_iddata(context.blend_data.objects, tags, data) if link_materials_to == "PREFERENCES": link_materials_to = bpy.context.preferences.edit.material_link if link_materials_to == 'OBDATA': link_materials_to = 'DATA' for slot in blender_object.material_slots: slot.link = link_materials_to if text_curve: text_tags = utils.create_tag_dict(uuid.uuid1(), f"TXT{ob.Attributes.Name}") text_curve[0].materials.append(rhinomat) text_object = utils.get_or_create_iddata(context.blend_data.objects, text_tags, text_curve[0]) text_object.material_slots[0].link = 'OBJECT' text_object.material_slots[0].material = rhinomat text_object.parent = blender_object texmatrix = text_curve[1] text_object.matrix_world = texmatrix else: blender_object = context.blend_data.objects.new(name+"_Instance", None) utils.tag_data(blender_object, tags) blender_object.color = [x/255. for x in view_color] if ob.Geometry.ObjectType == r3d.ObjectType.InstanceReference and options.get("import_instances",False): import_instance_reference(context, ob, blender_object, name, scale, options) # If subd, apply subdivision modifier if ob.Geometry.ObjectType == r3d.ObjectType.SubD: if blender_object.modifiers.find("SubD") == -1: blender_object.modifiers.new(type="SUBSURF", name="SubD") blender_object.modifiers["SubD"].levels = options.get("subD_level_viewport", 2) blender_object.modifiers["SubD"].render_levels = options.get("subD_level_render", 2) blender_object.modifiers["SubD"].boundary_smooth = options.get("subD_boundary_smooth", "ALL") # Import Rhino user strings for pair in ob.Attributes.GetUserStrings(): blender_object[pair[0]] = pair[1] for pair in ob.Geometry.GetUserStrings(): blender_object[pair[0]] = pair[1] if not ob.Attributes.IsInstanceDefinitionObject and ob.Geometry.ObjectType != r3d.ObjectType.InstanceReference and update_materials: blender_object.material_slots[0].link = 'OBJECT' blender_object.material_slots[0].material = rhinomat #instance definition objects are linked within their definition collections if not ob.Attributes.IsInstanceDefinitionObject: try: if options.get("import_layers_as_empties", False): blender_object.parent = layer if text_object is not None: text_object.parent = layer # also link object to same collections as parent for col in layer.users_collection: col.objects.link(blender_object) if text_object is not None: col.objects.link(text_object) else: layer.objects.link(blender_object) if text_object is not None: layer.objects.link(text_object) except Exception: pass ================================================ FILE: import_3dm/converters/annotation.py ================================================ # MIT License # Copyright (c) 2024 Nathan Letwory # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import rhino3dm as r3d from . import utils from . import curve from mathutils import Matrix import math from enum import IntEnum, auto import bpy class PartType(IntEnum): ExtensionLine = auto() DimensionLine = auto() CONVERT = {} class Arrow(IntEnum): Arrow1 = auto() Arrow2 = auto() Leader = auto() Leader2 = auto() # used in angular for second arrow def _arrowtype_from_arrow(dimstyle : r3d.DimensionStyle, arrow : Arrow): if arrow == Arrow.Arrow1: return dimstyle.ArrowType1 elif arrow == Arrow.Arrow2: return dimstyle.ArrowType2 elif arrow in (Arrow.Leader, Arrow.Leader2): return dimstyle.LeaderArrowType def _negate_vector3d(v : r3d.Vector3d): return r3d.Vector3d(-v.X, -v.Y, -v.Z) def _rotate_plane_to_line(plane : r3d.Plane, line : r3d.Line, addangle=0.0): rotangle = r3d.Vector3d.VectorAngle(_negate_vector3d(line.Direction), plane.XAxis) + addangle dpx = r3d.Vector3d.DotProduct(line.Direction, plane.XAxis) dpy = r3d.Vector3d.DotProduct(line.Direction, plane.YAxis) if dpx < 0 and dpy > 0 or dpx > 0 and dpy > 0: rotangle = 2*math.pi - rotangle plane = plane.Rotate(rotangle, plane.ZAxis) return plane def _add_arrow(dimstyle : r3d.DimensionStyle, pt : PartType, plane : r3d.Plane, bc, tip : r3d.Point3d, tail : r3d.Point3d, arrow : Arrow, scale : float): arrtype = _arrowtype_from_arrow(dimstyle, arrow) arrowhead_points = r3d.Arrowhead.GetPoints(arrtype, 1.0) arrowhead = bc.splines.new('POLY') arrowhead.use_cyclic_u = True arrowhead.points.add(len(arrowhead_points)-1) l = r3d.Line(tip, tail) arrowLength = dimstyle.ArrowLength inside = arrowLength * 2 < l.Length if arrow not in (Arrow.Leader, Arrow.Leader2) else True tip_plane = r3d.Plane(tip, plane.XAxis, plane.YAxis) if arrow == Arrow.Leader: # rotate tip_plane so we get correct orientation of arrowhead tip_plane = _rotate_plane_to_line(tip_plane, l) if arrtype in (r3d.ArrowheadTypes.SolidTriangle, r3d.ArrowheadTypes.ShortTriangle, r3d.ArrowheadTypes.OpenArrow, r3d.ArrowheadTypes.LongTriangle, r3d.ArrowheadTypes.LongerTriangle): if inside and arrow == Arrow.Arrow1: tip_plane = tip_plane.Rotate(math.pi, tip_plane.ZAxis) if not inside and arrow == Arrow.Arrow2: tip_plane = tip_plane.Rotate(math.pi, tip_plane.ZAxis) if arrtype in (r3d.ArrowheadTypes.Rectangle,): if arrow == Arrow.Arrow1: tip_plane = tip_plane.Rotate(math.pi, tip_plane.ZAxis) if inside: for i in range(0, len(arrowhead_points)): uv = arrowhead_points[i] p = tip_plane.PointAt(uv.X, uv.Y) arrowhead.points[i].co = (p.X * scale, p.Y * scale, p.Z * scale, 1) def _populate_line(dimstyle : r3d.DimensionStyle, pt : PartType, plane : r3d.Plane, bc, pt1 : r3d.Point3d, pt2 : r3d.Point3d, scale : float): rhl = r3d.Line(pt1, pt2) if rhl.Length < 1e-6: return line = bc.splines.new('POLY') line.points.add(1) # create line between given points if pt == PartType.ExtensionLine: ext = dimstyle.ExtensionLineExtension offset = dimstyle.ExtensionLineOffset extfr = 1.0 + ext / rhl.Length if rhl.Length > 0 else 0.0 offsetfr = offset / rhl.Length if rhl.Length > 0 else 0.0 pt1 = rhl.PointAt(offsetfr) pt2 = rhl.PointAt(extfr) pt1 *= scale pt2 *= scale line.points[0].co = (pt1.X, pt1.Y, pt1.Z, 1) line.points[1].co = (pt2.X, pt2.Y, pt2.Z, 1) def _add_text(dimstyle : r3d.DimensionStyle, plane : r3d.Plane, bc, pt : r3d.Point3d, txt : str, scale : float, left=False, textob=False): textcurve = bpy.context.blend_data.curves.new(name="annotation_text", type="FONT") textcurve.body = txt # for now only use blender built-in font. Scale that down to # 0.8 since it is a bit larger than Rhino default Arial textcurve.size = dimstyle.TextHeight * scale * 0.8 textcurve.align_x = 'CENTER' if not left else 'LEFT' pt *= scale plane = r3d.Plane(pt, plane.XAxis, plane.YAxis) if not textob: xform = r3d.Transform.PlaneToPlane(r3d.Plane.WorldXY(), plane) else: textcurve.align_x = 'CENTER' textcurve.align_y = 'TOP' plane = plane.Rotate(math.pi, plane.ZAxis) trl = r3d.Transform.Translation(0.0, -0.05, 0.00) xform = r3d.Transform.Multiply(trl, r3d.Transform.PlaneToPlane(r3d.Plane.WorldXY(), plane)) bm = utils.matrix_from_xform(xform) if textob: # when adding a text annotation we need to verify that the tranform # from XY plane to text plane has positive rotation value in the X of # euler that represents the rotation for this transform. # If it is negative add 180deg to both X and Z of the euler rotation. (loc, rot, sca) = bm.decompose() rote = rot.to_euler() if rote.x < 0: rote.x += math.pi rote.z += math.pi q = rote.to_quaternion() bm = Matrix.LocRotScale(loc, q, sca) return (textcurve, bm) def import_dim_linear(model, dimlin, bc, scale): pts = dimlin.Points txt = dimlin.PlainText dimstyle = model.DimStyles.FindId(dimlin.DimensionStyleId) p = dimlin.Plane displines = dimlin.GetDisplayLines(dimstyle) for displine in displines["lines"]: _populate_line(dimstyle, PartType.DimensionLine, p, bc, displine.From, displine.To, scale) _add_arrow(dimstyle, PartType.DimensionLine, p, bc, pts["arrowpt1"], pts["arrowpt2"], Arrow.Arrow1, scale) _add_arrow(dimstyle, PartType.DimensionLine, p, bc, pts["arrowpt2"], pts["arrowpt1"], Arrow.Arrow2, scale) return _add_text(dimstyle, p, bc, pts["textpt"], txt, scale) CONVERT[r3d.AnnotationTypes.Aligned] = import_dim_linear CONVERT[r3d.AnnotationTypes.Rotated] = import_dim_linear def import_radius(model, dimrad, bc, scale): pts = dimrad.Points txt = dimrad.PlainText dimstyle = model.DimStyles.FindId(dimrad.DimensionStyleId) p = dimrad.Plane displines = dimrad.GetDisplayLines(dimstyle) for displine in displines["lines"]: _populate_line(dimstyle, PartType.DimensionLine, p, bc, displine.From, displine.To, scale) _add_arrow(dimstyle, PartType.DimensionLine, p, bc, pts["radiuspt"], pts["dimlinept"], Arrow.Leader, scale) return _add_text(dimstyle, p, bc, pts["kneept"], txt, scale) CONVERT[r3d.AnnotationTypes.Radius] = import_radius CONVERT[r3d.AnnotationTypes.Diameter] = import_radius def import_angular(model, dimang, bc, scale): pts = dimang.Points r = dimang.Radius a = dimang.Angle txt = dimang.PlainText dimstyle = model.DimStyles.FindId(dimang.DimensionStyleId) displines = dimang.GetDisplayLines(dimstyle) p = dimang.Plane for line in displines["lines"]: _populate_line(dimstyle, PartType.DimensionLine, p, bc, line.From, line.To, scale) # set up midline and angle addition for text plane orientation arrow_line= r3d.Line(pts["arrowpt2"], pts["arrowpt1"]) mp = arrow_line.PointAt(0.5) midline = r3d.Line(mp, pts["centerpt"]) addangle = math.pi * -0.5 if a > math.pi: addangle = math.pi * 1.5 for arc in displines["arcs"]: nc_arc = arc.ToNurbsCurve() curve.import_nurbs_curve(nc_arc, bc, scale, is_arc=True) arc = displines["arcs"][0] # calculate the arrow tail points. These points we can pass # on to the arrow import function to ensure they are in a # mostly correct orientation. arrowLength = dimstyle.ArrowLength arclen = arc.Length T0 = nc_arc.Domain.T0 T1 = nc_arc.Domain.T1 domlen = T1 - T0 lenfrac = domlen / arclen arr_frac = arrowLength / domlen * lenfrac endpt1 = nc_arc.PointAt(T0 + arr_frac) endpt2 = nc_arc.PointAt(T1 - arr_frac) """ # Debug code adding empties for end points for ep, dispt in ((endpt1, 'PLAIN_AXES'), (endpt2, 'ARROWS')): tstob = bpy.context.blend_data.objects.new("tst", None) tstob.location = (ep.X, ep.Y, ep.Z) tstob.empty_display_type = dispt tstob.empty_display_size = 0.3 bpy.context.blend_data.collections[0].objects.link(tstob) """ # Add the arrow heads _add_arrow(dimstyle, PartType.DimensionLine, p, bc, pts["arrowpt1"], endpt1, Arrow.Leader, scale) _add_arrow(dimstyle, PartType.DimensionLine, p, bc, pts["arrowpt2"], endpt2, Arrow.Leader, scale) # set up the text plane textplane = dimang.Plane # rotate it according the midline and add extra angle to orient the text # correctly textplane = _rotate_plane_to_line(textplane, midline, addangle=addangle) textplane = r3d.Plane(pts["textpt"], textplane.XAxis, textplane.YAxis) # add the text and return the text curve so it can be added # properly to the scene, parented to the main annotation object return _add_text(dimstyle, textplane, bc, pts["textpt"], txt, scale) CONVERT[r3d.AnnotationTypes.Angular] = import_angular CONVERT[r3d.AnnotationTypes.Angular3pt] = import_angular def import_leader(model, dimlead, bc, scale): txt = dimlead.PlainText dimstyle = model.DimStyles.FindId(dimlead.DimensionStyleId) pts = dimlead.Points textptuv = dimlead.GetTextPoint2d(dimstyle, 1.0) textpt = dimlead.Plane.PointAt(textptuv.X, textptuv.Y) for i in range(0, len(pts)-1): _populate_line(dimstyle, PartType.DimensionLine, dimlead.Plane, bc, pts[i], pts[i+1], scale) _add_arrow(dimstyle, PartType.DimensionLine, dimlead.Plane, bc, pts[0], pts[1], Arrow.Leader, scale) return _add_text(dimstyle, dimlead.Plane, bc, textpt, txt, scale) CONVERT[r3d.AnnotationTypes.Leader] = import_leader def import_text(model, textannotation, bc, scale): txt = textannotation.PlainText dimstyle = model.DimStyles.FindId(textannotation.DimensionStyleId) textpt = textannotation.Plane.Origin return _add_text(dimstyle, textannotation.Plane, bc, textpt, txt, scale, left=False, textob=True) CONVERT[r3d.AnnotationTypes.Text] = import_text def import_ordinate(model, dimordinate, bc, scale): txt = dimordinate.PlainText dimstyle = model.DimStyles.FindId(dimordinate.DimensionStyleId) pts = dimordinate.Points textplane = dimordinate.Plane displines = dimordinate.GetDisplayLines(dimstyle) l = r3d.Line(pts["kinkpt1"], pts["defpt"]) textplane = _rotate_plane_to_line(textplane, l) for displine in displines["lines"]: _populate_line(dimstyle, PartType.DimensionLine, dimordinate.Plane, bc, displine.From, displine.To, scale) return _add_text(dimstyle, textplane, bc, pts["leaderpt"], txt, scale, left=True) CONVERT[r3d.AnnotationTypes.Ordinate] = import_ordinate def import_centermark(model, centermark, bc, scale): dimstyle = model.DimStyles.FindId(centermark.DimensionStyleId) lines = centermark.GetDisplayLines(dimstyle) for line in lines: _populate_line(dimstyle, PartType.DimensionLine, centermark.Plane, bc, line.From, line.To, scale) CONVERT[r3d.AnnotationTypes.CenterMark] = import_centermark def import_annotation(context, ob, name, scale, options): if not "rh_model" in options: return model = options["rh_model"] if not model: return og = ob.Geometry oa = ob.Attributes text = None curve_data = context.blend_data.curves.new(name, type="CURVE") curve_data.dimensions = '2D' curve_data.fill_mode = 'BOTH' if og.AnnotationType in CONVERT: text = CONVERT[og.AnnotationType](model, og, curve_data, scale) else: print(f"Annotation type {og.AnnotationType} not implemented") return (curve_data, text) ================================================ FILE: import_3dm/converters/curve.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import rhino3dm as r3d from . import utils from mathutils import Vector from mathutils.geometry import intersect_line_line CONVERT = {} def import_null(rcurve, bcurve, scale): print("Failed to convert type", type(rcurve)) return None def import_line(rcurve, bcurve, scale): fr = point_to_vector(rcurve.Line.From) * scale to = point_to_vector(rcurve.Line.To) * scale line = bcurve.splines.new('POLY') line.points.add(1) line.points[0].co = (fr.x, fr.y, fr.z, 1) line.points[1].co = (to.x, to.y, to.z, 1) return line CONVERT[r3d.LineCurve] = import_line def import_polyline(rcurve, bcurve, scale): N = rcurve.PointCount polyline = bcurve.splines.new('POLY') polyline.use_cyclic_u = rcurve.IsClosed if rcurve.IsClosed: N -= 1 polyline.points.add(N - 1) for i in range(0, N): rpt = rcurve.Point(i) polyline.points[i].co = (rpt.X * scale, rpt.Y * scale, rpt.Z * scale, 1) CONVERT[r3d.PolylineCurve] = import_polyline def import_nurbs_curve(rcurve, bcurve, scale, is_arc = False): # create a list of points where # we ensure we don't have duplicates. Rhino curves # may have duplicate points, which Blender doesn't like seen_pts = set() pts = list() for _p in rcurve.Points: p = (_p.X, _p.Y, _p.Z, _p.W) if not p in seen_pts: pts.append(_p) seen_pts.add(p) N = len(pts) nurbs = bcurve.splines.new('NURBS') N = len(pts) # creating a new spline already adds one point, so add # here only N-1 points nurbs.points.add(N - 1) # if we have a rational curve we may need to adjust control points with their # weights. Otherwise we'll get completely weird curves in Blender. # dividing the CVs with their weights gives what we are looking for. if rcurve.IsRational: if rcurve.IsClosed: is_arc = True _pts = pts[:] pts = list() for _p in _pts: w = 1 / _p.W p3d = r3d.Point3d(_p.X, _p.Y, _p.Z) * w pts.append(r3d.Point4d(p3d.X, p3d.Y, p3d.Z, _p.W)) # add the CVs to the Blender NURBS curve for i in range(0, N): rpt = pts[i] nurbs.points[i].co = (rpt.X * scale, rpt.Y * scale, rpt.Z * scale, rpt.W) # set relevant properties nurbs.resolution_u = 12 nurbs.use_bezier_u = rcurve.IsRational # set to bezier when rational nurbs.use_endpoint_u = is_arc if is_arc else not rcurve.IsClosed nurbs.use_cyclic_u = rcurve.IsClosed nurbs.order_u = rcurve.Order # For curves we don't want V to be used # so set to 1 and False where applicable nurbs.resolution_v = 1 nurbs.use_bezier_v = False nurbs.use_endpoint_v = False nurbs.use_cyclic_v = False nurbs.order_v = 1 CONVERT[r3d.NurbsCurve] = import_nurbs_curve def point_to_vector(point) -> Vector: return Vector((point.X, point.Y, point.Z)) def import_arc(rcurve, bcurve, scale): nc_arc = rcurve.Arc.ToNurbsCurve() import_nurbs_curve(nc_arc, bcurve, scale, is_arc=True) CONVERT[r3d.ArcCurve] = import_arc def import_polycurve(rcurve, bcurve, scale): for seg in range(rcurve.SegmentCount): segcurve = rcurve.SegmentCurve(seg) if type(segcurve) in CONVERT.keys(): CONVERT[type(segcurve)](segcurve, bcurve, scale) CONVERT[r3d.PolyCurve] = import_polycurve def import_curve(context, ob, name, scale, options): og = ob.Geometry curve_data = context.blend_data.curves.new(name, type="CURVE") if type(og) in CONVERT.keys(): curve_data.dimensions = '3D' curve_data.resolution_u = 2 if type(og) in (r3d.PolylineCurve, r3d.LineCurve) else 12 CONVERT[type(og)](og, curve_data, scale) return curve_data ================================================ FILE: import_3dm/converters/groups.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import utils def handle_groups(context,attr,toplayer, import_nested_groups): #check if object is member of one or more groups if attr.GroupCount>0: group_list = attr.GetGroupList() group_prefix = "Group_" group_col_id = "Groups" #if theres still no main collection to hold all groups, create one and link it to toplayer if not group_col_id in context.blend_data.collections: gcol = context.blend_data.collections.new(name=group_col_id) toplayer.children.link(gcol) #loop through the group ids that the object belongs to, build a hierarchy and link the object to the lowest one for index, gid in enumerate(group_list): #build child group id and check if it exists, if it doesnt, add a new collection, if it does, use the existing one child_id = group_prefix + str(gid) if not child_id in context.blend_data.collections: ccol = context.blend_data.collections.new(name=child_id) else: ccol = context.blend_data.collections[child_id] #same as before, if there is a parent group, use it. if not, or if nesting is disable default to main group collection try: parent_id = group_prefix + str(group_list[index+1]) except Exception: parent_id = None if parent_id==None or not import_nested_groups: parent_id = group_col_id if not parent_id in context.blend_data.collections: pcol = context.blend_data.collections.new(name=parent_id) else: pcol = context.blend_data.collections[parent_id] #if child group is not yet linked to its parent, do so if not child_id in pcol.children: pcol.children.link(ccol) #get the last create blender object by its id last_obj=None for o in context.blend_data.objects: if o.get('rhid', None) == str(attr.Id): last_obj=o if last_obj: #if were in the lowest group of the hierarchy and nesting is enabled, link the object to the collection if index==0 and import_nested_groups: try: ccol.objects.link(last_obj) except Exception: pass #if nested import is disabled, link to every collection it belongs to elif not import_nested_groups: try: ccol.objects.link(last_obj) except Exception: pass ================================================ FILE: import_3dm/converters/instances.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import bpy import rhino3dm as r3d from mathutils import Matrix, Vector from math import sqrt from . import utils #TODO #tag collections and references with guids #test w/ more complex blocks and empty blocks #proper exception handling def handle_instance_definitions(context, model, toplayer, layername): """ Import instance definitions from rhino model as empty collections. These will later be populated to contain actual geometry. """ # TODO: here we need to get instance name and material used by this instance # meaning we need to also extrapolate either layer material or by parent # material. # if not layername in context.blend_data.collections: instance_col = context.blend_data.collections.new(name=layername) instance_col.hide_render = True instance_col.hide_viewport = True toplayer.children.link(instance_col) for idef in model.InstanceDefinitions: tags = utils.create_tag_dict(idef.Id, idef.Name, None, None, True) idef_col=utils.get_or_create_iddata(context.blend_data.collections, tags, None ) try: instance_col.children.link(idef_col) except Exception: pass def _duplicate_collection(context : bpy.context, collection : bpy.types.Collection, newname : str): new_collection = bpy.context.blend_data.collections.new(name=newname) def _recurse_duplicate_collection(collection : bpy.types.Collection): for obj in collection.children: if type(obj.type) == bpy.types.Collection: pass else: new_obj = context.blend_data.objects.new(name=obj.name, object_data=obj.data) new_collection.objects.link(new_obj) for child in collection.children: new_child = bpy.context.blend_data.collections.new(name=child.name) new_collection.children.link(new_child) _recurse_duplicate_collection(child,new_child) def import_instance_reference(context : bpy.context, ob : r3d.File3dmObject, iref : bpy.types.Object, name : str, scale : float, options): # To be able to support ByParent material we need to add actual objects # instead of collection instances. That will allow us to add material slots # to instances and set them to 'OBJECT', which allows us to essentially # 'override' the material for the original mesh data tags = utils.create_tag_dict(ob.Geometry.ParentIdefId, "") iref.instance_type='COLLECTION' iref.instance_collection = utils.get_or_create_iddata(context.blend_data.collections, tags, None) #instance_definition = utils.get_or_create_iddata(context.blend_data.collections, tags, None) #iref.data = instance_definition.data xform=list(ob.Geometry.Xform.ToFloatArray(1)) xform=[xform[0:4],xform[4:8], xform[8:12], xform[12:16]] xform[0][3]*=scale xform[1][3]*=scale xform[2][3]*=scale iref.matrix_world = Matrix(xform) def populate_instance_definitions(context, model, toplayer, layername, options, scale): import_as_grid = options.get("import_instances_grid_layout",False) if import_as_grid: count = 0 columns = int(sqrt(len(model.InstanceDefinitions))) grid = options.get("import_instances_grid",False) *scale #for every instance definition fish out the instance definition objects and link them to their parent for idef in model.InstanceDefinitions: tags = utils.create_tag_dict(idef.Id, idef.Name, None, None, True) parent=utils.get_or_create_iddata(context.blend_data.collections, tags, None) objectids=idef.GetObjectIds() if import_as_grid: #calculate position offset to lay out block definitions in xy plane offset = Vector((count%columns * grid, (count-count%columns)/columns * grid, 0 )) parent.instance_offset = offset #this sets the offset for the collection instances (read: resets the origin) count +=1 for ob in context.blend_data.objects: for guid in objectids: if ob.get('rhid',None) == str(guid): try: parent.objects.link(ob) if import_as_grid: ob.location += offset #apply the previously calculated offset to all instance definition objects except Exception: pass ================================================ FILE: import_3dm/converters/layers.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from . import utils def handle_layers(context, model, toplayer, layerids, materials, update, import_hidden=False, layers_as_empties=False): """ In context read the Rhino layers from model then update the layerids dictionary passed in. Update materials dictionary with materials created for layer color. """ #setup main container to hold all layer collections layer_col_id="Layers" if not layer_col_id in context.blend_data.collections: layer_col = context.blend_data.collections.new(name=layer_col_id) try: toplayer.children.link(layer_col) except Exception: pass else: #If "Layers" collection is in place, we assume the plugin had imported 3dm before layer_col = context.blend_data.collections[layer_col_id] # build lookup table for LayerTable index # from GUID, create collection for each # layer for lid, l in enumerate(model.Layers): if not l.Visible and not import_hidden: continue tags = utils.create_tag_dict(l.Id, l.Name) if layers_as_empties: lcol = utils.get_or_create_iddata(context.blend_data.objects, tags, None, use_none=True) else: lcol = utils.get_or_create_iddata(context.blend_data.collections, tags, None) layerids[str(l.Id)] = (lid, lcol) #utils.tag_data(layerids[str(l.Id)][1], l.Id, l.Name) # second pass so we can link layers to each other for l in model.Layers: # link up layers to their parent layers if str(l.ParentLayerId) in layerids: parentlayer = layerids[str(l.ParentLayerId)][1] try: if layers_as_empties: # set the parent child = layerids[str(l.Id)][1] child.parent = parentlayer # and also link to Layers collection layer_col.objects.link(child) else: parentlayer.children.link(layerids[str(l.Id)][1]) except Exception: pass # or to the top collection if no parent layer was found else: try: if layers_as_empties: layer_col.objects.link(layerids[str(l.Id)][1]) else: layer_col.children.link(layerids[str(l.Id)][1]) except Exception: pass ================================================ FILE: import_3dm/converters/material.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import binascii import struct import bpy import rhino3dm as r3d from bpy_extras.node_shader_utils import ShaderWrapper, PrincipledBSDFWrapper from bpy_extras.node_shader_utils import rgba_to_rgb, rgb_to_rgba from . import utils from . import rdk_manager from pathlib import Path, PureWindowsPath, PurePosixPath import base64 import tempfile import uuid import os from typing import Any, Tuple ### default Rhino material name DEFAULT_RHINO_MATERIAL = "Rhino Default Material" DEFAULT_TEXT_MATERIAL = "Rhino Default Text" DEFAULT_RHINO_MATERIAL_ID = uuid.UUID("00000000-ABCD-EF01-2345-000000000000") DEFAULT_RHINO_TEXT_MATERIAL_ID = uuid.UUID("00000000-ABCD-EF01-6789-000000000000") #### material hashing functions _black = (0, 0, 0, 1.0) _white = (0.2, 1.0, 0.6, 1.0) def Bbytes(b): """ Return bytes representation of boolean """ return struct.pack("?", b) def Fbytes(f): """ Return bytes representation of float """ return struct.pack("f", f) def Cbytes(c): """ Return bytes representation of Color, a 4-tuple containing integers """ return struct.pack("IIII", *c) def tobytes(d): t = type(d) if t is bool: return Bbytes(d) if t is float: return Fbytes(d) if t is tuple and len(d) == 4: return Cbytes(d) def hash_color(C, crc): """ return crc from color C """ crc = binascii.crc32(tobytes(C), crc) return crc def hash_material(M): """ Hash a rhino3dm.Material. A CRC32 is calculated using the material name and data that affects render results """ crc = 13 crc = binascii.crc32(bytes(M.Name, "utf-8")) crc = hash_color(M.DiffuseColor, crc) crc = hash_color(M.EmissionColor, crc) crc = hash_color(M.ReflectionColor, crc) crc = hash_color(M.SpecularColor, crc) crc = hash_color(M.TransparentColor, crc) crc = binascii.crc32(tobytes(M.DisableLighting), crc) crc = binascii.crc32(tobytes(M.FresnelIndexOfRefraction), crc) crc = binascii.crc32(tobytes(M.FresnelReflections), crc) crc = binascii.crc32(tobytes(M.IndexOfRefraction), crc) crc = binascii.crc32(tobytes(M.ReflectionGlossiness), crc) crc = binascii.crc32(tobytes(M.Reflectivity), crc) crc = binascii.crc32(tobytes(M.RefractionGlossiness), crc) crc = binascii.crc32(tobytes(M.Shine), crc) crc = binascii.crc32(tobytes(M.Transparency), crc) return crc def srgb_eotf(srgb_color: Tuple[float, float, float, float]) -> Tuple[float, float, float, float]: # sRGB piece-wise electro optical transfer function # also known as "sRGB to linear" # assuming Rhino uses this instead of pure 2.2 gamma function def cc(value): if value <= 0.04045: return value / 12.92 else: return ((value + 0.055) / 1.055) ** 2.4 linear_color = tuple(cc(x) for x in srgb_color) return linear_color def get_color_field(rm : r3d.RenderMaterial, field_name : str) -> Tuple[float, float, float, float]: """ Get a color field from a rhino3dm.RenderMaterial """ colstr = rm.GetParameter(field_name) if not colstr: return _white coltup = tuple(float(f) for f in colstr.split(",")) # convert to tuple of floats return srgb_eotf(coltup) def get_float_field(rm : r3d.RenderMaterial, field_name : str) -> float: """ Get a float field from a rhino3dm.RenderMaterial """ fl = rm.GetParameter(field_name) if not fl: #print(f"No float field found {field_name}") return 0.0 return float(fl) def get_bool_field(rm : r3d.RenderMaterial, field_name : str) -> bool: """ Get a boolean field from a rhino3dm.RenderMaterial """ b = rm.GetParameter(field_name) if not b: #print(f"No bool field found {field_name}") return False return bool(b) def hash_rendermaterial(M : r3d.RenderMaterial): """ Hash a rhino3dm.Material. A CRC32 is calculated using the material name and data that affects render results """ crc = 13 crc = binascii.crc32(bytes(M.Name, "utf-8")) crc = binascii.crc32(bytes(M.GetParameter("pbr-base-color"), "utf-8"), crc) crc = binascii.crc32(bytes(M.GetParameter("pbr-emission"), "utf-8"), crc) crc = binascii.crc32(bytes(M.GetParameter("pbr-subsurface_scattering-color"), "utf-8"), crc) crc = binascii.crc32(tobytes(get_float_field(M, "pbr-opacity")), crc) crc = binascii.crc32(tobytes(get_float_field(M, "pbr-opacity-ior")), crc) crc = binascii.crc32(tobytes(get_float_field(M, "pbr-opacity-roughness")), crc) crc = binascii.crc32(tobytes(get_float_field(M, "pbr-roughness")), crc) crc = binascii.crc32(tobytes(get_float_field(M, "pbr-metallic")), crc) return crc def material_name(m): h = hash_material(m) return m.Name # + "~" + str(h) def rendermaterial_name(m): h = hash_rendermaterial(m) return m.Name #+ "~" + str(h) class PlasterWrapper(ShaderWrapper): NODES_LIST = ( "node_out", "node_diffuse_bsdf", "_node_texcoords", ) __slots__ = ( "material", *NODES_LIST ) NODES_LIST = ShaderWrapper.NODES_LIST + NODES_LIST def __init__(self, material): if bpy.app.version[0] < 5: super(PlasterWrapper, self).__init__(material, is_readonly=False, use_nodes=True) else: super(PlasterWrapper, self).__init__(material, is_readonly=False) def update(self): super(PlasterWrapper, self).update() tree = self.material.node_tree nodes = tree.nodes links = tree.links nodes.clear() node_out = nodes.new('ShaderNodeOutputMaterial') node_out.label = "Material Output" self._grid_to_location(1, 1, ref_node=node_out) self.node_out = node_out node_diffuse_bsdf = nodes.new('ShaderNodeBsdfDiffuse') node_diffuse_bsdf.label = "Diffuse BSDF" self._grid_to_location(0, 1, ref_node=node_diffuse_bsdf) links.new(node_diffuse_bsdf.outputs["BSDF"], self.node_out.inputs["Surface"]) self.node_diffuse_bsdf = node_diffuse_bsdf def base_color_get(self): if self.node_diffuse_bsdf is None: return self.material.diffuse_color return self.node_diffuse_bsdf.inputs["Color"].default_value def base_color_set(self, color): #color = rgb_to_rgba(color) self.material.diffuse_color = color if self.node_diffuse_bsdf is not None: self.node_diffuse_bsdf.inputs["Color"].default_value = color base_color = property(base_color_get, base_color_set) def paint_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): paint = PrincipledBSDFWrapper(blender_material, is_readonly = False) col = get_color_field(rhino_material, "color")[0:3] roughness = 1.0 - get_float_field(rhino_material, "reflectivity") paint.base_color = col paint.specular = 0.5 paint.roughness = roughness def plaster_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): plaster = PlasterWrapper(blender_material) col = get_color_field(rhino_material, "color") plaster.base_color = col def default_material(blender_material : bpy.types.Material): plaster = PlasterWrapper(blender_material) plaster.base_color = (0.9, 0.9, 0.9, 1.0) def default_text_material(blender_material : bpy.types.Material): plaster = PlasterWrapper(blender_material) plaster.base_color = (0.05, 0.05, 0.05, 1.0) def metal_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): metal = PrincipledBSDFWrapper(blender_material, is_readonly=False) col = get_color_field(rhino_material, "color")[0:3] roughness = get_float_field(rhino_material, "polish-amount") metal.base_color = col metal.metallic = 1.0 metal.roughness = roughness metal.transmission = 0.0 def glass_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): glass = PrincipledBSDFWrapper(blender_material, is_readonly=False) col = get_color_field(rhino_material, "color")[0:3] roughness = 1.0 - get_float_field(rhino_material, "clarity-amount") ior = get_float_field(rhino_material, "ior") glass.base_color = col glass.transmission = 1.0 glass.roughness = roughness glass.metallic = 0.0 glass.ior= ior def plastic_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): plastic = PrincipledBSDFWrapper(blender_material, is_readonly=False) col = get_color_field(rhino_material, "color")[0:3] roughness = 1.0 - get_float_field(rhino_material, "polish-amount") #roughness = 1.0 - get_float_field(rhino_material, "reflectivity") transparency = get_float_field(rhino_material, "transparency") plastic.base_color = col plastic.transmission = transparency plastic.roughness = roughness plastic.metallic = 0.0 plastic.ior= 1.5 def _get_blender_pbr_texture(pbr : PrincipledBSDFWrapper, field_name : str): if field_name == "pbr-base-color": return pbr.base_color_texture elif field_name == "pbr-roughness": return pbr.roughness_texture elif field_name == "pbr-metallic": return pbr.metallic_texture elif field_name == "pbr-specular": return pbr.specular_texture elif field_name == "pbr-opacity": return pbr.transmission_texture elif field_name == "pbr-alpha": return pbr.alpha_texture elif field_name == "pbr-emission": return pbr.emission_color_texture elif field_name == "pbr-emission-double-amount": return pbr.emission_strength_texture else: raise ValueError(f"Unknown field name {field_name}") def _get_blender_basic_texture(pbr : PrincipledBSDFWrapper, field_name : str): if field_name == "bitmap-texture": return pbr.base_color_texture else: raise ValueError(f"Unknown field name {field_name}") def handle_pbr_texture(rhino_material : r3d.RenderMaterial, pbr : PrincipledBSDFWrapper, field_name : str): rhino_tex = rhino_material.FindChild(field_name) if rhino_tex: fp = _name_from_embedded_filepath(rhino_tex.FileName) use_alpha = get_bool_field(rhino_tex, "use-alpha-channel") if fp in _efps.keys(): pbr_tex = _get_blender_pbr_texture(pbr, field_name) img = _efps[fp] pbr_tex.node_image.image = img if use_alpha and field_name in ("pbr-base-color", "diffuse"): pbr.material.node_tree.links.new(pbr_tex.node_image.outputs['Alpha'], pbr.node_principled_bsdf.inputs['Alpha']) else: print(f"Image {fp} not found in Blender") def handle_basic_texture(rhino_material : r3d.RenderMaterial, pbr : PrincipledBSDFWrapper, field_name : str): rhino_tex = rhino_material.FindChild(field_name) if rhino_tex: fp = _name_from_embedded_filepath(rhino_tex.FileName) if fp in _efps.keys(): pbr_tex = _get_blender_basic_texture(pbr, field_name) img = _efps[fp] pbr_tex.node_image.image = img else: print(f"Image {fp} not found in Blender") def pbr_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): pbr = PrincipledBSDFWrapper(blender_material, is_readonly=False) refl = get_float_field(rhino_material, "pbr-metallic") transp = 1.0 - get_float_field(rhino_material, "pbr-opacity") ior = get_float_field(rhino_material, "pbr-opacity-ior") roughness = get_float_field(rhino_material, "pbr-roughness") transrough = get_float_field(rhino_material, "pbr-opacity-roughness") spec = get_float_field(rhino_material, "pbr-specular") alpha = get_float_field(rhino_material, "pbr-alpha") basecol = get_color_field(rhino_material, "pbr-base-color") emission_color = get_color_field(rhino_material, "pbr-emission") emission_amount = get_float_field(rhino_material, "emission-multiplier") pbr.base_color = basecol[0:3] pbr.metallic = refl pbr.transmission = transp pbr.ior = ior pbr.roughness = roughness pbr.specular = spec pbr.emission_color = emission_color[0:3] pbr.emission_strength = emission_amount pbr.alpha = alpha if bpy.app.version[0] < 4: pbr.node_principled_bsdf.inputs[16].default_value = transrough handle_pbr_texture(rhino_material, pbr, "pbr-base-color") handle_pbr_texture(rhino_material, pbr, "pbr-metallic") handle_pbr_texture(rhino_material, pbr, "pbr-roughness") handle_pbr_texture(rhino_material, pbr, "pbr-specular") handle_pbr_texture(rhino_material, pbr, "pbr-opacity") handle_pbr_texture(rhino_material, pbr, "pbr-alpha") handle_pbr_texture(rhino_material, pbr, "pbr-emission") handle_pbr_texture(rhino_material, pbr, "emission-multiplier") def rcm_basic_material(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): # first version with just simple pbr node. Can do something more elaborate later pbr = PrincipledBSDFWrapper(blender_material, is_readonly=False) base_color = get_color_field(rhino_material, "diffuse") trans_color = get_color_field(rhino_material, "transparency-color") trans_color = get_color_field(rhino_material, "reflectivity-color") fresnel_enabled = get_bool_field(rhino_material, "fresnel-enabled") transparency = get_float_field(rhino_material, "transparency") reflectivity = get_float_field(rhino_material, "reflectivity") ior = get_float_field(rhino_material, "ior") roughness = 1.0 - get_float_field(rhino_material, "polish-amount") pbr.specular = 0.5 if transparency > 0.0: base_color = trans_color else: pbr.base_color = base_color[0:3] pbr.roughness = roughness if reflectivity > 0.0 and fresnel_enabled: pbr.metallic = reflectivity pbr.transmission = transparency pbr.ior = ior handle_basic_texture(rhino_material, pbr, "bitmap-texture") def not_yet_implemented(rhino_material : r3d.RenderMaterial, blender_material : bpy.types.Material): paint = PlasterWrapper(blender_material) paint.base_color = (1.0, 0.0, 1.0, 1.0) material_handlers = { 'rdk-paint-material': paint_material, 'rdk-metal-material': metal_material, 'rdk-plaster-material': plaster_material, 'rdk-glass-material': glass_material, 'rdk-plastic-material': plastic_material, 'rcm-basic-material': rcm_basic_material, '5a8d7b9b-cdc9-49de-8c16-2ef64fb097ab': pbr_material, } def harvest_from_rendercontent(model : r3d.File3dm, mat : r3d.RenderMaterial, blender_material : bpy.types.Material): if bpy.app.version[0] < 5: blender_material.use_nodes = True typeName = mat.TypeName material_handler = material_handlers.get(typeName, not_yet_implemented) material_handler(mat, blender_material) _model = None _efps = None def _name_from_embedded_filepath(efp : str) -> str: efpath = PureWindowsPath(efp) if not efpath.drive: efpath = PurePosixPath(efp) return efpath.name def handle_embedded_files(model : r3d.File3dm): global _model, _efps _model = model _efps = dict() for rhino_embedded_filename in _model.EmbeddedFilePaths(): if rhino_embedded_filename in _efps.keys(): continue encoded_img = _model.GetEmbeddedFileAsBase64(rhino_embedded_filename) decoded_img = base64.b64decode(encoded_img) ef_name = _name_from_embedded_filepath(rhino_embedded_filename) with tempfile.NamedTemporaryFile(delete=False) as tmpf: tmpf.write(decoded_img) blender_image = bpy.context.blend_data.images.load(tmpf.name, check_existing=True) blender_image.name = ef_name blender_image.pack() _efps[ef_name] = blender_image tmpfpath = tmpf.name try: tmpf.close() os.unlink(tmpfpath) except RuntimeError: pass def handle_materials(context, model : r3d.File3dm, materials, update): """ """ handle_embedded_files(model) if DEFAULT_RHINO_MATERIAL not in materials: tags = utils.create_tag_dict(DEFAULT_RHINO_MATERIAL_ID, DEFAULT_RHINO_MATERIAL) blmat = utils.get_or_create_iddata(context.blend_data.materials, tags, None) default_material(blmat) materials[DEFAULT_RHINO_MATERIAL] = blmat if DEFAULT_TEXT_MATERIAL not in materials: tags = utils.create_tag_dict(DEFAULT_RHINO_TEXT_MATERIAL_ID, DEFAULT_TEXT_MATERIAL) blmat = utils.get_or_create_iddata(context.blend_data.materials, tags, None) default_text_material(blmat) materials[DEFAULT_TEXT_MATERIAL] = blmat for mat in model.Materials: if not mat.PhysicallyBased: mat.ToPhysicallyBased() m = model.RenderContent.FindId(mat.RenderMaterialInstanceId) if not m: continue matname = rendermaterial_name(m) if matname not in materials: tags = utils.create_tag_dict(m.Id, m.Name) blmat = utils.get_or_create_iddata(context.blend_data.materials, tags, None) if update: harvest_from_rendercontent(model, m, blmat) materials[matname] = blmat ================================================ FILE: import_3dm/converters/pointcloud.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import rhino3dm as r3d from . import utils def import_pointcloud(context, ob, name, scale, options): og = ob.Geometry oa = ob.Attributes # add points as mesh vertices # The following line crashes. Seems rhino3dm does not like iterating over pointclouds. #vertices = [(p.X * scale, p.Y * scale, p.Z * scale) for p in og] vertices = [(og[v].X * scale, og[v].Y * scale, og[v].Z * scale) for v in range(og.Count)] pointcloud = context.blend_data.meshes.new(name=name) pointcloud.from_pydata(vertices, [], []) return pointcloud ================================================ FILE: import_3dm/converters/rdk_manager.py ================================================ import rhino3dm as r3d import xml.etree.ElementTree as ET class RdkManager(): def __init__(self, document : r3d.File3dm) -> None: self.doc = document self.rdkxml = ET.fromstring(self.doc.RdkXml()) self.mgr = self.rdkxml.find("render-content-manager-document") self.materials_xml = self.mgr.find("material-section") self.environments_xml = self.mgr.find("environment-section") self.textures_xml = self.mgr.find("texture-section") def get_materials(self): materials = [] for material in self.materials_xml.findall("material"): rm = r3d.RenderMaterial() rm.SetXML(ET.tostring(material, encoding="utf-8").decode()) materials.append(rm) return materials ================================================ FILE: import_3dm/converters/render_mesh.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import bpy import rhino3dm as r3d from . import utils import bpy import bmesh import bpy.app import traceback def import_render_mesh(context, ob, name, scale, options): # concatenate all meshes from all (brep) faces, # adjust vertex indices for faces accordingly # first get all render meshes og = ob.Geometry oa = ob.Attributes needs_welding = options.get("merge_by_distance", False) msh_tex = list() if og.ObjectType == r3d.ObjectType.Extrusion: msh = [og.GetMesh(r3d.MeshType.Any)] elif og.ObjectType == r3d.ObjectType.Mesh: msh = [og] elif og.ObjectType == r3d.ObjectType.SubD: msh = [r3d.Mesh.CreateFromSubDControlNet(og, False)] msh_tex = [r3d.Mesh.CreateFromSubDControlNet(og, True)] elif og.ObjectType == r3d.ObjectType.Brep: msh = [og.Faces[f].GetMesh(r3d.MeshType.Any) for f in range(len(og.Faces)) if type(og.Faces[f])!=list] fidx = 0 faces = [] vertices = [] coords = [] vcls = [] # now add all faces and vertices to the main lists for m in msh: if not m: continue faces.extend([list(map(lambda x: x + fidx, m.Faces[f])) for f in range(len(m.Faces))]) # Rhino always uses 4 values to describe faces, which can lead to # invalid faces in Blender. Tris will have a duplicate index for the 4th # value. for f in faces: if f[-1] == f[-2]: del f[-1] fidx = fidx + len(m.Vertices) vertices.extend([(m.Vertices[v].X * scale, m.Vertices[v].Y * scale, m.Vertices[v].Z * scale) for v in range(len(m.Vertices))]) coords.extend([(m.TextureCoordinates[v].X, m.TextureCoordinates[v].Y) for v in range(len(m.TextureCoordinates))]) vcls.extend((m.VertexColors[v][0], m.VertexColors[v][1], m.VertexColors[v][2], m.VertexColors[v][3]) for v in range(len(m.VertexColors))) tags = utils.create_tag_dict(oa.Id, oa.Name) mesh = utils.get_or_create_iddata(context.blend_data.meshes, tags, None) mesh.clear_geometry() mesh.from_pydata(vertices, [], faces, shade_flat=False) coords_tex = list() for mt in msh_tex: if not mt: continue coords_tex.extend([(mt.TextureCoordinates[v].X, mt.TextureCoordinates[v].Y) for v in range(len(mt.TextureCoordinates))]) if mesh.loops: # and len(coords) == len(vertices): # todo: # * check for multiple mappings and handle them # * get mapping name (missing from rhino3dm) # * rhino assigns a default mapping to unmapped objects, so if nothing is specified, this will be imported #create a new uv_layer and copy texcoords from input mesh mesh.uv_layers.new(name="RhinoUVMap") if sum(len(x) for x in faces) == len(mesh.uv_layers["RhinoUVMap"].data): uvl = mesh.uv_layers["RhinoUVMap"].data[:] for loop in mesh.loops: try: if coords_tex: uvl[loop.index].uv = coords_tex[loop.index] elif coords: # print(loop.index, loop.vertex_index, len(uvl), len(coords)) uvl[loop.index].uv = coords[loop.vertex_index] else: print("no tex coords") except IndexError: print(name) print(traceback.format_exc()) mesh.validate() mesh.update() else: #in case there was a data mismatch, cleanup the created layer mesh.uv_layers.remove(mesh.uv_layers["RhinoUVMap"]) if len(vcls) == len(vertices): mesh.attributes.new("RhinoColor", "FLOAT_COLOR", "POINT") rcl = mesh.attributes["RhinoColor"] for i in range(len(vcls)): vcl = vcls[i] rcl.data[i].color = (vcl[0] / 255.0, vcl[1] / 255.0, vcl[2] / 255.0, vcl[3] / 255.0) mesh.validate() mesh.update() if needs_welding: bm = bmesh.new() bm.from_mesh(mesh) merge_distance = options.get("merge_distance", 0.0001) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=merge_distance) bm.to_mesh(mesh) bm.free() if bpy.app.version >= (4, 1): mesh.set_sharp_from_angle(angle=0.523599) # 30deg else: mesh.use_auto_smooth = True # done, now add object to blender return mesh ================================================ FILE: import_3dm/converters/utils.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # *** data tagging import bpy import uuid import rhino3dm as r3d from mathutils import Matrix from typing import Any, Dict def tag_data( idblock : bpy.types.ID, tag_dict: Dict[str, Any] ) -> None: """ Given a Blender data idblock tag it with the id an name given using custom properties. These are used to track the relationship with original Rhino data. """ guid = tag_dict.get('rhid', None) name = tag_dict.get('rhname', None) matid = tag_dict.get('rhmatid', None) parentid = tag_dict.get('rhparentid', None) is_idef = tag_dict.get('rhidef', False) idblock['rhid'] = str(guid) idblock['rhname'] = name idblock['rhmatid'] = str(matid) idblock['rhparentid'] = str(parentid) idblock['rhidef'] = is_idef idblock['rhmat_from_object'] = tag_dict.get('rhmat_from_object', True) def create_tag_dict( guid : uuid.UUID, name : str, matid : uuid.UUID = None, parentid : uuid.UUID = None, is_idef : bool = False, mat_from_object : bool = True, ) -> Dict[str, Any]: """ Create a dictionary with the tag data. This can be used to pass to the tag_dict and get_or_create_iddata functions. guid and name are mandatory. """ return { 'rhid': guid, 'rhname': name, 'rhmatid': matid, 'rhparentid': parentid, 'rhidef': is_idef, 'rhmat_from_object': mat_from_object } all_dict = dict() def clear_all_dict() -> None: global all_dict all_dict = dict() def reset_all_dict(context : bpy.types.Context) -> None: global all_dict all_dict = dict() bases = [ context.blend_data.objects, context.blend_data.cameras, context.blend_data.lights, context.blend_data.meshes, context.blend_data.materials, context.blend_data.collections, context.blend_data.curves ] for base in bases: t = repr(base).split(',')[1] if t in all_dict: dct = all_dict[t] else: dct = dict() all_dict[t] = dct for item in base: rhid = item.get('rhid', None) if rhid: dct[rhid] = item def get_dict_for_base(base : bpy.types.bpy_prop_collection) -> Dict[str, bpy.types.ID]: global all_dict t = repr(base).split(',')[1] if t not in all_dict: pass return all_dict[t] def get_or_create_iddata( base : bpy.types.bpy_prop_collection, tag_dict: Dict[str, Any], obdata : bpy.types.ID, use_none : bool = False ) -> bpy.types.ID: """ Get an iddata. The tag_dict collection should contain a guid if the goal is to find an existing item. If an object with given guid is found in this .blend use that. Otherwise new up one with base.new, potentially with obdata if that is set If obdata is given then the found object data will be set to that. """ founditem : bpy.types.ID = None guid = tag_dict.get('rhid', None) name = tag_dict.get('rhname', None) matid = tag_dict.get('rhmatid', None) parentid = tag_dict.get('rhparentid', None) is_idef = tag_dict.get('rhidef', False) dct = get_dict_for_base(base) if guid is not None: strguid = str(guid) if strguid in dct: founditem = dct[strguid] if founditem: theitem = founditem theitem['rhname'] = name if obdata and type(theitem) != type(obdata): theitem.data = obdata else: if obdata or use_none: theitem = base.new(name=name, object_data=obdata) else: theitem = base.new(name=name) if guid is not None: strguid = str(guid) dct[strguid] = theitem tag_data(theitem, tag_dict) return theitem def matrix_from_xform(xform : r3d.Transform): m = Matrix( ((xform.M00, xform.M01, xform.M02, xform.M03), (xform.M10, xform.M11, xform.M12, xform.M13), (xform.M20, xform.M21, xform.M22, xform.M23), (xform.M30, xform.M31, xform.M32, xform.M33)) ) return m ================================================ FILE: import_3dm/converters/views.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import rhino3dm as r3d from . import utils from mathutils import Matrix def handle_view(context, view, name, scale): vp = view.Viewport # Construct transformation matrix mat = Matrix([ [vp.CameraX.X, vp.CameraX.Y, vp.CameraX.Z, 0], [vp.CameraY.X, vp.CameraY.Y, vp.CameraY.Z, 0], [vp.CameraZ.X, vp.CameraZ.Y, vp.CameraZ.Z, 0], [0,0,0,1]]) mat.invert() mat[0][3] = vp.CameraLocation.X * scale mat[1][3] = vp.CameraLocation.Y * scale mat[2][3] = vp.CameraLocation.Z * scale lens = vp.Camera35mmLensLength tags = utils.create_tag_dict(None, name) blcam = utils.get_or_create_iddata(context.blend_data.cameras, tags, None) # Set camera to perspective or parallel if vp.IsPerspectiveProjection: blcam.type = "PERSP" blcam.lens = lens blcam.sensor_width = 36.0 elif vp.IsParallelProjection: blcam.type = "ORTHO" frustum = vp.GetFrustum() blcam.ortho_scale = (frustum['right'] - frustum['left']) * scale # Link camera data to new object blobj = utils.get_or_create_iddata(context.blend_data.objects, tags, blcam) blobj.matrix_world = mat # Return new camera return blobj def handle_views(context, model, layer, views, layer_name, scale): collection_is_new = False if layer_name in context.blend_data.collections: viewLayer = context.blend_data.collections[layer_name] else: viewLayer = context.blend_data.collections.new(name=layer_name) collection_is_new = True for v in views: camera = handle_view(context, v, "RhinoView_" + v.Name, scale) try: viewLayer.objects.link(camera) except Exception: pass if collection_is_new: layer.children.link(viewLayer) ================================================ FILE: import_3dm/read3dm.py ================================================ # MIT License # Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os.path import bpy import sys import os from pathlib import Path from typing import Any, Dict, Set def modules_path(): # set up addons/modules under the user # script path. Here we'll install the # dependencies modulespath = os.path.normpath( os.path.join( bpy.utils.script_path_user(), "addons", "modules" ) ) if not os.path.exists(modulespath): os.makedirs(modulespath) # set user modules path at beginning of paths for earlier hit if sys.path[1] != modulespath: sys.path.insert(1, modulespath) return modulespath modules_path() import rhino3dm as r3d from . import converters def create_or_get_top_layer(context, filepath): top_collection_name = Path(filepath).stem if top_collection_name in context.blend_data.collections.keys(): toplayer = context.blend_data.collections[top_collection_name] else: toplayer = context.blend_data.collections.new(name=top_collection_name) return toplayer def read_3dm( context : bpy.types.Context, filepath : str, options : Dict[str, Any] ) -> Set[str]: converters.initialize(context) # Parse options import_views = options.get("import_views", False) import_annotations = options.get("import_annotations", False) import_curves = options.get("import_curves", False) import_pointset = options.get("import_pointset", False) import_meshes = options.get("import_meshes", False) import_subd = options.get("import_subd", False) import_extrusions = options.get("import_extrusions", False) import_brep = options.get("import_brep", False) import_named_views = options.get("import_named_views", False) import_hidden_objects = options.get("import_hidden_objects", False) import_hidden_layers = options.get("import_hidden_layers", False) import_layers_as_empties = options.get("import_layers_as_empties", False) import_groups = options.get("import_groups", False) import_nested_groups = options.get("import_nested_groups", False) import_instances = options.get("import_instances",False) update_materials = options.get("update_materials", False) model = None try: model = r3d.File3dm.Read(filepath) except: print("Failed to import .3dm model: {}".format(filepath)) return {'CANCELLED'} # place model in context so we can access it when we need to # find data from different tables, like for instance dimension # styles while working on annotation import. options["rh_model"] = model toplayer = create_or_get_top_layer(context, filepath) # Get proper scale for conversion scale = r3d.UnitSystem.UnitScale(model.Settings.ModelUnitSystem, r3d.UnitSystem.Meters) / context.scene.unit_settings.scale_length layerids = {} materials = {} # Import Views and NamedViews if import_views: converters.handle_views(context, model, toplayer, model.Views, "Views", scale) if import_named_views: converters.handle_views(context, model, toplayer, model.NamedViews, "NamedViews", scale) # Handle materials converters.handle_materials(context, model, materials, update_materials) # Handle layers converters.handle_layers(context, model, toplayer, layerids, materials, update_materials, import_hidden_layers, import_layers_as_empties) materials[converters.DEFAULT_RHINO_MATERIAL] = None #build skeletal hierarchy of instance definitions as collections (will be populated by object importer) if import_instances: converters.handle_instance_definitions(context, model, toplayer, "Instance Definitions") # Handle objects ob : r3d.File3dmObject = None for ob in model.Objects: og : r3d.GeometryBase = ob.Geometry # Skip unsupported object types early if og.ObjectType not in converters.RHINO_TYPE_TO_IMPORT and og.ObjectType != r3d.ObjectType.InstanceReference: print("Unsupported object type: {}".format(og.ObjectType)) continue if not import_curves and og.ObjectType == r3d.ObjectType.Curve: continue if not import_annotations and og.ObjectType == r3d.ObjectType.Annotation: continue if not import_pointset and og.ObjectType == r3d.ObjectType.PointSet: continue if not import_brep and og.ObjectType == r3d.ObjectType.Brep: continue if not import_extrusions and og.ObjectType == r3d.ObjectType.Extrusion: continue if not import_subd and og.ObjectType == r3d.ObjectType.SubD: continue if not import_meshes and og.ObjectType == r3d.ObjectType.Mesh: continue # Check object visibility attr = ob.Attributes if not attr.Visible and not import_hidden_objects: continue # Check object layer visibility rhinolayer = model.Layers.FindIndex(attr.LayerIndex) if not rhinolayer.Visible and not import_hidden_layers: continue # Create object name if none exists or it is an empty string. # Otherwise use the name from the 3dm file. if attr.Name == "" or attr.Name is None: object_name = str(og.ObjectType).split(".")[1]+" " + str(attr.Id) else: object_name = attr.Name # Get render material, either from object. or if MaterialSource # is set to MaterialFromLayer, from the layer. mat_index = attr.MaterialIndex if attr.MaterialSource == r3d.ObjectMaterialSource.MaterialFromLayer: mat_index = rhinolayer.RenderMaterialIndex rhino_material = model.Materials.FindIndex(mat_index) # Get material name. In case of the Rhino default material use # DEFAULT_RHINO_MATERIAL, otherwise compute a name from the material # so that it is fit for Blender usage. if mat_index == -1 or rhino_material.Name == "": matname = converters.material.DEFAULT_RHINO_MATERIAL else: matname = converters.material_name(rhino_material) # Handle object view color if ob.Attributes.ColorSource == r3d.ObjectColorSource.ColorFromLayer: view_color = rhinolayer.Color else: view_color = ob.Attributes.ObjectColor # Get the corresponding Blender material based on the material name # from the material dictionary if matname not in materials.keys(): matname = converters.material.DEFAULT_RHINO_MATERIAL blender_material = materials[matname] if og.ObjectType == r3d.ObjectType.Annotation: blender_material = materials[converters.material.DEFAULT_TEXT_MATERIAL] # Fetch layer layer = layerids[str(rhinolayer.Id)][1] if og.ObjectType==r3d.ObjectType.InstanceReference and import_instances: object_name = model.InstanceDefinitions.FindId(og.ParentIdefId).Name # Convert object converters.convert_object(context, ob, object_name, layer, blender_material, view_color, scale, options) if import_groups: converters.handle_groups(context,attr,toplayer,import_nested_groups) if import_instances: converters.populate_instance_definitions(context, model, toplayer, "Instance Definitions", options, scale) # finally link in the container collection (top layer) into the main # scene collection. if toplayer.name not in context.scene.collection.children: context.scene.collection.children.link(toplayer) if bpy.app.version[0] < 4: bpy.ops.object.shade_smooth({'selected_editable_objects': toplayer.all_objects}) else: # set the active object on the viewlayer to none as that is checked by shade smooth active_object = bpy.context.view_layer.objects.active bpy.context.view_layer.objects.active = None with context.temp_override(selected_editable_objects=toplayer.all_objects): bpy.ops.object.shade_smooth() bpy.context.view_layer.objects.active = active_object converters.cleanup() return {'FINISHED'} ================================================ FILE: requirements.txt ================================================ rhino3dm>=8.6.0 ================================================ FILE: test/pytest.ini_example ================================================ [pytest] blender-executable = C:/Program Files/Blender Foundation/Blender 4.2/blender.exe pythonpath = C:/Users/USERNAME/AppData/Roaming/Blender Foundation/Blender/4.2/extensions/REPOSITORY_NAME/import_3dm addopts = --cov "C:/Users/USERNAME/AppData/Roaming/Blender Foundation/Blender/4.2/extensions/REPOSITORY_NAME/import_3dm" --cov-report html ================================================ FILE: test/pytest_setup.md ================================================ ## pytest setup local python needed install pytest and pytest-blender install pip in blender (on windows/blender 4.2: ` & 'C:\Program Files\Blender Foundation\Blender 4.2\4.2\python\bin\python.exe' -m ensurepip`) then install pytest in blender's python (on windows/blender 4.2: ` & 'C:\Program Files\Blender Foundation\Blender 4.2\4.2\python\bin\python.exe' -m pip install -r test-requirements.txt`) also see https://pypi.org/project/pytest-blender/ for more info setup pytest.ini, you can use pytest.ini_example as a starting point, change the file paths accordingly, be aware that this uses the paths blender installs extensions to since blender 4.2 it also includes options for code coverage, pytest-cov needed in local python and blenders python ## unittest setup local python needed run `py -m pytest` ================================================ FILE: test/test_import_3dm.py ================================================ #!python3 import pytest import bpy import addon_utils testfiles = [ "units/boxes_in_cm.3dm", "units/boxes_in_ft.3dm", "units/boxes_in_in.3dm", ] # ############################################################################## # # autouse fixtures # ############################################################################## # @pytest.fixture(scope="session", autouse=True) def enable_addon(): addon_utils.enable("import_3dm") # ############################################################################## # # test cases # ############################################################################## # @pytest.mark.parametrize("filepath", testfiles) def test_create_article(filepath): bpy.ops.import_3dm.some_data(filepath=filepath)