Full Code of jesterKing/import_3dm for AI

master 77c53bf97d3c cached
40 files
99.5 KB
26.2k tokens
98 symbols
1 requests
Download .txt
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)
Download .txt
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
Download .txt
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.

Copied to clipboard!