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)
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
SYMBOL INDEX (98 symbols across 15 files)
FILE: import_3dm/__init__.py
class Import3dm (line 51) | class Import3dm(Operator, ImportHelper):
method poll (line 225) | def poll(cls, context: bpy.types.Context):
method execute (line 228) | def execute(self, context : bpy.types.Context):
method draw (line 233) | def draw(self, _ : bpy.types.Context):
method invoke (line 296) | def invoke(self, context, event):
class IO_FH_3dm_import (line 301) | class IO_FH_3dm_import(bpy.types.FileHandler):
method poll_drop (line 308) | def poll_drop(cls, context):
function menu_func_import (line 315) | def menu_func_import(self, _ : bpy.types.Context):
function register (line 319) | def register():
function unregister (line 325) | def unregister():
FILE: import_3dm/converters/__init__.py
function initialize (line 59) | def initialize(
function cleanup (line 64) | def cleanup() -> None:
function convert_object (line 70) | def convert_object(
FILE: import_3dm/converters/annotation.py
class PartType (line 34) | class PartType(IntEnum):
class Arrow (line 41) | class Arrow(IntEnum):
function _arrowtype_from_arrow (line 48) | def _arrowtype_from_arrow(dimstyle : r3d.DimensionStyle, arrow : Arrow):
function _negate_vector3d (line 57) | def _negate_vector3d(v : r3d.Vector3d):
function _rotate_plane_to_line (line 60) | def _rotate_plane_to_line(plane : r3d.Plane, line : r3d.Line, addangle=0...
function _add_arrow (line 70) | def _add_arrow(dimstyle : r3d.DimensionStyle, pt : PartType, plane : r3d...
function _populate_line (line 102) | def _populate_line(dimstyle : r3d.DimensionStyle, pt : PartType, plane :...
function _add_text (line 125) | def _add_text(dimstyle : r3d.DimensionStyle, plane : r3d.Plane, bc, pt :...
function import_dim_linear (line 161) | def import_dim_linear(model, dimlin, bc, scale):
function import_radius (line 180) | def import_radius(model, dimrad, bc, scale):
function import_angular (line 198) | def import_angular(model, dimang, bc, scale):
function import_leader (line 269) | def import_leader(model, dimlead, bc, scale):
function import_text (line 287) | def import_text(model, textannotation, bc, scale):
function import_ordinate (line 296) | def import_ordinate(model, dimordinate, bc, scale):
function import_centermark (line 314) | def import_centermark(model, centermark, bc, scale):
function import_annotation (line 324) | def import_annotation(context, ob, name, scale, options):
FILE: import_3dm/converters/curve.py
function import_null (line 32) | def import_null(rcurve, bcurve, scale):
function import_line (line 37) | def import_line(rcurve, bcurve, scale):
function import_polyline (line 52) | def import_polyline(rcurve, bcurve, scale):
function import_nurbs_curve (line 70) | def import_nurbs_curve(rcurve, bcurve, scale, is_arc = False):
function point_to_vector (line 129) | def point_to_vector(point) -> Vector:
function import_arc (line 133) | def import_arc(rcurve, bcurve, scale):
function import_polycurve (line 140) | def import_polycurve(rcurve, bcurve, scale):
function import_curve (line 149) | def import_curve(context, ob, name, scale, options):
FILE: import_3dm/converters/groups.py
function handle_groups (line 24) | def handle_groups(context,attr,toplayer, import_nested_groups):
FILE: import_3dm/converters/instances.py
function handle_instance_definitions (line 36) | def handle_instance_definitions(context, model, toplayer, layername):
function _duplicate_collection (line 63) | def _duplicate_collection(context : bpy.context, collection : bpy.types....
function import_instance_reference (line 77) | def import_instance_reference(context : bpy.context, ob : r3d.File3dmObj...
function populate_instance_definitions (line 95) | def populate_instance_definitions(context, model, toplayer, layername, o...
FILE: import_3dm/converters/layers.py
function handle_layers (line 26) | def handle_layers(context, model, toplayer, layerids, materials, update,...
FILE: import_3dm/converters/material.py
function Bbytes (line 51) | def Bbytes(b):
function Fbytes (line 58) | def Fbytes(f):
function Cbytes (line 65) | def Cbytes(c):
function tobytes (line 72) | def tobytes(d):
function hash_color (line 82) | def hash_color(C, crc):
function hash_material (line 90) | def hash_material(M):
function srgb_eotf (line 114) | def srgb_eotf(srgb_color: Tuple[float, float, float, float]) -> Tuple[fl...
function get_color_field (line 128) | def get_color_field(rm : r3d.RenderMaterial, field_name : str) -> Tuple[...
function get_float_field (line 139) | def get_float_field(rm : r3d.RenderMaterial, field_name : str) -> float:
function get_bool_field (line 149) | def get_bool_field(rm : r3d.RenderMaterial, field_name : str) -> bool:
function hash_rendermaterial (line 159) | def hash_rendermaterial(M : r3d.RenderMaterial):
function material_name (line 178) | def material_name(m):
function rendermaterial_name (line 182) | def rendermaterial_name(m):
class PlasterWrapper (line 187) | class PlasterWrapper(ShaderWrapper):
method __init__ (line 202) | def __init__(self, material):
method update (line 208) | def update(self):
method base_color_get (line 228) | def base_color_get(self):
method base_color_set (line 233) | def base_color_set(self, color):
function paint_material (line 242) | def paint_material(rhino_material : r3d.RenderMaterial, blender_material...
function plaster_material (line 250) | def plaster_material(rhino_material : r3d.RenderMaterial, blender_materi...
function default_material (line 255) | def default_material(blender_material : bpy.types.Material):
function default_text_material (line 259) | def default_text_material(blender_material : bpy.types.Material):
function metal_material (line 263) | def metal_material(rhino_material : r3d.RenderMaterial, blender_material...
function glass_material (line 272) | def glass_material(rhino_material : r3d.RenderMaterial, blender_material...
function plastic_material (line 283) | def plastic_material(rhino_material : r3d.RenderMaterial, blender_materi...
function _get_blender_pbr_texture (line 296) | def _get_blender_pbr_texture(pbr : PrincipledBSDFWrapper, field_name : s...
function _get_blender_basic_texture (line 317) | def _get_blender_basic_texture(pbr : PrincipledBSDFWrapper, field_name :...
function handle_pbr_texture (line 323) | def handle_pbr_texture(rhino_material : r3d.RenderMaterial, pbr : Princi...
function handle_basic_texture (line 338) | def handle_basic_texture(rhino_material : r3d.RenderMaterial, pbr : Prin...
function pbr_material (line 349) | def pbr_material(rhino_material : r3d.RenderMaterial, blender_material :...
function rcm_basic_material (line 384) | def rcm_basic_material(rhino_material : r3d.RenderMaterial, blender_mate...
function not_yet_implemented (line 418) | def not_yet_implemented(rhino_material : r3d.RenderMaterial, blender_mat...
function harvest_from_rendercontent (line 432) | def harvest_from_rendercontent(model : r3d.File3dm, mat : r3d.RenderMate...
function _name_from_embedded_filepath (line 444) | def _name_from_embedded_filepath(efp : str) -> str:
function handle_embedded_files (line 450) | def handle_embedded_files(model : r3d.File3dm):
function handle_materials (line 482) | def handle_materials(context, model : r3d.File3dm, materials, update):
FILE: import_3dm/converters/pointcloud.py
function import_pointcloud (line 27) | def import_pointcloud(context, ob, name, scale, options):
FILE: import_3dm/converters/rdk_manager.py
class RdkManager (line 4) | class RdkManager():
method __init__ (line 5) | def __init__(self, document : r3d.File3dm) -> None:
method get_materials (line 13) | def get_materials(self):
FILE: import_3dm/converters/render_mesh.py
function import_render_mesh (line 33) | def import_render_mesh(context, ob, name, scale, options):
FILE: import_3dm/converters/utils.py
function tag_data (line 32) | def tag_data(
function create_tag_dict (line 53) | def create_tag_dict(
function clear_all_dict (line 78) | def clear_all_dict() -> None:
function reset_all_dict (line 82) | def reset_all_dict(context : bpy.types.Context) -> None:
function get_dict_for_base (line 106) | def get_dict_for_base(base : bpy.types.bpy_prop_collection) -> Dict[str,...
function get_or_create_iddata (line 113) | def get_or_create_iddata(
function matrix_from_xform (line 156) | def matrix_from_xform(xform : r3d.Transform):
FILE: import_3dm/converters/views.py
function handle_view (line 28) | def handle_view(context, view, name, scale):
function handle_views (line 66) | def handle_views(context, model, layer, views, layer_name, scale):
FILE: import_3dm/read3dm.py
function modules_path (line 31) | def modules_path():
function create_or_get_top_layer (line 58) | def create_or_get_top_layer(context, filepath):
function read_3dm (line 67) | def read_3dm(
FILE: test/test_import_3dm.py
function enable_addon (line 20) | def enable_addon():
function test_create_article (line 29) | def test_create_article(filepath):
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (107K chars).
[
{
"path": ".gitattributes",
"chars": 20,
"preview": "*.whl -text\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 612,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise descriptio"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 560,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? "
},
{
"path": ".gitignore",
"chars": 1256,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2018 Nathan Letwory\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "PULL_REQUEST_TEMPLATE.md",
"chars": 393,
"preview": "# Title of the PR changeset (Fix certain issue, or Implement/Add feature)\n\nA short description of the changes\n\n## detail"
},
{
"path": "README.md",
"chars": 589,
"preview": "Import Rhinoceros 3D files in Blender\n=====================================\n\nThis add-on uses the `rhino3dm.py` module\n("
},
{
"path": "import_3dm/__init__.py",
"chars": 10876,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/blender_manifest.toml",
"chars": 2448,
"preview": "schema_version = \"1.0.0\"\n\n# Example of manifest file for a Blender extension\n# Change the values according to your exten"
},
{
"path": "import_3dm/converters/__init__.py",
"chars": 6922,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/annotation.py",
"chars": 12841,
"preview": "# MIT License\n\n# Copyright (c) 2024 Nathan Letwory\n\n# Permission is hereby granted, free of charge, to any person obtain"
},
{
"path": "import_3dm/converters/curve.py",
"chars": 4936,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/groups.py",
"chars": 3870,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/instances.py",
"chars": 5595,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/layers.py",
"chars": 3607,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/material.py",
"chars": 18507,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/pointcloud.py",
"chars": 1718,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/rdk_manager.py",
"chars": 710,
"preview": "import rhino3dm as r3d\nimport xml.etree.ElementTree as ET\n\nclass RdkManager():\n def __init__(self, document : r3d.File3"
},
{
"path": "import_3dm/converters/render_mesh.py",
"chars": 5599,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/utils.py",
"chars": 5379,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/converters/views.py",
"chars": 3075,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "import_3dm/read3dm.py",
"chars": 9350,
"preview": "# MIT License\n\n# Copyright (c) 2018-2024 Nathan Letwory, Joel Putnam, Tom Svilans, Lukas Fertig\n\n# Permission is hereby "
},
{
"path": "requirements.txt",
"chars": 16,
"preview": "rhino3dm>=8.6.0\n"
},
{
"path": "test/pytest.ini_example",
"chars": 344,
"preview": "[pytest]\nblender-executable = C:/Program Files/Blender Foundation/Blender 4.2/blender.exe\npythonpath = C:/Users/USERNAME"
},
{
"path": "test/pytest_setup.md",
"chars": 816,
"preview": "## pytest setup\n\nlocal python needed \ninstall pytest and pytest-blender\n\ninstall pip in blender (on windows/blender 4.2"
},
{
"path": "test/test_import_3dm.py",
"chars": 769,
"preview": "#!python3\nimport pytest\n\nimport bpy\nimport addon_utils\n\n\ntestfiles = [\n \"units/boxes_in_cm.3dm\",\n \"units/boxes_in_"
}
]
// ... and 14 more files (download for full content)
About this extraction
This page contains the full source code of the jesterKing/import_3dm GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (99.5 KB), approximately 26.2k tokens, and a symbol index with 98 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.