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